Skip to content
This page has been auto-translated and may contain errors.View in English

박스 모델

docs.scrimba.com

CSS로 요소 간 간격을 조절하기 시작하면 누구나 한 번쯍 겪는 문제가 있습니다. 너비를 지정하고 패딩을 조금 넣었더니, 요소가 지정한 값보다 더 넓게 나오는 것입니다. 두 문단이 숫자로 계산한 것보다 더 가깝게 붙어 있기도 합니다. 이것은 버그가 아닙니다. 각 요소가 차지하는 공간을 결정하는 규칙인 박스 모델 때문이며, 이 그림을 한 번 머릿속에 넣어두면 간격에 관한 대부분의 의외의 상황이 더 이상 의외가 아니게 됩니다.

모든 요소는 박스입니다

CSS는 페이지 위의 모든 요소를, 단어 하나나 링크 하나까지도 사각형으로 취급합니다. 이 사각형은 안쪽에서 바깥쪽으로 쌓인 네 개의 층으로 이루어지는데, 이것이 바로 **박스 모델**입니다. 가운데에 콘텐츠가 있고, 그 주위에 패딩, 그다음 테두리, 가장 바깥에 마진이 있습니다.

벽에 걸린 액자 사진을 떠올려 보세요. 사진이 콘텐츠입니다. 사진 주위의 흰 매트는 패딩으로, 액자 안쪽의 여유 공간입니다. 나무 액자 틀 자체는 테두리입니다. 그리고 이 액자와 옆 액자 사이의 빈 벽면이 마진입니다. 스타일을 적용하는 모든 요소는 안쪽에서 바깥쪽으로 이렇게 똑같은 방식으로 구성됩니다.

css
.card {
  padding: 16px;              /* 콘텐츠 안쪽 여백 */
  border: 2px solid #333;     /* 액자 틀 */
  margin: 24px;               /* 바깥쪽 여백, 이웃 요소를 밀어냄 */
}

브라우저가 렌더링하는 모든 요소는 사각형 박스이며, 이 박스는 가운데에서 바깥쪽으로 측정되는 네 부분으로 이루어집니다: 콘텐츠 박스(텍스트나 이미지가 들어가는 곳), 패딩, 테두리, 마진입니다. CSS는 어떻게 동작하는가에서 이 개념을 이미 본 적이 있을 텐데, 박스 모델은 그중에서 크기와 간격을 결정하는 부분입니다.

이 순서가 중요한 이유는 각 층이 이전 층을 감싸기 때문입니다. 패딩은 콘텐츠와 테두리 사이에 있으므로, 배경색은 콘텐츠와 패딩을 채우고 테두리의 바깥쪽 경계에서 멈춥니다. 마진은 테두리 바깥에 있으며 항상 투명한데, 그래서 마진에는 색을 지정할 수 없습니다.

css
.card {
  padding: 16px;              /* 테두리 안쪽, 배경색이 함께 적용됨 */
  border: 2px solid #333;
  margin: 24px;              /* 테두리 바깥, 항상 투명 */
  background: #f4f4f4;       /* 콘텐츠 + 패딩을 채우고 테두리에서 멈춤 */
}

렌더링되는 모든 요소는 적어도 하나의 사각형 **박스**를 생성하며, 박스 모델은 그 안에 중첩된 네 영역에 대한 명세입니다. 안쪽에서 바깥쪽 순서로 보면: 콘텐츠 박스(요소의 텍스트, 이미지, 자식 박스가 들어가는 영역), 패딩 박스(콘텐츠에 패딩을 더한 것으로, 투명하지만 요소의 배경색이 여전히 적용되는 공간), 테두리 박스(패딩 박스에 테두리를 더한 것으로, 눈에 보이는 경계), 마진 박스(테두리 박스에 마진을 더한 것으로, 요소를 이웃 요소와 분리시키는 가장 바깥의 투명한 영역)입니다.

