CSS 이야기: 마진 병합(collapsing margins)

CSS를 이용해서 블록 레벨 요소를 배치하다보면 간혹 수직 방향으로 마진이 적용되지 않을 때가 있습니다. 이런 현상은 대부분 CSS의 중요한 레이아웃 모델 중 하나인 마진 병합(collapsing margins) 때문에 발생하는데 브라우저에 따라서 다르게 나타나는 경우가 많기 때문에 크로스 브라우징에 어려움을 주기도 합니다.

사실 마진 병합은 블록 레벨 요소의 height 결정 방식이나 float, overflow같은 다른 여러 가지 내용과 연결되어 있습니다. 이 글에서는 마진 병합의 정의와 계산 방법, 그리고 일반적인 문서 흐름(normal-flow)에서의 세 가지 마진 병합 패턴에 대해서만 알아보고 나머지 내용은 다음 글에서 이어가도록 하겠습니다.

† 기본적으로 마진 병합은 hasLayout 모델을 쓰는 IE 7 이하 브라우저에서는 정확하게 표현되지 않을 가능성이 큽니다. 관련된 버그도 상당히 많고요. 그러니 직접 테스트를 해보실 분들은 반드시 IE 8이나 파이어폭스, 오페라, 사파리 등의 브라우저를 사용하시기 바랍니다. ^^;

그리고 언제나 그렇듯이 설명에 오류가 있을 수도 있습니다. 오류를 알려주시면 즉시 반영하도록 하겠습니다.

마진 병합의 정의

먼저 마진 병합이 정확하게 무엇을 뜻하는지 알아봐야겠지요. CSS 2.1 박스 모델의 마진 병합 설명에는 이렇게 정의되어 있습니다(세부적인 조건은 뒤에서 따로 설명합니다).

“마진 병합은 인접한 두 개 이상의 수직 방향 박스 마진이 하나의 마진으로 합쳐지는 것을 의미한다.”

무엇보다 마진 병합은 수직 방향의 마진, 다시 말해서 margin-topmargin-bottom에만 의미를 갖습니다. 따라서 margin-leftmargin-right는 전혀 그 고려 대상이 아닙니다.

또한, 두 개 이상의 마진이 인접했다는 것은 마진의 방향(top 또는 bottom)과 상관 없이 최소한 두 개의 마진이 서로 닿아있다는 의미입니다. 그렇다면 마진이 서로 닿으려면 어떻게 배치되어야 할까요? 크게 세 가지 경우가 있습니다.

  1. 두 요소가 형제(sibling) 관계일 때 위에 있는 요소의 margin-bottom과 아래 있는 요소의 margin-top이 서로 닿는 경우
  2. 두 요소가 부모, 자식 관계일 때 부모 요소의 margin-top과 자식 요소의 margin-top이 서로 닿는 경우(margin-bottom의 경우도 성립)
  3. 한 요소의 margin-topmargin-bottom이 서로 닿는 경우

두 번째와 세 번째 경우를 생각해보면 한 가지 중요한 사실을 알 수 있습니다. CSS의 박스 모델에서 하나의 박스는 안에서 밖으로 봤을 때 “컨텐츠 영역” -> “패딩 영역” -> “보더 영역” -> “마진 영역”으로 구성됩니다. 그런데 일반적으로 부모와 자식 요소의 margin-top이 서로 닿으려면 부모 요소와 첫 번째 자식 요소 사이에 컨텐츠와 패딩, 보더 영역이 있어서는 안 됩니다. 한 요소 안에서 서로 닿으려면 해당 요소가 비어 있고 패딩, 보더가 없어야 하고요.

요소간의 관계로 미루어보면 두 요소(또는 한 요소)의 마진만 합쳐지는 것으로 생각할 수 있지만 실제로는 두 가지 이상의 패턴이 동시에 적용될 수 있습니다. 그래서 위의 정의에서도 “두 개 이상의 마진”이라고 표현하고 있지요.

여기에서는 최대한 간략하게 마진 병합을 정의했는데 실제로 마진이 합쳐지려면 여러 가지 조건을 만족해야 합니다. 이런 조건들은 나중에 설명하기로 하고 먼저 병합되는 마진 값이 어떻게 계산되는지 알아보겠습니다.

병합되는 마진 값 계산 방법

두 개 이상의 마진이 하나로 병합되어 만들어지는 단일 마진 값은 아래 규칙에 따라서 계산됩니다.

  • 마진의 부호가 모두 같으면 절대값이 큰 쪽의 마진이 적용된다.
  • 부호가 다른 마진이 있으면 양수 마진의 최대값에서 음수 마진의 절대값 중 최대값을 뺀다.

몇 가지 예를 들어보겠습니다.

