마진 병합(Margin Collapse)이란?

인접한 블록 요소 사이에서 마진이 합산되지 않고 병합되는 현상의 발생 조건과 규칙을 이해하고, 예상치 못한 레이아웃 문제를 예방합니다

중급 15분 마진 병합 Margin Collapse 블록 요소 발생 조건

마진 병합(Margin Collapse)은 CSS에서 인접한 블록 요소들의 수직 마진이 서로 합산되지 않고 더 큰 값 하나로 합쳐지는 현상입니다. 이 현상은 CSS 명세에 의도적으로 정의된 동작이지만, 그 발생 조건이 복잡하고 직관에 반하는 경우가 많아 개발자들이 레이아웃 작업에서 자주 혼란을 겪습니다. 마진 병합을 이해하지 못하면 요소 사이 간격이 예상과 다르게 나타나거나, 부모 요소가 자식 요소의 마진을 흡수하는 이상한 현상을 마주하게 됩니다. 이 주제를 통해 마진 병합이 언제, 왜 발생하는지 원리부터 이해하면, 레이아웃 문제를 정확히 진단하고 의도한 대로 제어할 수 있게 됩니다.

🔍 핵심 문제점

  • 형제 요소 간 수직 마진이 합산되지 않고 큰 값 하나만 적용되어 간격이 예상보다 좁아짐
  • 부모 요소에 배경이나 패딩이 없으면 자식의 마진이 부모 바깥으로 빠져나가는 마진 전이 현상 발생
  • 내용이 없는 빈 블록 요소는 자신의 상하 마진이 서로 병합되어 공간을 차지하지 않을 수 있음
  • 개발자 도구에서도 병합 여부를 눈으로 확인하기 어려워 디버깅 시간이 길어짐
  • overflow, border, padding, flex, grid 등 특정 CSS 속성이 병합을 차단한다는 사실이 알려지지 않아 우연히 문제가 해결되거나 재발하는 패턴 반복

🧭 왜 중요한가?

마진 병합은 CSS 박스 모델에서 수직 리듬을 일관되게 유지하려는 설계 의도에서 비롯되었으나, 현대 레이아웃 개발 환경에서는 오히려 예측 불가능한 간격 문제의 주범이 되는 경우가 많습니다. 특히 컴포넌트 기반 개발에서는 개별 컴포넌트의 마진이 상위 컨테이너나 인접 컴포넌트와 예상치 못하게 병합되어 디자인 시스템의 간격 규칙이 깨지는 상황이 빈번하게 발생합니다. 이 현상의 발생 조건을 정확히 알고 있으면 레이아웃 버그를 수분 안에 해결할 수 있지만, 모르는 경우 동일한 문제를 반복해서 맞닥뜨리게 됩니다. Flexbox와 Grid 레이아웃이 마진 병합을 원천 차단한다는 사실을 알면, 현대 레이아웃 기법이 왜 예측 가능한 간격 제어를 제공하는지도 함께 이해할 수 있습니다. 마진 병합을 이해하는 것은 단순한 CSS 지식을 넘어, 레이아웃 시스템 전체를 체계적으로 사고하는 능력을 키우는 데 핵심이 됩니다.


핵심 개념

마진 병합의 기본 규칙

입문

두 블록 요소가 위아래로 붙어 있을 때, 마진이 더해지지 않고 더 큰 쪽 하나만 남는 현상이에요. 왜 그런지 함께 알아볼게요!

📏 마진이 뭔가요? 마진은 요소 바깥쪽의 빈 공간이에요. 마치 책상과 책상 사이의 간격처럼, 화면에서 요소들 사이의 여백을 만들어 주는 역할을 해요.

🤔 더하면 20이 되어야 하는데 왜 10이에요? 위 요소에 아래 마진 10px, 아래 요소에 위 마진 10px이 있다고 해봐요. 보통 생각하면 사이 간격이 20px이어야 할 것 같지만, CSS는 10px만 적용해요! 두 마진이 겹쳐서 더 큰 값 하나만 남는 거예요. 이걸 ‘마진 병합’이라고 불러요.

