본문으로 건너뛰기

마진 상쇄 규칙

무료2017-11-12#CSS#CSS margin collapse#collapsed margin#合并外边距#外边距折叠#CSS clearance

CSS 박스 모델에서 매우 흥미롭지만 이해하기 어려운 부분 중 하나입니다.

들어가며

margin의 상쇄(collapse) 규칙은 CSS 박스 모델에서 가장 복잡한 부분 중 하나입니다. 이 부분은 clearance(간격), normal flow/in-flow(일반 흐름), BFC(블록 서식 맥락), line box(행 박스), inline box(인라인 박스), bidi(양방향 환경) 등 이해하기 쉽지 않은 여러 개념을 포함하고 있기 때문입니다.

CSS 박스 모델은 단순히 7개의 가로 속성 + 7개의 세로 속성만이 아닙니다.

margin
  border
    padding
      width/height

P.S. 하이힐 유머가 생각나네요. "패딩만 있는 게 아니라 오늘은 마진도 더했어요."

관련된 내용은 최소한 다음을 포함합니다.

  • context-boxborder-box

  • padding/margin 백분율 계산 방식

  • backgroundpadding/margin/border의 관계

  • margin 음수 값

  • margin 상쇄

박스 모델은 시각적 서식 모델의 기초 단위이며 CSS 레이아웃 모델에서 없어서는 안 될 부분입니다.

CSS 박스 모델은 문서 트리의 요소를 위해 생성되고 시각적 서식 모델에 따라 배치되는 직사각형 박스를 설명합니다.

(출처: 8 盒模型)

따라서 박스 모델은 CSS가 문서 트리 위에 세운 첫 번째 추상화 계층이며, CSS 레이아웃 제어가 문서 요소와 직접 연결되는 부분입니다. 그리고 마진 상쇄는 수직 서식에 직접적인 영향을 미치는 요인 중 하나이므로 깊이 있게 이해할 필요가 있습니다.

1. 전형적인 시나리오

다음 예시에서 UA(사용자 에이전트)에 기본 스타일시트가 없으며, 선언되지 않은 스타일 속성은 명세를 따라 초기값을 갖는다고 가정합니다.

또한, 모든 UA는 CSS 명세를 준수한다고 가정합니다.

1. 목록 항목 간의 마진 상쇄

li {
    margin: 8px;
}

그렇다면 목록 항목(list item) 사이의 간격은 얼마일까요?

.li-case1 li {
    margin: 8px;
    /* 상단 패딩 추가 */
    padding-top: 1px;
}

.li-case2 li {
    margin: 8px;
    /* 하단 테두리 추가 */
    border-bottom: 1px solid;
}

case1과 case2에서 목록 항목 사이의 간격은 각각 얼마일까요?

2. 깊게 중첩된 마진 상쇄

/* 들여쓰기는 문서 구조의 중첩 관계를 나타냅니다 */
div.outer,
  div.container,
    div.content,
      div.inner {
    margin: 10px;
    min-width: 100px;
    min-height: 100px;
}

이 4개의 중첩된 div의 렌더링 결과는 어떤 모습일까요?

div.outer,
  div.container,
    div.content,
      div.inner {
    margin: 10px;
    min-width: 100px;
    min-height: 100px;
    /* 테두리 추가 */
    border: 1px solid;
}

지금은요?

div.outer,
  div.container,
    div.content,
      div.inner {
    margin: 10px;
    /* min-width, min-height, border 삭제 */
}

그럼 지금은 어떤 모습일까요?

3. 간격(clearance)이 있는 마진 상쇄

div.container {
    border-top: 1px solid;
    background: #ccc;
    margin-bottom: 60px;
}
  /* 들여쓰기는 문서 구조의 중첩 관계를 나타냅니다 */
  div.float {
      float: left;
      width: 100px;
      height: 50px;
  }
  div.following-float {
      clear: left;
      margin-top: 50px;
  }
div.following-container {
    color: red;
}

빨간색 텍스트 상단에서 .following-float 하단까지의 거리는 얼마일까요?