조건 마진 A (절대값) 마진 B (절대값) 마진 C (절대값) 적용되는 마진 값
둘 다 양수 10px (10) 30px (30) - 30px
둘 다 음수 -10px (10) -30px (30) - -30px
셋 다 양수 10px (10) 20px (20) 30px (30) 30px
셋 다 음수 -10px (10) -20px (20) -30px (30) -30px
양수와 음수 A 10px (10) -30px (30) - 10 – 30 = -20(px)
양수와 음수 B -10px (10) 30px (30) - 30 – 10 = 20(px)
양수와 음수 C 10px (10) -20px (20) 30px (30) 30 – 20 = 10(px)
양수와 음수 D -10px (10) 20px (20) -30px (30) 20 – 30 = -10(px)

세 가지 마진 병합 패턴

마진이 서로 닿게 되는 세 가지 경우를 앞서 살펴봤습니다. 마진 병합은 그 각각의 경우에 대응되는 형제 요소간의 병합, 부모와 자식 요소간의 병합, 자체 병합의 세 가지 패턴으로 구분할 수 있습니다. 복잡한 상황은 미뤄두고 일단 가장 간단한 사례를 생각해보겠습니다.

패턴 1. 형제 요소간의 병합

일반적인 문서 흐름에서 두 형제 요소가 인접하는 경우로 마크업 예는 아래와 같습니다.

<div style="margin-bottom: 10px; height: 30px; background-color: blue;"></div>
<div style="margin-top: 30px; height: 30px; background-color: orange;"></div>

마진 병합을 제대로 처리하는 브라우저에서는 아래 이미지처럼 랜더링됩니다.

형제 요소간의 마진 병합

블록 A의 margin-bottom과 블록 B의 margin-top이 하나로 합쳐져서 30px로 표현됩니다. 가장 흔히 발생하는 패턴으로 이해하기도 쉽습니다. 형제 요소(블록 B)가 clearance를 가질 때를 제외하면 언제나 성립하는데 이에 관해서는 나중에 설명하겠습니다.

패턴 2. 부모, 자식 요소간의 병합

일반적인 문서 흐름에서 부모, 자식 요소간의 마진 병합은 margin-topmargin-top이 인접하는 경우와 margin-bottommargin-bottom이 인접하는 두 가지 경우에 발생하며 각각의 성립 조건이 약간 다릅니다. 먼저 조금 더 간단한 margin-top의 경우를 살펴보지요.

<div style="margin-top: 10px; background-color: blue;">
	<div style="margin-top: 30px; height: 30px; background-color: orange;"></div>
</div>

직관적이지는 않지만 아래처럼 렌더링되는 것이 정상입니다.

부모, 자식 요소간의 마진 병합

언듯 생각하면 부모 요소와 자식 요소에 margin-top이 따로 적용되어야 할 것 같지만 두 요소를 갈라 놓는 컨텐츠, 패딩, 보더, clearance가 없기 때문에 두 마진이 30px의 단일 마진으로 합쳐집니다. 그런데 합쳐진 마진은 왜 자식 요소가 아닌 부모 요소에 적용될까요?

사실 마진 병합은 블록 레벨 요소의 height 결정 방식과 밀접한 관계가 있습니다. 일반적인 문서 흐름에서 overflow의 산출 값이 visible인 블록 레벨 요소의 높이는 다음과 같이 정의됩니다(CSS 2.1 10.6.4 참고).

overflow 속성의 산출 값이 visible인 블록 레벨 요소가 블록 레벨 자식을 가질 때 부모 요소의 height는 가장 위에 있는 블록 레벨 자식(자체적으로 마진이 병합되지 않는)의 상단 보더 경계(top border-edge)에서 가장 아래에 있는 블록 레벨 자식(자체적으로 마진이 병합되지 않는)의 하단 보더 경계(bottom border-edge)까지의 거리와 같다.”

“하지만 부모 요소에 top 패딩이나 보더가 있거나 부모 요소가 시조(root) 요소이면 컨텐츠 영역은 가장 위에 있는 블록 레벨 자식의 상단 마진 경계(top margin-edge)부터 시작된다. 비슷한 방식으로 마지막 자식 요소의 margin-bottom과 부모 요소의 margin-bottom이 병합되지 않으면 가장 아래에 있는 블록 레벨 자식의 하단 마진 경계(bottom margin-edge)에서 컨텐츠 영역이 끝난다.”

† 원문에서는 컨텐츠의 유무에 대해서 언급하지 않고, 대신 “익명 블록 박스(anonymous block box)도 자식 요소가 될 수 있다.”라고 명시하고 있습니다. 인라인 컨텐츠가 들어가면 자동으로 익명 블록 박스가 생성되어 자식 요소 박스처럼 동작한다는 의미입니다.