📐 어떤 값이 남나요? 두 마진 중 더 큰 값 하나만 남아요. 위 마진이 30px, 아래 마진이 10px이면 사이 간격은 30px이 돼요. 두 값이 같으면 그 값 하나만 적용돼요. 가장 큰 마진이 ‘대표’로 남는다고 생각하면 쉬워요!

📌 위아래만 해당돼요, 좌우는 안 해요 이 현상은 오직 수직 방향(위아래)에서만 일어나요. 왼쪽-오른쪽 마진은 절대 병합되지 않고 항상 더해져요. CSS 명세가 수직 방향에만 이 규칙을 적용하도록 설계되어 있어요.

🏗️ 블록 요소에서만 일어나요 마진 병합은 블록 요소끼리 만날 때만 발생해요. 블록 요소는 줄 전체를 차지하는 요소(제목, 문단, 목록 등)예요. 인라인 요소(글자, 링크 등)에서는 이 현상이 일어나지 않아요.

중급

마진 병합(Margin Collapse)은 두 개 이상의 블록 레벨 요소의 수직 마진이 서로 인접할 때, 각 마진이 합산되지 않고 그 중 더 큰 값 하나로 합쳐지는 CSS 명세 동작입니다.

병합 규칙 요약

  • 양수 마진끼리: 더 큰 값 하나만 적용
  • 양수 + 음수 마진: 두 값의 합 적용 (ex: 20px + -10px = 10px)
  • 음수 마진끼리: 더 작은(절대값이 큰) 값 적용

병합은 수직 방향(margin-top, margin-bottom)에서만 발생하며, 수평 방향(margin-left, margin-right)에는 적용되지 않습니다.

/* 두 요소 사이 간격은 40px이 아닌 30px */
.element-a {
  margin-bottom: 30px;
}
.element-b {
  margin-top: 20px;
}
/* 실제 간격: max(30px, 20px) = 30px */

음수 마진 병합 음수 마진이 포함되면 단순히 큰 값을 선택하는 것이 아니라 합산 방식이 달라집니다. 양수 마진과 음수 마진이 병합될 때는 두 값을 더한 결과가 적용됩니다.

.element-a {
  margin-bottom: 30px;
}
.element-b {
  margin-top: -10px;
}
/* 실제 간격: 30px + (-10px) = 20px */

심화

마진 병합은 W3C CSS 2.1 명세 Section 8.3.1 (Collapsing margins)에 명시적으로 정의된 동작으로, 블록 포매팅 컨텍스트(BFC, Block Formatting Context) 경계 내에서 인접한 블록 레벨 박스의 수직 마진이 단일 마진으로 병합되는 현상입니다.

W3C CSS 2.1 명세 기반 병합 규칙 CSS 2.1 Section 8.3.1에 따르면, 병합 조건은 다음과 같습니다.

  • 두 마진이 동일한 블록 포매팅 컨텍스트(BFC) 내에 위치해야 함
  • 두 마진 사이에 라인 박스(line box), 클리어런스(clearance), 패딩(padding), 보더(border)가 없어야 함
  • 인접한(adjoining) 마진이어야 함 — 즉 명세에서 정의한 “in the block direction” 조건 충족

병합 값 계산은 다음 알고리즘을 따릅니다.

  • 모두 양수: max(margin-a, margin-b)
  • 양수 + 음수 혼재: max(양수들) + min(음수들)
  • 모두 음수: min(margin-a, margin-b) (절대값 최대)

블록 포매팅 컨텍스트(BFC)와의 관계 마진 병합은 오직 동일 BFC 내에서만 발생합니다. BFC는 블록 레벨 박스들이 독립적으로 레이아웃되는 독립적 렌더링 영역으로, 새로운 BFC를 생성하는 요소(overflow: hidden, display: flow-root, float 요소 등)는 내부 마진이 외부로 전파되지 않습니다.

이는 CSS 레이아웃 엔진이 BFC를 “마진 병합 격리 단위”로 사용함을 의미합니다. Flexbox 포매팅 컨텍스트(FFC)와 그리드 포매팅 컨텍스트(GFC)는 BFC가 아니므로, flex/grid 컨테이너 내의 자식 요소들 사이에서는 마진 병합이 발생하지 않습니다.