이 중첩 구조에서 나중에 발목을 잡는 두 가지 결과가 있습니다. 첫째, 배경색은 패딩 박스까지 칠해지고 마진 박스에는 칠해지지 않으므로, 패딩은 클릭 가능하고 색을 입힐 수 있는 내부 공간을 주지만 마진은 스타일을 줄 수 없는 빈 분리 공간입니다. 둘째, 여러분이 선언하는 width와 height는 기본적으로 박스 전체를 나타내지 않으며, 오직 그중 한 영역만을 나타냅니다. 그 영역이 어느 것인지는 box-sizing에 따라 달라지며, 이는 다음 절에서 정확히 다룹니다.

css
.card {
  padding: 16px;
  border: 2px solid #333;
  margin: 24px;
  background: #f4f4f4;       /* 콘텐츠 + 패딩을 칠하고, 마진에는 칠해지지 않음 */
}
Juno모든 요소는 박스입니다 모든 요소는 안쪽에서 바깥쪽 순서로 콘텐츠, 패딩, 테두리, 마진이라는 네 층으로 이루어진 박스입니다. 액자 사진 이미지가 저한테는 오래 남았어요. 사진, 매트, 액자 틀, 다음 액자까지의 간격. 이 그림을 머릿속에 새겨두면 이 장의 나머지 내용은 자연스럽게 따라옵니다.
Juno모든 요소는 박스입니다 콘텐츠, 패딩, 테두리, 마진이 그 순서대로 바깥쪽으로 감싸고 있습니다. 배경색은 콘텐츠와 패딩을 채우고 테두리에서 멈추며, 마진은 항상 투명하기 때문에 margin에는 색을 줄 수 없습니다. 이 중첩 순서를 정확히 붙잡아 두면 간격 규칙이 더 이상 임의로 느껴지지 않습니다.
Juno모든 요소는 박스입니다 네 개의 중첩 영역: 콘텐츠 박스, 패딩 박스, 테두리 박스, 마진 박스입니다. 배경색은 테두리 박스까지 칠해지고 마진에는 절대 칠해지지 않으므로, 패딩은 색을 입힐 수 있는 내부이고 마진은 빈 분리 공간입니다. 그리고 여러분이 선언한 width는 이 영역 중 오직 하나만의 크기를 정하는데, 이것이 바로 다음 절에서 box-sizing을 다루는 이유입니다.

padding, border, margin

세 가지 간격 층은 각각 하나의 역할을 합니다. **패딩**은 안쪽 공간으로, 콘텐츠와 테두리 사이에 있습니다. 마진은 바깥쪽 공간으로, 다른 요소를 밀어냅니다. 테두리는 그 사이에 그려진 선입니다.

네 면에 한 번에 하나의 값을 지정할 수도 있고, 특정 면만 지정할 수도 있습니다:

css
.card {
  padding: 20px;             /* 네 면 모두 20px */
  margin-bottom: 32px;       /* 아래쪽 면만 */
}

padding: 20px 같은 축약형은 위, 오른쪽, 아래, 왼쪽을 한 번에 지정하는 빠른 방법입니다. 한 면만 다르게 지정해야 할 때는 margin-bottom 같은 구체적인 속성을 사용하세요.

**패딩**은 테두리 안쪽 공간이고, 마진은 테두리 바깥쪽 공간이며, 테두리는 그 사이의 경계선입니다. 기억해야 할 규칙은 마진은 이웃 요소를 밀어내고, 패딩은 요소 자신의 내부를 넓힌다는 것입니다. 배경이 텍스트에 비해 답답해 보인다면 패딩을 조정할 일이고, 두 요소가 너무 가까이 붙어 있다면 마진을 조정할 일입니다.

각각 위에서부터 시계방향으로 면을 채우는 축약형을 가지고 있습니다. 값이 하나면 네 면 모두에 적용되고, 두 개면 세로 다음 가로 순서로 지정되며, 네 개면 위, 오른쪽, 아래, 왼쪽 순서로 지정됩니다:

css
.hero {
  padding: 40px 24px;        /* 위아래 40px, 좌우 24px */
  margin: 0 auto;            /* 세로 마진 없음, 좌우는 auto로 가운데 정렬 */
}

.card {
  padding-left: 16px;        /* 한 면만 다를 때는 개별 속성 사용 */
}

