CSS 박스 모델: margin, padding, border와 box-sizing
이 글의 핵심
박스 모델은 시각적 은유일 뿐 아니라, 브라우저가 ‘어떤 사각형을 어디에 그릴지’를 결정하는 레이아웃 알고리즘의 입력입니다.
CSS 기초에서 선택자와 캐스케이드를 익혔고 이제 레이아웃을 잡을 차례라면, 박스 모델부터 봐야 합니다. Flex·Grid로 넘어가기 전에 “왜 너비가 이상해졌지?”를 규칙으로 설명하려면 여기가 출발선입니다.
box-sizing: border-box는 무조건 쓰십시오
분명하게 말씀드리겠습니다. content-box는 명세의 기본값이고 의미도 명확하지만, 실무 UI에서는 전역 border-box를 적용하는 것이 맞습니다. width: 300px에 padding과 border가 붙으면 content-box에서는 크기가 바깥으로 불어나서 50% + 50% 그리드가 깨지고, 머릿속으로 계속 계산을 해야 합니다.
*,
*::before,
*::after {
box-sizing: border-box;
}
border-box에서는 width가 콘텐츠 + padding + border를 합친 “테두리 박스”에 맞춰집니다. 산수와 씨름할 일이 줄어드는 쪽이 유리합니다. 레거시 코드나 px 단위로 정확히 맞춰야 하는 특수한 경우가 아니라면, 저는 이 설정 없이 새 프로젝트를 시작하지 않습니다.
margin은 box-sizing 값과 관계없이 항상 바깥쪽입니다. width에 포함되지 않습니다. 헷갈린다면 DevTools의 박스 다이어그램을 열어 보십시오.
시각적으로만 비교해 보겠습니다. 같은 width: 300px에 좌우 padding: 20px, border: 2px일 때:
content-box: "300"은 맨 안 content만 → 겉 점유는 300+40+4 … 더 넓어짐
border-box: "300"이 랩 전체(패딩·보더 포함) → 겉 300, 안쪽 콘텐츠만 쪼그라듦
두 방식 모두 명세상 올바르지만, 개인 취향과 별개로 팀 규칙은 border-box로 고정하시기를 권합니다.
margin collapsing 때문에 3시간을 쓴 날
한번은 섹션 간격을 맞추려고 margin-bottom: 2rem을 줬는데, 위아래 섹션 간격이 제 계산과 맞지 않았습니다. Figma 시안과도 달랐습니다. DevTools로 보면 숫자는 맞는데 간격이 절반처럼 보이거나, 아래쪽은 큰데 위쪽은 이상한 식이었습니다. 원인은 결국 margin collapsing(마진 병합)이었습니다. 인접한 블록의 위·아래 margin이 더해지는 것이 아니라 큰 쪽 값으로 합쳐지는 현상입니다. 20+30이 50이 아니라 30이 될 수 있습니다. 그날 3시간 동안은 “CSS가 나를 싫어한다”는 기분만 쌓였습니다.
.box1 { margin-bottom: 30px; }
.box2 { margin-top: 20px; }
/* 사이 실제 간격: 50 아니고 30 (큰 쪽) */
부모에 border도 padding도 없는 상태에서 첫 자식에만 margin-top을 주면, 그 여백이 부모 바깥으로 “튀어나가 보이는” 것도 같은 계열의 현상입니다. “부모에 margin을 준 적이 없는데?” 하며 한참을 찾게 됩니다.
병합을 끊을 때 실제로 쓰는 방법은 다음과 같습니다. 부모에 약간의 padding이나 border: 1px solid transparent를 주는 방법, display: flow-root, overflow를 visible이 아닌 값으로 주는 방법(BFC 생성), 그리고 Flex/Grid + gap으로 간격을 아예 margin 대신 통일하는 방법입니다. 저는 “형제 요소의 위아래 margin을 양쪽 모두 주는” 패턴을 가능하면 쓰지 않습니다. 한쪽 방향만 쓰거나, 아니면 gap을 씁니다.
상자의 층은 이렇게 기억하십시오
표로 정리하는 대신 바깥에서 안쪽으로 감싸는 상자로 그려 보겠습니다.
┌ margin (배경 안 칠해지는 편) ┐
│ ┌ border (경계선) ┐ │
│ │ ┌ padding ────┐ │ │
│ │ │ content │ │ │
│ │ └─────────────┘ │ │
│ └─────────────────┘ │
└──────────────────────────────┘
- content: 말 그대로 텍스트나 이미지
- padding: 테두리 안쪽 영역이며, 보통 배경이 함께 깔립니다. 버튼이나 카드의 “안쪽”을 넓힐 때 사용합니다
- border: 테두리 선입니다. 두께가 공간을 차지합니다(border-box에서는 width 안에 포함됩니다)
- margin: 박스 바깥 영역이며, 배경색이 칠해지지 않습니다
display: inline 요소에는 세로 margin이 적용되지 않는다는 점도 함께 기억해 두십시오. 세로 padding과 border는 그려지기는 하지만 줄 높이 계산에 들어가지 않아서, <a>에 padding: 10px을 주면 배경이 윗줄과 아랫줄 글자 위로 겹쳐 보입니다. 링크나 태그 칩의 클릭 영역을 넓히려고 padding을 줬는데 레이아웃이 이상하다면 display: inline-block이나 inline-flex로 바꾸면 해결됩니다. (CSS 기초에서 본 “표시 방식”과 연결해서 기억하면 좋습니다.)
width / %: “무엇을 기준으로 한 %인가?”
블록 요소에 width: auto를 주면 보통 부모 콘텐츠 너비를 꽉 채우도록 사용값이 정해집니다. width: 100%는 부모 콘텐츠 너비의 100%인데, content-box에서는 여기에 padding과 border가 더해져 가로 스크롤이 생길 수 있습니다. “꽉 차되 넘치지 않게”가 목표라면 auto와 100%의 차이를 꼭 짚고 넘어가십시오.
padding-top: 50%를 줬는데 “왜 가로 너비에 맞춰지지?” 싶다면, 퍼센트의 기준 축이 속성마다 다르기 때문입니다. padding과 margin의 %는 위아래 방향까지 포함해 모두 containing block의 너비를 기준으로 합니다. 부모의 높이는 보통 자식의 크기로 정해지므로, 자식의 세로 여백이 부모 높이를 참조하면 서로가 서로를 기다리는 순환이 생기기 때문입니다. 반면 height: 50%는 부모의 height가 명확히 정해져 있어야 의미가 있고, 부모가 auto면 auto처럼 동작합니다. Computed 탭에서 “무엇의 몇 %“인지 역추적해 보십시오.
min-width / max-width는 반응형 레이아웃과 카드에서 기본적으로 씁니다. Flex 자식이 터무니없이 넓어진다면 min-width: auto와 콘텐츠 크기 때문인 경우가 많은데, 이 부분은 Flexbox 글의 min-width: 0 이야기로 이어집니다.
BFC가 정확히 무엇을 바꾸는가
앞에서 병합을 끊는 방법으로 여러 번 등장한 BFC(Block Formatting Context)는 “블록들이 위에서 아래로 쌓이는 독립된 구역”입니다. 새 BFC를 만든 요소는 세 가지가 달라집니다. 첫째, 자식의 margin이 이 요소의 경계를 넘어 바깥 margin과 병합되지 않습니다. 둘째, 안에 있는 float 자식의 높이까지 포함해 자기 높이를 계산하므로, float만 들어 있어 높이가 0으로 무너지던 부모 문제가 사라집니다. 셋째, 바깥의 float와 겹치지 않고 옆으로 비켜 섭니다.
BFC를 만드는 방법은 여럿이지만 부작용이 각각 다릅니다. overflow: hidden은 가장 오래된 방법이지만 그림자나 드롭다운처럼 박스 밖으로 나와야 하는 내용까지 잘라 버립니다. display: inline-block이나 float는 요소의 배치 방식 자체를 바꿉니다. 부작용 없이 “BFC만 만들기” 위해 추가된 값이 display: flow-root이고, 지원하지 않는 브라우저를 고려할 필요가 없다면 이것을 쓰는 것이 가장 의도가 분명합니다. Flex와 Grid 컨테이너도 자식에게 각각의 formatting context를 만들기 때문에, 그 안에서는 마진 병합이 일어나지 않습니다. 블록 레이아웃에서 쓰던 스타일을 Flex 컨테이너로 옮겼더니 간격이 두 배로 벌어지는 이유가 이것입니다.
padding / border / margin 핵심만
padding 단축 속성은 값이 하나면 전체, 둘이면 상하|좌우, 셋이면 상|좌우|하, 넷이면 상|우|하|좌(시계 방향)입니다. 음수 값은 허용되지 않습니다.
border: 2px solid #333처럼 한 줄로 쓰고, border-radius는 카드나 버튼에 함께 씁니다. border-box를 쓰면 보더가 두꺼워져도 “바깥 width”는 늘어나지 않으므로 계산이 단순해집니다.
margin: 0 0 1rem처럼 한 방향만 쓰는 습관을 들이면, 병합과 양쪽 중복 문제를 동시에 줄일 수 있습니다. 병합 규칙을 정확히 적으면 양수 margin끼리는 큰 값, 음수 margin끼리는 절댓값이 큰 값, 양수와 음수가 만나면 둘의 합이 적용되며, 이 규칙은 세로 방향에만 해당합니다. 가로 margin은 어떤 경우에도 병합되지 않습니다. 가로 가운데 정렬은 width(또는 max-width)가 지정된 블록에 margin-inline: auto를 주면 됩니다. 인라인 요소에 auto만 주는 것은 효과가 없을 수 있습니다.
음수 margin은 요소를 끼워 맞추거나 겹칠 때 쓰지만, 포커스 링이나 터치 영역이 꼬이면 접근성 문제가 생깁니다. 정말 필요할 때만 쓰십시오.
outline vs border: 박스 바깥에 겹쳐 그리는 선
border는 (box-sizing에 따라) 레이아웃 수치에 영향을 줍니다. outline은 겹쳐 그리는 외곽선이라 공간을 차지하지 않습니다. 포커스 링에 자주 쓰이며, 디버깅할 때는 *에 outline: 1px solid rgba(255,0,0,0.35) 같은 스타일을 잠깐 씌워 보면 편합니다. outline: none만 적용하지는 마십시오. 대체 포커스 스타일이 없으면 키보드 사용자는 현재 위치를 잃어버립니다.
DevTools
Elements → Layout/Computed 탭에서 박스를 시각적으로 보여 줍니다. 마진 병합인지, 100%가 어디서 넘쳤는지, box-sizing이 전역 * 선택자에 적용되었는지를 여기서 확인하십시오. 의심해 볼 순서는 (1) 마진 병합 (2) 자식의 100% + 부모의 padding (3) 스크롤 컨테이너의 크기 계산입니다.
overflow: auto / hidden은 콘텐츠 잘라내기뿐 아니라 병합·BFC와도 연관되므로, “뭔가 끊겼다” 싶을 때 함께 확인하십시오. border-radius + overflow: hidden은 이미지 클리핑에 자주 쓰는 패턴입니다.
실제로 적용하면
카드에는 box-sizing: border-box, max-width, border + border-radius, overflow: hidden을 자주 씁니다. card__body에는 padding을 주고, 제목과 문단의 margin은 한 방향으로만 줘서 리듬을 맞춥니다.
.card {
box-sizing: border-box;
max-width: 300px;
border: 1px solid #e5e5e5;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
}
.card__body { padding: 20px; }
.card__title { margin: 0 0 0.5rem; }
섹션 간격은 형제 요소 양쪽의 margin에 기대지 말고 부모 padding + gap으로 잡으십시오. Flex/Grid를 쓰면 gap은 마진 병합과 충돌하지 않습니다.
.vstack { display: flex; flex-direction: column; gap: 1rem; }
미디어 요소에 max-width: 100%를 주는 것도 잊지 마십시오. width/margin/font-size 변경은 reflow를 자주 일으키므로, 애니메이션은 되도록 transform/opacity로 처리하십시오.
브라우저 기본 h1 margin 같은 부분은 reset/normalize에 관한 팀 규칙에 맡기되, 전역 border-box는 그 규칙의 첫 줄에 두는 것이 좋다고 봅니다. 제 의견은 앞에서 말씀드린 대로입니다.
이어 읽기
같이 보면 좋은 글
- CSS 기초 | 선택자, 속성, 색상, 폰트
- CSS position 심화 | 포함 블록, 쌓임 맥락, 레이아웃 영향
- CSS Flexbox 입문: flex-direction, justify-content, align-items로 레이아웃 잡기
- CSS Grid 레이아웃: grid-template-areas, fr·minmax·auto-fit, Flexbox와 함께 쓰기