렌더링 엔진 구현 관점 Blink 렌더링 엔진(Chrome/Edge)에서 마진 병합은 레이아웃 단계(Layout Phase)의 블록 레이아웃 알고리즘에서 처리됩니다. 구체적으로 BlockNode::Layout()BlockLayoutAlgorithm::HandleNewFormattingContext() 경로에서 인접 마진의 병합 여부를 판단하며, 병합된 마진은 단일 마진 스트럿(margin strut)으로 표현됩니다. 마진 스트럿은 양수/음수 마진의 최대/최소값을 별도로 추적하여 최종 사용 값(used value)을 계산합니다.

형제 요소 간 마진 병합

입문

위아래로 나란히 있는 두 요소 사이에서 마진이 병합되는 가장 흔한 상황이에요. 글쓰기 앱에서 문단 사이 간격을 생각해 보세요!

📄 문단 사이 간격 예시 워드 문서에서 첫 번째 문단과 두 번째 문단 사이에는 적당한 간격이 있죠. CSS에서도 각 문단(<p> 태그)은 기본적으로 위아래 마진을 가져요. 그런데 두 문단이 위아래로 만나면 마진이 병합돼서 간격이 예상보다 작아질 수 있어요.

🔗 언제 병합되나요? 두 블록 요소가 바로 위아래로 붙어 있고, 그 사이에 아무것도 없을 때 병합이 일어나요. 두 요소 사이에 다른 요소나 선, 패딩 같은 게 끼어 있으면 병합되지 않아요.

🧩 여러 요소가 겹치면요? 세 개, 네 개 요소가 연속으로 있어도 마진 병합이 일어나요. 모든 인접한 마진 중 가장 큰 값 하나만 최종적으로 적용돼요. 예를 들어 10px, 20px, 15px 마진이 연속으로 있으면 가장 큰 20px만 남아요.

💡 왜 이렇게 설계됐나요? 웹 초기에는 긴 글을 표시하는 용도로 HTML을 많이 사용했어요. 각 문단마다 아래 마진을 주면 문단 사이 간격이 두 배로 벌어지는 문제가 있었어요. 마진 병합은 이를 자연스럽게 해결하기 위해 설계된 기능이에요.

중급

형제 요소 간 마진 병합은 Normal Flow(일반 흐름) 안에서 인접한 블록 레벨 형제 요소의 margin-bottommargin-top이 병합되는 가장 일반적인 케이스입니다.

발생 조건

  1. 두 요소가 같은 블록 포매팅 컨텍스트(BFC) 내에 있을 것
  2. 두 요소 사이에 border, padding, line box, clearance가 없을 것
  3. 두 요소가 수직으로 인접(adjoining)할 것
p {
  margin-top: 16px;
  margin-bottom: 16px;
}
/* 연속된 <p> 태그 사이 간격은 32px이 아닌 16px */
/* max(16px, 16px) = 16px */
.wrapper {
  border: 1px solid transparent; /* border가 생기면 병합 차단 */
}

/* 또는 패딩으로 차단 */
.wrapper {
  padding: 1px 0;
}

float, absolute, flex, grid 컨텍스트에서는 병합 없음 요소가 float이거나 position: absolute/fixed이거나, 부모가 flex/grid 컨테이너이면 해당 요소의 마진은 병합되지 않습니다. 이는 해당 요소들이 일반 흐름(Normal Flow) 밖에 있거나 BFC가 아닌 다른 포매팅 컨텍스트 안에 위치하기 때문입니다.

심화

형제 요소 간 마진 병합은 CSS 2.1 Section 8.3.1의 “adjoining margins” 정의를 따릅니다. 두 마진 박스가 “adjoining”하려면 둘 사이에 라인 박스, 클리어런스, 패딩, 보더가 존재하지 않아야 하며(no non-zero height content separates them), 동일한 블록 포매팅 컨텍스트에 속해야 합니다.

Normal Flow와 마진 인접성(Adjoining) 마진이 “인접”하다는 것은 단순히 DOM 상 형제 관계가 아닌 레이아웃 상의 인접성입니다. 명세에서는 다음 경우를 adjoining으로 정의합니다.

  • 부모의 top margin과 첫 번째 자식의 top margin (분리 요소 없음)
  • 형제 요소의 bottom margin과 다음 형제의 top margin (분리 요소 없음)
  • 자신의 top margin과 bottom margin (높이 0, 콘텐츠 없음)
  • 마지막 자식의 bottom margin과 부모의 bottom margin (분리 요소 없음)