다시 말해서, 부모와 자식 요소간에 마진 병합이 발생하면 자식 요소의 margin-top이나 margin-bottom이 부모 요소의 높이 계산에 전혀 영향을 미치지 않는다는 얘기입니다. 따라서 부모와 자식 요소의 마진 영역이 같은 선(부모나 자식 요소의 보더 경계)에서 시작되고 그 두 마진이 하나로 합쳐지기 때문에 margin-top으로 병합되면 위쪽으로, margin-bottom으로 병합되면 아래쪽으로 마진이 적용됩니다.

부모, 자식 요소간의 margin-bottom이 합쳐지는 경우는 조건이 조금 더 복잡해지는데 우선 부모 요소의 heightauto여야 하며 min-height는 요소의 실제 높이보다 작고 max-height는 요소의 실제 높이보다 커야 합니다. 여기에서 min-heightmax-height 조건에 따라서 첫 번째 조건인 height 산출 값(computed value)이 달라질 수 있기 때문에(CSS 2.1 10.7 참고) 첫 번째 조건이 핵심이라고 할 수 있습니다.

이런 조건이 필요한 이유는 height가 지정되면 자식 요소와 상관 없이 높이가 결정되고, 마진 영역은 투명하기 때문에 overflowvisible인 상태에서는 가장 아래에 있는 자식 요소의 margin-bottom이 부모 요소의 렌더링에 영향을 미치지 않고, 만약 margin-bottom이 적용되면 부모 요소 다음에 배치되는 박스와의 관계에 혼란을 줄 수 있기 때문이 아닐까 생각합니다.

패턴 3. 요소의 자체 병합

마지막 세 번째 패턴입니다. 어떤 요소의 margin-topmargin-bottom이 합쳐지려면 까다로운 조건을 만족시켜야 하지만 실제로는 아주 쉽게 발생하기도 합니다. 자체 병합이 이루어지려면 min-height 속성 값이 0이고, top, bottom 보더와 패딩이 없고, height 속성 값이 0 또는 auto이고, 어떤 라인 박스도 포함하지 않고, 해당 요소의 흐름 안에 있는 자식 요소들의 마진이(있다면) 모두 인접해야 합니다.

이런 조건은 문서에 수직 방향 보더와 패딩이 없는 빈 블록 레벨 요소가 들어가면 간단히 만족됩니다. min-heightheight를 지정하는 경우가 별로 없으니까요. 간단한 마크업 예는 다음과 같습니다.

<div style="margin-top: 10px; background-color: blue;">
	<div style="margin: 30px 0 10px 0; background-color: orange;"></div>
	<div style="margin: 20px 0; background-color: orange;">블록 A</div>
	<div style="margin: 20px 0; background-color: orange;">블록 B</div>
</div>

랜더링 결과는 다음과 같습니다.

요소의 자체 병합

블록 A의 margin-top 20px과 빈 div 요소의 margin-bottom 10px, margin-top 30px, 부모 요소의 margin-top 10px이 하나로 합쳐져서 30px의 단일 마진으로 표현됩니다. 패턴 1, 2, 3가 함께 적용된 결과이지요.

† 처음에는 패턴 2, 3만 적용된다고 생각했는데 블록 A와 빈 요소가 형제 관계라서 패턴 1도 성립하네요. 관련해서 일부 내용을 업데이트했습니다.

이렇게 세 개 이상의 마진이 합쳐지고, 마진의 부호가 서로 다를 때에는 마진이 합쳐지는 순서가 중요합니다. 아래와 같은 상황을 생각해보지요.

<div style="margin-top: 50px; background-color: blue;">
	<div style="margin: 30px 0 -40px 0; background-color: orange;"></div>
	<div style="margin: 20px 0; background-color: orange;">블록 A</div>
	<div style="margin: 20px 0; background-color: orange;">블록 B</div>
</div>

만약에 빈 div의 마진이 먼저 합쳐지고, 이렇게 만들어진 단일 마진이 부모 요소와 블록 A의 마진과 합쳐진다면 최종 마진은 30 – 40 = -10(px), 50 – 10 = 40(px)이 됩니다. 하지만 네 마진이 한꺼번에 합쳐지면 앞서 설명한 계산 방법대로 50 – 40 = 10(px)이 됩니다.

파이어폭스 3, 사파리 3에서 테스트해보니 명세서대로 한꺼번에 합쳐져서 10px로 표현되었습니다. 오페라에서는 다른 결과가 나왔는데 버그일 가능성이 높다고 생각됩니다.