div.container {
    border-top: 1px solid;
    background: #ccc;
    margin-bottom: 60px;
}
  /* 들여쓰기는 문서 구조의 중첩 관계를 나타냅니다 */
  div.float {
      float: left;
      width: 100px;
      height: 50px;
  }
  div.following-float {
      clear: left;
      /* 50을 49로 변경 */
      margin-top: 49px;
  }
div.following-container {
    color: red;
}

지금은요?

다시 50051로 바꾸면 각각 어떤 상황이 벌어질까요?

P.S. 이 질문들에 대한 답은 아직 알 수 없습니다. 데모를 아직 작성하지 않았거든요 ;-) 그러니 우리가 진지하게 추측해 볼 시간은 충분합니다.

2. 상쇄 조건

어떤 마진들이 상쇄될까요?

가로 마진은 상쇄되지 않습니다. 인접한 수직 마진은 상쇄됩니다. 단, 다음 2가지 예외 상황이 있습니다.

  • 루트 요소 박스의 마진은 상쇄되지 않습니다.

  • 간격(clearance)을 가진 요소의 상단 마진과 하단 마진이 인접할 경우, 해당 마진은 바로 인접한 형제 요소의 인접 마진과 상쇄되지만, 상쇄된 결과는 다시 부모 블록의 하단 마진과 상쇄되지 않습니다.

1번은 넘어갑니다. 루트 요소에 마진을 적용하는 것은 일반적이지 않습니다.

2번에서는 "간격(clearance)"이라는 새로운 개념이 등장합니다. 이는 clear 속성과 관련이 있어 보이며, 직관적으로 보았을 때 clear 속성으로 인해 요소의 위치가 이동하면서 생기는 틈을 의미합니다 (CSS 명세 9 视觉格式化模型 참조). 여기에는 두 가지 핵심 포인트가 있습니다.

  • clear 속성을 가지고 있음

  • 그리고 (clear 속성으로 인해) 요소의 위치가 실제로 이동함

이 두 조건을 만족하면 해당 요소가 간격을 가졌다고 말합니다.

주의: 만약 clear 속성을 적용했더라도 요소의 실제 위치가 변하지 않는다면(예를 들어 margin-top을 통해 이미 그 위치에 배치된 경우), 즉 clear 속성이 추가적인 공간 점유를 발생시키지 않는다면 간격을 가진 것이 아닙니다. 반대로 clear 속성 적용으로 인해 요소의 실제 위치가 변했다면, 즉 요소 상단의 일부 공간이 clear 속성 때문에 생긴 것이라면 간격을 가진 것으로 봅니다.

간격을 가진 것만으로는 부족하며, 해당 요소의 상단 마진과 하단 마진이 인접해야 합니다(즉 요소의 실제 높이가 0이고 padding, border가 없어야 함). 이 조건들을 동시에 만족하면 해당 요소의 마진 상쇄에 제한이 생깁니다. 즉 해당 요소의 마진은 바로 인접한 형제 요소의 마진과만 상쇄되며, 그 결과물은 부모 블록의 하단 마진과 다시 상쇄되지 않습니다.

P.S. 이제 전형적인 시나리오 3에 도전할 자격이 생겼지만, 아직 갈 길이 멉니다.

"인접"의 정의

두 마진이 어떤 상황일 때 "인접(adjacent)"하다고 할까요?

  • 둘 다 in-flow(일반 흐름 내) 블록 레벨 박스에 속하며 동일한 블록 서식 맥락(BFC)에 있음

  • 행 박스(line box), 간격(clearance), 패딩 또는 테두리에 의해 격리되지 않음

  • 모두 수직으로 인접한 박스 경계(vertically-adjacent box edges)에 속함

3개의 문장에 4개의 새로운 개념이 있네요. 깊이 우선 탐색(DFS) 방식으로 훑어봅시다.

흐름 내(in-flow)

in-flow/out-of-flow는 해당 요소를 배치할 때 일반 흐름(normal flow) 배치 방식을 사용하는지 여부를 나타냅니다.