계단식(Cascading) 병합 여러 마진이 연쇄적으로 병합될 수 있습니다. A-B-C 세 요소가 연속으로 있을 때, A의 bottom과 B의 top이 병합되고, 그 결과와 B의 bottom, C의 top이 다시 병합되는 방식으로 처리됩니다. 최종적으로 관련된 모든 마진(양수/음수 포함) 중 양수 최댓값 + 음수 최솟값의 합이 적용됩니다.

Blink/Gecko 엔진의 마진 스트럿(Margin Strut) Blink 엔진은 마진 병합을 처리하기 위해 MarginStrut 구조체를 사용합니다. 이 구조체는 positive_margin(최대 양수 마진)과 negative_margin(최소 음수 마진) 두 값을 별도로 추적합니다. 레이아웃 알고리즘이 형제 요소를 순차적으로 처리할 때, 각 요소의 마진을 MarginStrut에 누적하고 분리 조건(border/padding/line-box 등)이 발생하면 누적된 MarginStrut를 확정합니다. 이 설계 덕분에 O(n)의 선형 시간에 연쇄 병합을 처리할 수 있습니다.

부모-자식 마진 전이

입문

부모 요소 안에 있는 자식 요소의 마진이 부모 바깥으로 빠져나오는 신기한 현상이 있어요. 마치 자식의 마진이 부모를 뚫고 나오는 것처럼요!

🏠 집 안의 방 비유 집(부모) 안에 방(자식)이 있다고 생각해 보세요. 방 위에 공간(마진)을 만들면, 보통은 집 안에서만 공간이 생겨야 해요. 그런데 CSS에서는 집과 방 사이에 아무 구분(경계)이 없으면, 방의 위 공간이 집 위로 튀어나와 버려요!

🤯 왜 이런 일이 생기나요? 부모 요소에 border(테두리)나 padding(안쪽 여백)이 없고, 자식이 부모의 첫 번째 요소라면 자식의 위 마진이 부모의 위 마진과 병합되어 부모 바깥쪽에 적용돼요. 결과적으로 부모가 내려가 버리고 부모 안에 공간이 생기는 게 아니라 부모 자체가 아래로 밀려요.

🔍 어떻게 확인할 수 있나요? 부모 요소에 배경색을 주면 쉽게 확인할 수 있어요. 자식의 마진이 부모 안에 있는 게 아니라 부모 위에 빈 공간으로 나타나면 마진 전이가 일어난 거예요. 부모가 내려가서 위에 빈 공간이 생긴 것처럼 보여요.

🛡️ 어떻게 막을 수 있나요? 부모에 border나 padding을 1px이라도 주면 돼요. 또는 부모에 overflow: hidden을 추가해도 막을 수 있어요. 이렇게 하면 부모와 자식 마진 사이에 ‘벽’이 생겨서 마진이 새어 나가지 않아요.

중급

부모-자식 마진 전이(Margin Transmission 또는 Margin Escape)는 부모 요소와 첫 번째/마지막 자식 요소 사이에 border, padding, 라인 박스가 없을 때, 자식의 top/bottom 마진이 부모의 마진과 병합되어 부모 바깥으로 전파되는 현상입니다.

발생 조건

  • 부모의 margin-top + 첫 번째 자식의 margin-top: 부모에 border-top, padding-top, 라인 박스(텍스트 등)가 없을 때
  • 부모의 margin-bottom + 마지막 자식의 margin-bottom: 부모에 border-bottom, padding-bottom이 없고 height가 auto일 때
/* 부모의 배경색을 줘도 자식 마진이 부모 밖으로 빠져나옴 */
.parent {
  background: lightblue; /* 배경색은 병합을 막지 않음 */
  /* border나 padding이 없으므로 마진 전이 발생 */
}

.child {
  margin-top: 30px; /* 이 마진이 .parent 위로 전이됨 */
}
/* 방법 1: padding 추가 */
.parent {
  padding-top: 1px; /* 경계를 만들어 병합 차단 */
}

/* 방법 2: border 추가 */
.parent {
  border-top: 1px solid transparent;
}