† 2010.06.22: 오페라 10에서 확인해보니 파이어폭스, 사파리와 동일하게 10px로 표현되네요. 이전에 테스트를 잘못했거나 아니면 버그가 수정되었나 봅니다. ^^;

요소의 마진이 자체적으로 병합될 때 마진이 topbottom 중 어느 방향으로 적용되는지에 관한 명확한 설명은 찾지 못했습니다. 그래서 나름대로 테스트를 해봤는데 방향 자체가 무의미하지 않을까 하는 생각이 듭니다. 최종 마진을 계산할 때 자체 병합되는 요소의 마진 값 두 개를 추가시키고, 그 결과 값을 문서 구조에 따라서 패턴 1, 2에 적용시키는 것이 아닐까 싶네요.

예를 들어서 바로 위 예제에서 최종 마진 값은 10px입니다. 이제 빈 요소를 무시하고 패턴 2에 따라서 부모 요소와 블록 A 사이에 10px의 마진이 있다고 가정하면 최종적으로 부모 요소에 10px의 margin-top이 적용됩니다.

물론 이것이 정확한 방식인지 확신할 수는 없습니다. 테스트를 더 하면서 추론과 위배되는 결과가 발생하거나 정확한 방식을 설명하는 자료를 찾으면 글을 업데이트하도록 하겠습니다.

마치며

다양한 조건에서 마진 병합이 발생하지만 일반적인 문서 흐름에서는 거의 대부분 위에 설명한 세 가지 패턴의 조합이라고 생각합니다. 하지만 float된 요소의 유무나 overflow, position 등의 속성 값에 따라서 마진 병합이 적용되지 않는 경우도 있는데 이런 상황에 대해서는 다음 글에서 알아보겠습니다.

댓글 6개가 달렸습니다. 태그: , , ,

  1. 정찬명 | 2008-09-11 17:37

    설명 잘 봤습니다. 얼마전에 제가 CDK에 올렸던 질문을 잘 정리해서 포스팅 해주셨네요. 조만간 저도 wystan님 글 참고해서 관련글을 써볼까 합니다. 형제간 마진은 이해가 쉬운 편인데 부모자식간 수직 마진은 제가 봐도 이해하기 쉽진 않네요. ^^;

  2. wystan | 2008-09-12 15:57

    잘 정리해보려고 했는데 그게 쉽지가 않네요~ ^^; 전에도 마진 병합을 한 번 다뤄보려고 했는데 만만치 않은 주제라는 것을 알고 지금껏 미뤄두고 있었습니다.

    그래도 계속 보다 보니 어느 정도 이해는 되지만 그게 정확하다는 확신은 아직 없습니다. 그래서 다음 글을 쓰기 전에 여러 가지 소스를 만들어서 테스트를 해 볼 생각입니다. 역시 소스와 렌더링 결과를 직접 확인하는 것이 가장 이해하기 쉬울 것 같아서요.

    비록 IE 6, 7 때문에 실무에 적용하기는 어렵지만 그래도 이번 기회에 확실히 알고 넘어갔으면 하는 바램입니다. 관련 글을 올려주시면 저도 많이 참고하겠습니다.

  3. 강짱 | 2009-01-07 16:36

    지금 레이아웃 잡는데 수직 마진 때문에 고생하고 있습니다.
    이글을 보니 아주쬐끔~이해가 가긴하는데..
    아직도 너무 이해가 힘드네요..

  4. wystan | 2009-01-13 01:06

    수직 마진이 상당히 까다롭습니다.

    현 상황으로는 아예 마진 병합이 발생하지 않도록 하는 것이 최선인데…
    overflow:hidden을 적용할 수 없는 상황이 있어서 문제가 됩니다.

    최대한 피하고, 불가피하면 핵을 쓰는 방법 밖에 없는 것 같아요. ^^;

  5. lainfox | 2013-01-18 19:22

    @wystan
    마진병합 이라는 한글 단어를 오늘 처음 들었는데
    collapsing-margin 였군요 ;D

    원하는 결과를 얻기위해 마진병합을 제거하는 방법은 여러가지가 있을텐데 꽤 괜찮은 트릭을 써놓고 갑니다
    1) 부모에 overflow:auto 또는 hidden
    2) 부모에 border-top:1px solid transparent
    3) 부모에 padding-top:1px

    1번 외의 트릭은
    위에 언급하신 컨텐츠 영역, 패딩 영역, 보더 영역 부분중 패딩, 보더를 이용하는 트릭입니다 :p

  6. sammykim | 2013-07-29 11:56

    패턴 1의 예제 파란색 블록과 오렌지색 블록의 위치가 바뀐것 아닌가요..
    : ) 맞다면 정정 부탁드립니다.

댓글이 닫혔습니다.