패딩, 테두리, 마진은 같은 간격을 부르는 세 가지 이름이 아니라, 서로 다르게 동작하는 세 가지 독립적인 간격 제어 수단입니다. 패딩은 테두리 안쪽의 투명한 영역으로, 여전히 배경색이 적용되고 클릭에도 반응합니다. 마진은 테두리 바깥쪽의 투명한 영역으로, 요소를 형제 요소와 분리하며 배경색이 적용되지 않습니다. 테두리는 실제로 그려지는 경계선이며, 자신의 너비만큼 레이아웃 공간도 차지합니다.

세 속성 모두 시계방향 축약형(하나, 둘, 셋, 넷의 값이 위 / 오른쪽 / 아래 / 왼쪽으로 확장됨)을 받아들이고, 각각 개별 면을 지정하는 롱핸드 속성도 있습니다. 정확히 알아둘 만한 두 가지 동작이 있습니다. 마진은 음수가 될 수 있어서 요소를 이웃 요소 쪽으로, 혹은 그 위로 끌어당길 수 있고 이는 정당한 레이아웃 기법입니다. 반면 패딩은 0보다 작아질 수 없습니다. 그리고 margin: 0 auto는 요소에 명확한 너비가 지정되어 있을 때만 블록을 가로로 가운데 정렬합니다. auto는 남아 있는 가로 공간을 나눠서 값을 결정하는데, 박스가 이미 컨테이너를 다 채우고 있다면 나눌 남은 공간이 없기 때문입니다.

css
.hero {
  padding: 40px 24px;        /* 시계방향 쌍: 세로, 가로 */
  margin: 0 auto;            /* .hero에 너비가 지정되어 있을 때만 가운데 정렬됨 */
  border-bottom: 1px solid #ddd;  /* 테두리 롱핸드, 한쪽 면, 1px의 레이아웃 공간을 차지 */
}

.card {
  margin-top: -8px;          /* 음수 마진이 카드를 위쪽으로 끌어당김 */
}
Junopadding, border, margin 패딩은 테두리 안쪽 공간, 마진은 테두리 바깥쪽 공간, 테두리는 그 사이의 선입니다. 박스 안에서 텍스트가 답답하게 느껴지면 padding을 쓰고, 두 박스가 너무 가까이 붙어 있으면 margin을 쓰세요. padding: 20px 같은 값 하나로 네 면 모두를 지정할 수 있고, 한 면만 바꿔야 할 때는 margin-bottom처럼 면을 지정하면 됩니다.
Junopadding, border, margin 패딩은 요소 자신의 내부를 넓히고, 마진은 이웃 요소를 밀어냅니다. 어느 쪽이 필요한지 알면 추측할 일이 훨씬 줄어듭니다. 축약형은 위에서부터 시계방향으로 진행됩니다. 값 하나면 모든 면, 두 개면 세로 다음 가로입니다. 배경이 텍스트를 너무 꽉 감싸고 있다면 필요한 건 패딩이지 마진이 아닙니다.
Junopadding, border, margin 패딩은 배경색과 클릭을 처리하고, 마진은 투명한 분리 공간이며, 테두리는 자신의 너비만큼 실제 레이아웃 공간을 차지합니다. 마진은 음수가 되어 요소를 서로 끌어당길 수 있지만, 패딩은 그럴 수 없습니다. 그리고 margin: 0 auto는 명확한 너비를 가진 블록만 가운데 정렬합니다. 그렇지 않으면 auto가 나눠 가질 남은 공간이 없기 때문입니다.

box-sizing: content-box vs border-box

거의 모든 사람이 걸려드는 함정이 여기 있습니다. 카드에 width: 300px를 지정하고 패딩을 조금 넣었더니, 화면에 나온 카드가 300픽셀보다 더 넓어져 있습니다. 여러분이 뭔가를 잘못한 게 아닙니다. 기본적으로 width는 가운데의 콘텐츠만 측정하고, 패딩과 테두리는 그 위에 추가로 더해지기 때문입니다.

너비를 기대한 대로 동작하게 만드는 한 줄짜리 해결책이 있습니다. box-sizing: border-box를 지정하면, 이제 여러분이 입력한 너비는 패딩과 테두리를 포함한 박스 전체의 너비가 됩니다:

css
.card {
  box-sizing: border-box;    /* 이제 width에 패딩과 테두리가 포함됨 */
  width: 300px;
  padding: 20px;             /* 카드는 340px가 아니라 300px 너비를 유지함 */
}

기본적으로 모든 요소는 box-sizing: content-box를 사용하며, 이는 여러분이 지정한 width가 콘텐츠 박스에만 적용된다는 뜻입니다. 패딩과 테두리는 그 바깥에 추가로 더해지므로, 실제로 화면에 보이는 박스는 입력한 숫자보다 더 넓습니다. 이것이 수많은 레이아웃을 망가뜨리는 대표적인 크기 계산 함정입니다.

계산을 한 번 해보면 명확해집니다. content-box에서는 300px 너비에 양쪽 각각 20px 패딩과 양쪽 각각 2px 테두리가 더해져 300 + 40 + 4 = 344px로 렌더링됩니다. **border-box**로 바꾸면 이제 width는 테두리 박스를 의미합니다. 즉 패딩과 테두리가 안쪽에서 빠져나가므로, 요소는 정확히 300px를 유지하고 콘텐츠가 그만큼 줄어들어 맞춰집니다.

css
.card {
  box-sizing: content-box;   /* 기본값 */
  width: 300px;
  padding: 20px;
  border: 2px solid #333;
  /* 실제 렌더링 너비: 300 + 20 + 20 + 2 + 2 = 344px */
}

거의 항상 이 방식을 원하기 때문에, 전체 선택자를 사용해서 모든 요소에 전역으로 적용하는 것이 표준적인 방법입니다:

css
* {
  box-sizing: border-box;    /* 규칙 하나로 어디서나 예측 가능한 크기 계산 */
}

box-sizing은 여러분의 widthheight가 어느 영역을 측정하는지를 결정하며, 두 가지 값을 가집니다. 초기값은 content-box입니다. width는 콘텐츠 박스를 지정하고, 패딩과 테두리는 그 너비 바깥에 배치되므로, 테두리 박스, 즉 페이지에서 실제로 차지하는 면적은 width + padding-left + padding-right + border-left + border-right가 됩니다. 다른 옵션은 **border-box**입니다. width가 테두리 박스를 직접 지정하고, 브라우저는 패딩과 테두리를 안쪽에서 빼서 콘텐츠 박스를 남은 만큼으로 줄입니다.

이 계산은 정확히 짚어볼 가치가 있습니다. 이 속성이 존재하는 이유 전체가 여기에 있기 때문입니다. width: 300px; padding: 20px; border: 2px인 경우:

css
/* content-box (기본값): 패딩과 테두리가 위에 추가됨 */
.card {
  box-sizing: content-box;
  width: 300px;
  padding: 20px;
  border: 2px solid #333;
  /* 테두리 박스 = 300 + 40 + 4 = 344px; 콘텐츠 박스 = 300px */
}

/* border-box: 패딩과 테두리가 지정한 너비를 안쪽에서 갉아먹음 */
.card {
  box-sizing: border-box;
  width: 300px;
  padding: 20px;
  border: 2px solid #333;
  /* 테두리 박스 = 300px; 콘텐츠 박스 = 300 - 40 - 4 = 256px */
}

border-box를 추천합니다. 크기 계산을 조합하기 쉽게 만들어 주기 때문입니다. 실용적인 이유가 있습니다. border-box를 사용하면 width: 25%로 지정한 여러 칼럼이 어떤 패딩을 추가하더라도 컨테이너의 4분의 1을 유지하는데, 패딩이 더 이상 바깥쪽 크기를 바꾸지 않기 때문입니다. 이 예측 가능성 때문에 거의 모든 곳에서 쓰이는 리셋 코드가 이를 모든 요소, 보통은 가상 요소까지 포함해서 적용하며, 아무것도 다시 content-box로 돌아가지 않도록 합니다:

css
*, *::before, *::after {
  box-sizing: border-box;
}

정확히 짚어둘 점 하나: 이 속성은 오직 widthheight만 좌우합니다. 마진은 box-sizing과 무관하게 항상 테두리 박스 바깥에 있으므로, 지정한 크기에 절대 포함되지 않습니다. 이는 아래에서 다룰 마진 상쇄와 직결됩니다. 이 박스들의 크기를 안정적으로 지정할 단위가 필요하다면, 크기 단위에서 퍼센트, rem, 뷰포트 단위가 어떻게 계산되는지 다룹니다.