/* 방법 3: BFC 생성 */
.parent {
  overflow: hidden; /* 또는 display: flow-root */
}

display: flow-root의 의미 overflow: hidden은 내용을 잘라내는 부작용이 있습니다. 마진 전이만 막으려면 명시적으로 BFC를 생성하는 display: flow-root를 사용하는 것이 더 의미가 명확합니다. 이 속성은 해당 요소를 BFC 루트로 만들어 내부 마진이 외부로 전파되는 것을 차단합니다.

심화

부모-자식 마진 전이는 CSS 2.1 Section 8.3.1에서 adjoining 마진의 한 케이스로 정의됩니다. 부모의 top margin과 첫 번째 자식의 top margin이 adjoining하려면, 부모에 border-top, padding-top이 없고, 부모와 첫 번째 자식 사이에 라인 박스가 없어야 합니다. 이때 두 마진은 병합되어 부모 요소의 외부 마진으로 적용됩니다.

BFC(Block Formatting Context)가 차단하는 원리 BFC를 생성하는 요소는 CSS 2.1 Section 9.4.1에 따라 “establishes a new block formatting context for its contents”로 정의됩니다. BFC의 핵심 속성 중 하나는 “floats, absolutely positioned elements, block containers that are not block boxes, and block boxes with ‘overflow’ other than ‘visible’ establish new block formatting contexts for their contents”이며, BFC 내의 마진은 외부로 전파(escape)되지 않습니다.

따라서 다음 속성 중 하나라도 있으면 부모가 새로운 BFC를 생성하고 마진 전이를 차단합니다.

  • overflow: visible이 아닌 모든 값 (hidden, auto, scroll)
  • display: flow-root, inline-block, table-cell, flex, grid, …
  • float: none이 아닌 값
  • position: absolute, fixed

display: flow-root의 명세 정의 CSS Display Level 3 명세에서 도입된 display: flow-root는 새로운 BFC 루트를 생성하는 가장 명시적인 방법입니다. 기존 overflow: hidden이나 display: inline-block은 BFC 생성을 의도한 속성이 아닌 부작용으로 BFC를 생성하는 반면, flow-root는 오직 BFC 생성만을 목적으로 설계되었습니다. 이는 의도를 코드에 명시적으로 표현(explicit intent)하는 관점에서 더 나은 선택입니다.

스택 오버플로우 패턴과 실무 함의 컴포넌트 기반 아키텍처에서 부모-자식 마진 전이는 특히 위험합니다. 래퍼(wrapper) 컴포넌트가 내부 콘텐츠의 마진을 외부로 전파하면, 컴포넌트의 외부 인터페이스(마진 동작)가 내부 구현에 종속되어 캡슐화(encapsulation)가 깨집니다. React, Vue 등에서 컴포넌트 래퍼는 기본적으로 display: flow-root 또는 overflow: hidden을 적용하여 마진 캡슐화를 보장하는 것이 방어적 설계(defensive design) 관점에서 권장됩니다.

마진 병합 차단 방법

입문

마진 병합을 막고 싶을 때 사용할 수 있는 방법들이 있어요. 마치 두 자석이 붙지 못하도록 사이에 무언가를 끼워 넣는 것처럼요!

🧱 사이에 벽 세우기 — border와 padding 두 마진 사이에 border(테두리)나 padding(안쪽 여백)을 넣으면 병합이 일어나지 않아요. 마치 두 자석 사이에 나무판을 끼워 놓으면 자석이 서로 달라붙지 않는 것처럼, border와 padding이 마진이 만나는 것을 막아줘요.

🏰 독립된 공간 만들기 — overflow: hidden 요소에 overflow: hidden이라는 CSS를 추가하면 그 요소는 독립된 공간(블록 포매팅 컨텍스트)이 돼요. 독립된 공간에서는 내부 마진이 밖으로 새어 나가지 않아요. 마치 독립된 방의 소리가 방 밖으로 나가지 않는 것처럼요.

📦 현대적 해결책 — Flexbox와 Grid 부모 요소에 display: flex 또는 display: grid를 주면 그 안의 자식들 사이에서 마진 병합이 아예 일어나지 않아요. 현대적인 레이아웃 방법을 쓰면 마진 병합 걱정을 많이 줄일 수 있어요.