배치 방식은 크게 3가지로 나뉩니다.

  • 일반 흐름(Normal flow): 블록 서식, 인라인 서식 및 상대 위치 지정을 포함합니다.

  • 플로트(Floats): 일반 흐름의 위치에서 꺼내어 왼쪽이나 오른쪽으로 이동시킵니다.

  • 절대 위치 지정(Absolute positioning): 일반 흐름에서 완전히 벗어나 포함 블록(containing block)을 기준으로 자신의 위치를 결정합니다.

요소가 플로트되지 않았고(float 값이 none), 절대 위치 지정도 아니며(position 값이 absolutefixed가 아님), 루트 요소가 아니라면 일반 흐름에 따라 배치되는 in-flow 요소에 해당합니다. 그렇지 않으면 out-of-flow 요소입니다.

블록 서식 맥락(BFC)

플로트, 절대 위치 지정 요소, 블록 박스가 아닌 블록 컨테이너(예: inline-block, table-cell, table-caption), 그리고 'overflow' 값이 'visible'이 아닌 블록 박스(해당 값이 뷰포트로 전파된 경우는 제외)는 그 내용을 위해 새로운 블록 서식 맥락을 생성합니다.

블록 서식 맥락에서 박스들은 포함 블록의 상단부터 시작하여 수직 방향으로 차례대로 배치됩니다. 두 형제 박스 사이의 수직 거리는 'margin' 속성에 의해 결정됩니다.

즉, 아무도 새로운 BFC를 생성하지 않는다면 현재의 BFC에 속하게 됩니다. JS 스코프처럼 기본적으로 모두가 최외각 스코프(최외각 블록 서식 맥락)에 위치하며, 일반 블록 레벨 박스를 만나면 BFC에 넣고, 특수한 것(플로트, 절대 위치 등)을 만나면 새로운 스코프(새로운 BFC)를 생성하여 그 안의 요소들을 해당 내각 스코프에 넣는 방식입니다.

배치가 완료된 후 서식 맥락의 관점에서 보면, 이는 일련의 중첩된 BFC들이며 각 BFC는 일련의 블록 박스(또는 블록 레벨 요소)의 배치를 관리하게 됩니다.

참고: 여기서는 인라인 서식 맥락(IFC)을 언급하지 않는데, 서로 다른 IFC를 구분하는 것이 큰 의미가 없기 때문입니다(명세 정의상 IFC를 넘나드는 특수한 시나리오가 없습니다). 그렇다면 언제 새로운 IFC가 생성될까요? 명세에 따르면, 블록 컨테이너가 인라인 레벨 박스만 포함할 때 새로운 IFC를 생성하며, BFC처럼 명시적으로 강제 생성할 수는 없습니다.

P.S. 언제 새로운 IFC가 생성되는지에 대한 더 많은 논의는 When does a box establish an inline formatting context?를 참조하세요.

행 박스(line box)

동일한 줄에 있는 박스들을 포함하는 직사각형 영역을 행 박스라고 합니다.

행 박스는 항상 그 안에 포함된 모든 박스를 수용할 수 있을 만큼 충분히 높습니다.

행 박스는 '줄(line)'에 대한 CSS의 추상적인 표현입니다. 각 줄의 요소들은 동일한 행 박스 안에 위치합니다. 내용이 너무 길어 자동 줄바꿈이 발생하면 다음 줄을 위해 또 다른 행 박스가 생성됩니다. 또한 행 박스는 단순한 추상적 정의가 아니라 너비와 높이를 가지며 줄 배치를 결정하는 데 사용됩니다.

인접 마진 사이에 "행 박스가 없다"는 것은 간단히 말해 그 사이에 인라인 요소가 끼어들지 않았음을 의미합니다.

수직으로 인접한 박스 경계

