개요
컴포넌트를 만들다 보면 필연적으로 간격을 어디에 줄 것인가에 대한 고민에 빠지게 된다. 예를 들어, 리스트에 들어갈 `UserCard`라는 컴포넌트를 만든다고 가정해보자. 리스트 아이템 사이에는 20px의 간격이 필요하다. 이때 가장 쉬운 방법은 `UserCard` 자체에 `margin-bottom: 20px`을 주는 것이다.
"어차피 이 카드는 리스트에서만 쓰니까, 알아서 간격을 가지고 있으면 편하잖아?"
하지만 프로젝트가 커지고 `UserCard`를 다른 곳(가로 스크롤 뷰, 모달 내부, 그리드 레이아웃 등등)에서 재사용하려고 할 때, 내부에 심어둔 이 margin은 굉장히 거슬리는 무언가가 돼버린다. ▼
<div style="text-align: center; margin-bottom: 20px;">
나는 "자식 컴포넌트는 패딩이나 마진 같은 외부 간격에 대해 전혀 몰라야 한다"는 원칙을 고수하는 편이다. 간격은 오로지 부모(컨테이너)의 책임이어야 한다. 왜 그런지 그 이유를 정리해보았다.
캡슐화와 재사용성 (Encapsulation)
컴포넌트는 '어디에 놓이느냐'에 따라 간격이 달라질 수 있다.
- 메인 피드에서는 아래쪽 간격이 20px이어야 한다.
- 사이드바에서는 간격이 10px이어야 한다.
- 가로 스크롤 영역에서는 오른쪽 간격이 15px이어야 한다.
만약 컴포넌트 내부에 margin이 하드코딩 되어 있다면, 우리는 재사용할 때마다 이 스타일을 덮어쓰기(Override) 위해 className을 추가로 받거나 style props를 뚫어줘야 한다.
// ❌ 나쁜 예: 컴포넌트가 외부 간격을 가짐
const UserCard = () => {
return <div className="mb-5 p-4 border...">...</div>; // mb-5(20px)가 박혀있음
};
// 재사용할 때마다 마진을 초기화하거나 덮어씌워야 하는 고통 발생
<UserCard className="mb-0 mr-4" />
컴포넌트는 레고 블록과 같아야 한다. 레고 블록 자체에 '옆 블록과의 거리'가 붙어있지 않다. 블록은 블록의 모양(내부)만 책임지고, 그걸 어디에 꽂을지는 만드는 사람(부모)이 결정한다.
레이아웃의 책임 분리 (Separation of Concerns)
UI 개발에서 무엇(Content)을 보여주는지와 어떻게 배치(Layout)하는지는 철저히 분리되어야 한다.
- 자식 컴포넌트: 나는 내 사이즈와 내 내부 디자인(Padding, Font, Color)만 신경 쓴다.
- 부모 컴포넌트: 나는 자식들을 어떻게 배열하고(Flex, Grid), 자식들 사이에 얼마만큼의 간격을 줄지(Gap) 결정한다.
이 책임이 섞이는 순간, 레이아웃 버그를 잡기 위해 수많은 자식 컴포넌트 파일을 열어봐야 하는 지옥이 펼쳐진다.
해결책: 부모가 제어하는 Gap과 Spacer
그렇다면 간격은 어떻게 처리하는 게 좋을까? CSS의 발전으로 이제는 다소 우아한(?) 세련된(?) 방법들이 존재한다.
Flex/Grid의 gap 속성 활용
가장 이상적인 방법이다. 부모가 `gap`을 주면 자식은 아무런 마진이 없어도 알아서 간격이 생긴다. 자식은 렌더링만 되고, 사이 간격은 부모가 통제한다. ▼
// ✅ 좋은 예: 부모가 간격을 결정
const UserList = () => {
return (
<div className="flex flex-col gap-5"> {/* 부모가 20px 간격 지정 */}
<UserCard />
<UserCard />
<UserCard />
</div>
);
};
const UserCard = () => {
return <div className="p-4 border...">...</div>; // 내부는 자기 할 일만 함
};
Layout 컴포넌트 도입 (Stack, Spacer)
gap을 쓰기 애매하거나, 좀 더 명시적인 레이아웃이 필요하다면 Stack이나 Spacer 같은 레이아웃 전용 컴포넌트를 사용한다. 이는 Braid Design System이나 Chakra UI 같은 곳에서 적극적으로 차용하는 방식이다. ▼
<Stack space="medium">
<Text>Title</Text>
<Text>Description</Text>
</Stack>
Margin considered harmful
나 혼자만 이렇게 생각을 하는 것은 아니다. styled-components의 창시자이자 유명한 프론트엔드 개발자인 Max Stoiber는 이미 2018년에 Margin considered harmful (마진은 해롭다)라는 글을 통해 이 문제를 공론화했다. 그는 컴포넌트 기반 시스템에서 마진이 캡슐화를 깨뜨린다고 강력하게 주장한다.
"Margins break component encapsulation.
A component should not influence anything outside of its own bounding box.""마진은 컴포넌트의 캡슐화를 깨뜨린다. 컴포넌트는 자신의 경계 박스 외부의 어떤 것에도 영향을 미쳐서는 안 된다."
— Max Stoiber
Margin considered harmful
We should ban margin from our React components. Hear me out.
mxstbr.com
그는 마진 대신 Spacer 컴포넌트를 사용하여 요소 사이의 공간을 명시적으로 소유하는 방식을 제안했다. 이는 코드를 읽을 때 '여기 빈
공간이 있구나'를 컴포넌트 레벨에서 바로 인지할 수 있게 해 준다. ▼
<div style="text-align: center; margin-bottom: 20px;">
(이전 플러터 글에서도 이야기했는데, 필자는 그렇게 좋아하지는 않는다. 이 글에 대략적인 이유가 있다.) ▼
[Flutter] SizedBox를 Padding 대신 사용하지 말아야 하는 이유
개요 Flutter로 앱 개발을 하다보면 `Padding`과 `SizedBox`를 굉장히 많이 사용하게 된다. `Padding`은 이름 그대로 위젯에게 padding, 즉 간격을 주는 위젯이기에 많이 사용되고, `SizedBox`는 `Padding`과 같은
noguen.com
+) 마진과 패딩의 방향 규칙 (L-T 원칙)
gap을 지원하지 않는 구형 브라우저를 대응하거나, 부득이하게 마진을 써야 할 때 나는 헷갈림을 방지하기 위해 나만의 규칙을 하나 세워두고 있다. 바로 "마진은 왼쪽(Left)과 위쪽(Top)으로만 준다"는 원칙이다.
우리가 글을 읽는 시선의 방향이 '좌 -> 우', '위 -> 아래'이기 때문이다. 즉, 요소가 렌더링 될 때 "내 앞에 있는 녀석으로부터 나를 밀어낸다"는 개념으로 접근하는 것이다. 누군가는 `margin-bottom`을 주고, 누군가는 `margin-top`을 줘서 간격이 두 배가 되거나 상쇄되는 대참사를 막기 위함이다.
- 기본 원칙: 모든 간격은 `margin-top`, `margin-left`로 밀어낸다.
- 예외: 리스트의 가장 마지막 아이템이거나, 컨테이너의 끝부분 여백(Safe Area)이 필요할 때만 `padding-bottom`이나 `margin-bottom`을 허용한다.
이렇게 방향을 한쪽으로 강제해두면, CSS 코드에서 마진과 패딩이 뒤섞여 '도대체 이 여백은 어디서 온 거야?'라며 개발자 도구를 뒤지는 시간을 획기적으로 줄일 수 있다.
마치며
물론 '그냥 마진 주면 당장 편한데?'라는 유혹을 뿌리치기는 쉽지 않다. 특히 퍼블리싱 단계에서 빠르게 쳐내야 할 때는 더욱 그렇다.
하지만 컴포넌트의 재사용성을 높이고, 유지보수 지옥에서 벗어나고 싶다면
"자식은 다소 멍청하게(내부만), 부모는 보다 똑똑하게(배치만)"
라는 원칙을 지키는 것이 좋다고 생각한다. 처음에는 코드가 조금 늘어나는 것 같아도, 나중에 디자인 변경 사항이 왔을 때 부모 컴포넌트의 `gap`값 하나만 수정하고 홀가분하게 퇴근할 수 있을 것이다.
'Develop > Develop' 카테고리의 다른 글
| [Develop] 국제화 JSON 파일을 페이지별로 분할 관리하기 (0) | 2025.11.02 |
|---|---|
| [Develop] 모델 검증 로직은 어디에 위치하는게 좋을까 (0) | 2025.07.03 |
| [Develop] 제어 주도에 따른 동기와 비동기 (0) | 2025.05.12 |
| [Develop] svg vs png : 뭐가 더 좋을까? (0) | 2025.02.07 |