Junobox-sizing: content-box vs border-box 기본적으로 여러분이 지정한 width는 가운데의 콘텐츠만 측정하기 때문에, 패딩과 테두리가 더해지면 박스가 입력한 숫자보다 더 커집니다. box-sizing: border-box를 지정하면 이 문제가 해결됩니다. 이제 너비는 패딩과 테두리를 포함한 박스 전체를 의미합니다. 저도 이것 때문에 오랫동안 헷갈렸으니, 일찍 기억해두면 좋습니다.
Junobox-sizing: content-box vs border-box 기본값인 content-boxwidth가 콘텐츠만 측정한다는 뜻이므로, 패딩과 테두리가 실제 박스를 지정한 값보다 더 넓게 밀어냅니다. 20px 패딩과 2px 테두리가 있는 300px 카드는 실제로 344px로 렌더링됩니다. border-box로 바꾸면 너비가 박스 전체를 의미하게 되므로, 대부분의 스타일시트에서 *에 전역으로 이를 지정하는 것이 표준적인 첫 줄입니다.
Junobox-sizing: content-box vs border-boxcontent-box는 지정한 너비 바깥에 패딩과 테두리를 더하므로 300px 박스가 344px가 되고, border-box는 그것들을 안쪽에서 빼서 300px를 유지합니다. 패딩이 바깥쪽 크기를 더 이상 바꾸지 않으므로 퍼센트 너비를 조합하기 쉬워진다는 점에서 border-box를 사용하세요. *와 가상 요소에까지 적용해서 아무것도 조용히 기본값으로 돌아가지 않게 하세요.

마진 상쇄

한 가지 더 놀랄 만한 상황이 있는데, 이번엔 마진이 겹쳐 쌓이는 방식에 관한 것입니다. 문단 두 개를 위아래로 놓고 첫 번째에 아래쪽 마진 24px, 두 번째에 위쪽 마진 16px를 주면, 둘 사이에 40px의 간격이 생길 거라고 예상할 수 있습니다. 하지만 실제로는 24px가 나옵니다.

이것을 **마진 상쇄**라고 부릅니다. 두 세로 마진이 만나면, 브라우저는 이 둘을 더하지 않습니다. 둘 중 더 큰 값만 유지하고 작은 값은 버립니다. 그래서 더 큰 마진이 이기고, 간격은 40px가 아니라 24px가 됩니다. 이 현상은 위쪽과 아래쪽 마진에서만 일어나며, 패딩에서는 절대 일어나지 않습니다.

css
.intro {
  margin-bottom: 24px;
}
.details {
  margin-top: 16px;
  /* 둘 사이의 간격은 더 큰 마진인 24px이며, 40px가 아님 */
}

한 블록의 아래쪽 마진이 다음 블록의 위쪽 마진과 만나면, 두 값은 더해지지 않습니다. 둘 중 더 큰 값과 같은 하나의 마진으로 상쇄됩니다. 한 문단에 margin-bottom: 24px를 주고 다음 문단에 margin-top: 16px를 주면, 둘 사이의 간격은 40px가 아니라 24px가 됩니다. 이것이 **마진 상쇄**이며, 세로 마진에서만 발생합니다.

이를 알아둘 필요가 있는 이유는 간격을 추론하는 방식이 바뀌기 때문입니다. 간격이 두 숫자의 합보다 작다면 아마 상쇄가 일어나고 있는 것이며, 작은 마진 값을 늘려봐도 더 큰 마진을 넘어서기 전까지는 아무 변화가 없습니다. 세로 리듬을 깔끔하게 제어하는 방법은 한쪽 방향으로만 마진을 지정하는 것입니다. 예를 들어 모든 블록에 margin-bottom만 주고 margin-top은 주지 않으면, 상쇄가 해소할 대상 자체가 없어집니다.

css
.article > * {
  margin-top: 0;
  margin-bottom: 24px;       /* 한쪽 방향으로만 지정해서 간격을 예측 가능하게 함 */
}