다음 4가지 시나리오는 마진이 모두 수직으로 인접한 박스 경계에 속하는 경우를 만족합니다.

  • 박스의 상단 마진과 그 박스의 첫 번째 in-flow 자식의 상단 마진

  • 박스의 하단 마진과 그 박스의 바로 다음 in-flow 형제 요소의 상단 마진

  • 마지막 in-flow 자식의 하단 마진과 'height' 값이 'auto'인 부모 요소의 하단 마진

  • 박스의 상단 마진과 하단 마진. 단, 해당 박스가 새로운 BFC를 생성하지 않아야 하며, 'min-height' 값이 0이고 'height' 값이 0 또는 'auto'이며 in-flow 자식이 없어야 함

내용이 너무 기니 조건을 단순화하여 모두 in-flow 요소라고 가정하면 다음과 같습니다.

  • 부모와 자식: 부모의 상단 마진과 첫째 자식의 상단 마진

  • 형제: 요소의 하단 마진과 오른쪽 형제의 상단 마진

  • 부모와 자식: 막내 자식의 하단 마진과 부모의 하단 마진

  • 자기 자신: 높이가 0인 "진공" 요소의 상단 마진과 하단 마진

P.S. 여기서 "진공"이란 감자칩 봉지를 진공 상태로 만드는 것과 같습니다. 안에 아무것도 없거나 in-flow 자식들이 모두 빠져나간 상태를 말합니다.

즉, "인접 마진"의 위치 정의는 구체적으로 부모-자식, 형제, 그리고 자기 자신(자신의 상하 마진 상쇄는 꽤 특이한 경우입니다)의 3가지 상황으로 나뉩니다.

"인접"과 마진 상쇄 다시 이해하기

앞서 설명한 개념들을 바탕으로 이제 흩어진 정보들을 하나로 모아 "인접"을 다시 정의해 봅시다.

부모-자식, 형제 혹은 요소 자신의 마진이 서로 맞닿아 있는 것이 바로 "인접"입니다.

여기서 핵심은 맞닿아 있다는 점입니다. 즉, 두 마진 사이에 "벽"이 없어야 합니다. 이 "벽"은 3가지 종류가 있습니다.

  • 종족: 양쪽 모두 반드시 in-flow 블록 레벨 박스여야 함

  • 신앙: 동일한 블록 서식 맥락(BFC)에 있어야 함

  • 지역: 둘 사이에 행 박스(line box), 간격(clearance), 패딩 및 테두리가 없어야 함

이제 "인접"의 의미가 명확해졌습니다. 다시 마진 상쇄의 정의를 정리해 보겠습니다.

루트 요소가 아닌 요소의 인접한 수직 마진은 상쇄되며, 간격(clearance)이 있다면 상쇄가 제한됩니다.

제한됨이란 간격을 가진 요소 자신의 상하 마진이 인접할 경우 형제 요소의 마진하고만 상쇄될 뿐, 부모 요소의 하단 마진과는 상쇄될 수 없음을 의미합니다.

3. 상쇄 조건의 추론

마진 상쇄 발생 조건에 따라 8가지 추론을 도출할 수 있습니다.

  • 플로트 박스와 다른 어떤 박스 사이의 마진도 상쇄되지 않습니다 (플로트 박스와 그 안의 in-flow 자식 사이도 마찬가지).

  • 새로운 BFC를 생성한 요소(예: 플로트 박스 및 'overflow'가 'visible'이 아닌 요소)의 마진은 그들의 in-flow 자식과 상쇄되지 않습니다.

  • 절대 위치 지정 박스의 마진은 상쇄되지 않습니다 (자식들과도 마찬가지).

  • 인라인 블록 박스의 마진은 상쇄되지 않습니다 (자식들과도 마찬가지).

  • in-flow 블록 레벨 요소의 하단 마진은 항상 다음 in-flow 블록 레벨 형제의 상단 마진과 상쇄됩니다. 단, 형제 요소가 간격(clearance)을 가진 경우는 제외합니다.

  • in-flow 블록 레벨 요소의 상단 마진은 첫 번째 in-flow 블록 레벨 자식의 상단 마진과 상쇄됩니다. 단, 해당 요소에 상단 테두리와 상단 패딩이 없어야 하며 자식이 간격을 가지지 않아야 합니다.

  • 'height'가 'auto'이고 'min-height'가 0인 in-flow 블록 레벨 박스의 하단 마진은 마지막 in-flow 블록 레벨 자식의 하단 마진과 상쇄됩니다. 단, 해당 박스에 하단 패딩과 하단 테두리가 없어야 하며 자식의 하단 마진이 간격을 가진 상단 마진과 상쇄되지 않아야 합니다.

  • 박스 자신의 마진도 상쇄됩니다. 단, 'min-height'가 0이고 상하 테두리 및 상하 패딩이 없으며, 'height'가 0 또는 'auto'이고 행 박스를 포함하지 않는다면, 모든 in-flow 자식의 마진(존재한다면)이 상쇄됩니다.