💡 어떤 방법을 쓰면 좋나요? 형제 요소 사이 간격을 원하는 대로 설정하려면 gap 속성 (Flexbox/Grid 전용)이 가장 예측 가능하고 직관적이에요. 마진 대신 gap을 쓰면 병합 걱정 없이 요소 사이 간격을 정확하게 조절할 수 있어요.

중급

마진 병합이 발생하는 근본 원인은 관련 요소들이 동일한 블록 포매팅 컨텍스트(BFC) 내에서 인접 마진을 갖는 것입니다. 따라서 차단 방법은 크게 두 가지입니다.

  1. 마진 사이에 분리 요소(border/padding/라인 박스) 삽입
  2. 새로운 BFC 생성
/* 방법 1: 투명 border */
.parent {
  border-top: 1px solid transparent;
}

/* 방법 2: 1px padding */
.parent {
  padding-top: 1px;
}

/* 방법 3: 인라인 콘텐츠 삽입 (비권장) */
.parent::before {
  content: '';
  display: table; /* BFC 생성 */
}
/* 방법 1: display: flow-root (권장) */
.parent {
  display: flow-root; /* 명시적 BFC 생성, 부작용 없음 */
}

/* 방법 2: overflow */
.parent {
  overflow: hidden; /* BFC 생성, 내용 클리핑 부작용 있음 */
}

/* 방법 3: flex/grid (자식 간 병합 차단) */
.parent {
  display: flex;    /* 또는 grid */
  flex-direction: column;
  gap: 16px; /* 마진 대신 gap 사용 */
}

gap vs margin 선택 가이드 Flexbox/Grid에서 자식 요소 간 간격은 gap을 사용하는 것이 권장됩니다. gap은 마진 병합이 없고, 컨테이너 끝 요소에도 불필요한 여백이 생기지 않아요. 반면 개별 요소의 외부 간격은 여전히 margin으로 관리합니다.

심화

마진 병합 차단은 CSS 레이아웃 컨텍스트 모델과 직접 연관됩니다. 차단 전략은 크게 세 가지로 분류됩니다.

전략 1 — 마진 인접성(Adjoining) 조건 해제 CSS 2.1 Section 8.3.1의 adjoining 조건 중 하나를 위반하면 병합이 차단됩니다. 구체적으로 두 마진 사이에 border, padding, 라인 박스, clearance 중 하나를 삽입하는 방식입니다. 이는 BFC를 변경하지 않고 마진 인접성만 파괴하는 최소 개입(minimal intervention) 방식입니다.

전략 2 — BFC 분리 새로운 BFC를 생성하여 내부 마진이 외부와 병합되지 않도록 격리합니다. BFC 생성 메커니즘은 CSS 2.1 Section 9.4.1에 정의되어 있으며, CSS Display Level 3에서 display: flow-root로 명시적 API가 추가되었습니다. display: flow-root는 기존 방법들(overflow, inline-block, float 등)과 달리 BFC 생성 이외의 시각적/레이아웃 부작용이 없습니다.

전략 3 — 포매팅 컨텍스트 전환 display: flex 또는 display: grid는 BFC가 아닌 FFC(Flex Formatting Context) 또는 GFC(Grid Formatting Context)를 생성합니다. CSS Flexbox Level 1 Section 8 및 CSS Grid Level 1 Section 9에 따르면, flex/grid 컨테이너의 자식 요소들 사이에서는 마진 병합이 발생하지 않습니다. 이는 FFC/GFC의 레이아웃 알고리즘이 마진을 독립적으로 처리하기 때문입니다.

gap 속성과 마진의 명세적 차이 gap(CSS Box Alignment Level 3)은 마진과 근본적으로 다른 레이아웃 프리미티브(layout primitive)입니다. 마진은 박스 모델의 일부로 요소에 속하지만, gap은 컨테이너 레이아웃 알고리즘이 트랙(track) 사이에 삽입하는 거터(gutter)로 정의됩니다. gap은 첫/마지막 자식에 적용되지 않고, 마진 병합 대상이 아니며, 서브픽셀 렌더링에서도 더 일관된 결과를 제공합니다. 이러한 이유로 컴포넌트 내부의 간격 관리에는 gap이, 컴포넌트 외부와의 간격에는 margin이 적합합니다.