**마진 상쇄**는 일반 흐름(normal flow)에서 인접한 세로 마진이 하나로 합쳐지는 규칙으로, 그 크기는 둘 중 더 큰 값(또는 가장 큰 양수값이며, 음수가 있을 경우 가장 큰 양수와 가장 작은 음수를 더해서 계산)이 됩니다. 일반 흐름이란 flex, grid, float 등이 바꾸기 전, 블록 박스가 위에서 아래로 기본적으로 쌓이는 방식을 말합니다. .intro에 아래쪽 마진 24px, .details에 위쪽 마진 16px를 주면 간격은 24px가 되는데, 브라우저가 두 값을 하나의 상쇄된 마진으로 처리하기 때문입니다.

정확히 알아둘 부분은 무엇이 상쇄되고 무엇이 상쇄되지 않는지인데, 이 예외 상황들이 바로 놀라운 결과를 막는 방법입니다:

  • 세로 마진에만 영향을 줍니다. 가로 마진은 절대 상쇄되지 않으며, 나란히 있는 두 요소는 서로 마주한 마진을 항상 더합니다.
  • 두 마진 사이에 무언가가 있으면 상쇄가 멈춥니다. 테두리, 패딩, 또는 자식의 마진과 부모 자신의 마진 사이에 있는 콘텐츠가 그렇습니다.
  • 마진은 포매팅 컨텍스트 경계를 넘어서 상쇄되지 않습니다. flex나 grid 컨테이너는 자식 요소를 위한 새로운 포매팅 컨텍스트를 만들기 때문에, 그 안의 아이템들은 마진이 절대 상쇄되지 않습니다. 이것이 float 시절의 레이아웃보다 현대적인 레이아웃이 더 예측 가능하게 느껴지는 이유 중 하나입니다.
  • 부모와 그 첫 번째 또는 마지막 자식 사이에 아무것도 없으면, 이 둘은 서로를 뚫고 상쇄될 수 있습니다. 그래서 자식의 위쪽 마진이 예상치 못하게 부모 바깥으로 나타날 수 있습니다. 부모에 padding-top이나 테두리를 추가하면 이를 막을 수 있습니다.
css
/* 상쇄가 일어남: 일반 흐름에서 인접한 형제 요소 */
.intro { margin-bottom: 24px; }
.details { margin-top: 16px; }   /* 간격은 24px로 상쇄됨 */

/* 상쇄가 멈춤: 두 마진 사이에 테두리가 있음 */
.section {
  border-top: 1px solid #eee;    /* 이제 두 마진이 모두 적용되고, 합쳐지지 않음 */
}
Juno마진 상쇄 아래쪽 마진이 위쪽 마진과 만나면, 브라우저는 이 둘을 더하는 대신 더 큰 값을 유지하고 작은 값을 버립니다. 그래서 아래쪽 24px가 위쪽 16px를 만나면 40px가 아니라 24px 간격이 나옵니다. 이 현상은 위쪽과 아래쪽 마진에서만 일어나며, 처음 겪으면 누구나 놀랍니다.
Juno마진 상쇄 블록 사이의 세로 마진은 합이 아니라 둘 중 더 큰 값으로 상쇄되므로, 간격이 숫자로 예상한 것보다 작을 수 있습니다. 이런 상황에서는 더 큰 마진을 넘어서기 전까지 작은 마진 값을 늘려도 아무 변화가 없습니다. 모든 블록에 margin-bottom만 주는 식으로 한쪽 방향으로만 마진을 지정하면 이 문제를 통째로 피할 수 있습니다.
Juno마진 상쇄 일반 흐름에서 인접한 세로 마진은 더 큰 값으로 합쳐지고, 가로 마진은 절대 그렇지 않습니다. 그 사이에 테두리나 패딩이 있으면 상쇄가 멈추고, flex와 grid 컨테이너는 새로운 포매팅 컨텍스트를 만들기 때문에 상쇄를 완전히 막습니다. 부모와 첫 번째 자식이 만나는 경우를 주의하세요. 자식의 위쪽 마진이 부모 바깥으로 빠져나갈 수 있는데, padding-top이나 테두리를 추가해야 이를 붙잡아둘 수 있습니다.