간략하게 요약하자면 다음 4가지 포인트입니다.

  • in-flow가 아닌 경우(절대 위치 또는 플로트) 상쇄되지 않음

  • 새로운 BFC 생성을 유발하는 경우(플로트, 절대 위치 요소, 블록 박스가 아닌 블록 컨테이너 및 'overflow'가 'visible'이 아닌 특정 블록 박스) 자식과 상쇄되지 않음

  • 블록 레벨 박스가 아닌 경우(인라인 블록) 상쇄되지 않음

  • 일반적인 상황에서 형제 요소의 하단-상단 마진, 부모-자식 요소의 상단 마진 및 하단 마진, 요소 자신의 상단-하단 마진은 상쇄됨

앞의 3가지 포인트는 "인접"의 전제 조건(in-flow, 동일 BFC, 블록 레벨 박스)에 대한 설명이고, 4번째 포인트는 4가지 "인접" 시나리오를 옮겨 놓은 것이며 이를 자세히 풀면 8가지 추론이 됩니다.

4. 상쇄 동작

두 인접 마진이 상쇄된 후 형성된 마진을 collapsed margin(상쇄된 마진)이라고 부릅니다.

P.S. 결과로서의 상태와 동작을 구분하기 위해 상쇄된(collapsed) 결과임을 강조합니다.

마진 상쇄에는 2가지 특징이 있습니다.

  • 재귀(Recursive): 즉, 깊은 상쇄입니다. 한 번 상쇄된 후, 그 결과물과 인접한 또 다른 마진이 상쇄 가능한지 확인하여 가능하다면 계속해서 상쇄해 나갑니다.

  • 탐욕(Greedy): 가장 넓은 상쇄 결과를 추구합니다. 두 마진이 양수라면 더 큰 값을 취하고, 둘 다 음수라면 절대값이 더 큰 값을 취합니다.

재귀 특성과 관련하여 "인접"의 정의는 다음과 같은 재귀 공식으로 확장됩니다.

상쇄된 마진의 어느 한 부분이라도 다른 마진과 인접한다면 그 둘 또한 인접한 것으로 간주합니다.

탐욕은 마진 상쇄 결과의 계산 방식과 관련이 있습니다. 마진에는 음수가 허용되므로 상황이 조금 복잡해집니다.

  • 둘 다 양수인 경우: 두 값 중 최댓값을 취함

  • 하나가 양수이고 하나가 음수인 경우: 두 값을 더함

  • 둘 다 음수인 경우: 두 값의 절대값 중 최댓값을 취함

예를 들어:

ul {margin-bottom: -15px;}
  /* 들여쓰기는 문서 구조의 중첩 관계를 나타냅니다 */
  li {margin-bottom: 20px;}
h1 {margin-top: -18px;}

그렇다면 h1과 마지막 li 사이의 수직 거리는 20 + -max(|-15|, |-18|) = 2px가 됩니다.

양수든 음수든 최댓값을 취하는 원칙은 상쇄 결과를 최대한 넓게(절대값이 더 큰 음수는 요소의 내용을 더 멀리 밀어낼 수 있음) 만들려는 것, 즉 탐욕성에 기반합니다.

5. 온라인 데모

데모 주소: http://ayqy.net/temp/margin-collapse.html

P.S. 모든 정답은 데모 안에 있고, 모든 설명은 소스 코드 안에 있습니다.

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성