05 — Position Context
CSS에서 “내 부모는 누구인가” 라는 질문에는 4가지 다른 답이 있다 —
containing block,BFC,IFC,stacking context. 같은 요소가 4개의 다른 부모를 동시에 가질 수 있고, 어떤 부모를 묻느냐에 따라 위치·z-index·overflow가 결정된다.
Why — 왜 컨텍스트가 박스보다 중요한가
박스의 크기와 display 값이 같아도 — 어떤 컨텍스트 안에 있는지에 따라 결과가 완전히 다르다.
- 같은
position: absolute요소가 부모 A 안에서는 왼쪽 위에, 부모 B 안에서는 화면 왼쪽 위에 붙는다. - 같은
z-index: 999가 어떤 부모 아래에서는 위로 올라오고, 어떤 부모 아래에서는 묻힌다. - 같은
margin-top: 20px이 어떤 부모에서는 부모를 뚫고 합쳐지고, 어떤 부모에서는 안에서 작동한다.
이런 “같은 코드, 다른 결과” 의 원인은 거의 항상 컨텍스트다. Position context를 이해하면 CSS의 70%가 예측 가능해진다.
How — 4가지 컨텍스트의 정의
1) Containing Block — 위치 계산의 좌표 원점
position이 무엇이냐에 따라 기준이 되는 박스가 다르다.
position | Containing Block |
|---|---|
static (기본) | 가장 가까운 block-level 조상의 content box |
relative | 자신의 normal flow 위치 (top/left는 이동량) |
absolute | 가장 가까운 position이 static이 아닌 조상 |
fixed | viewport (또는 transform이 걸린 조상) |
sticky | 가장 가까운 scrolling 조상 |
.parent { position: relative; }
.child { position: absolute; top: 0; left: 0; }
/* .child는 .parent의 좌상단에 붙음 (viewport가 아니라) */함정: transform/filter/perspective/will-change가 걸린 조상은 fixed 자식의 containing block을 가로챈다. position: fixed 모달이 갑자기 viewport가 아니라 부모에 갇히는 현상의 원인.
2) Block Formatting Context (BFC) — 블록 흐름의 격리 공간
BFC는 블록 자식들의 배치가 외부에 영향을 주지 않는 격리된 영역이다. BFC가 시작되면:
- 내부 자식의
float가 외부로 새지 않음 (자동 clearfix). - 내부 자식의 margin이 외부와 상쇄되지 않음.
- BFC 자체가 float를 회피.
BFC를 만드는 조건
| 트리거 | 비고 |
|---|---|
<html> (root) | 자동 |
display: flow-root | 모던 권장 |
display: flex / grid | 자식은 flex/grid item으로 |
display: inline-block | inline 외부 흐름 + BFC |
overflow: auto/hidden/scroll/clip | 부작용 있음 |
position: absolute / fixed | 자동 |
column-count 또는 column-width | 멀티컬럼 |
contain: layout / paint / strict | 모던 |
예시 — 마진 상쇄 차단:
.parent { display: flow-root; }
.child { margin-top: 40px; }
/* .child의 margin-top이 .parent를 뚫고 합쳐지지 않음 */3) Inline Formatting Context (IFC) — 텍스트 흐름의 공간
블록 박스 안에 inline 자식만 있을 때 또는 inline 자식이 줄을 이룰 때 형성된다. IFC 안에서:
- 자식은 line box 단위로 묶임.
vertical-align,line-height,text-align이 의미를 가짐.width/height는 inline 자식에 무시됨.- baseline 정렬이 기본.
<p>Hello <em>world</em>, <img src="..." /> here.</p><p>는 BFC, 그 안의 텍스트+<em>+<img>는 IFC. 이미지의 baseline 아래 빈 공간 4px이 IFC의 라인 박스에서 비롯된다 (vertical-align: middle 또는 display: block으로 회피).
4) Stacking Context — z-index의 격리 공간
스택 컨텍스트는 Z축 정렬이 외부에 영향을 주지 않는 격리 영역이다. 새 스택 컨텍스트가 시작되면, 그 내부의 모든 z-index는 외부와 별개로 정렬된다.
Stacking Context를 만드는 조건
| 트리거 | 비고 |
|---|---|
<html> (root) | 자동 |
position: relative/absolute/fixed/sticky + z-index ≠ auto | 전통 |
opacity < 1 | 간과되기 쉬움 |
transform: ≠ none | 간과되기 쉬움 |
filter, backdrop-filter, mix-blend-mode | 모던 |
isolation: isolate | 이걸 쓰자 — 부작용 없이 stacking만 |
will-change: opacity/transform | 합성기 힌트 |
contain: layout / paint | 모던 |
flex/grid item with z-index ≠ auto | 의외 |
z-index: 9999가 안 먹히는 이유
.parent { transform: translateZ(0); } /* ← 여기서 새 stacking context */
.parent .modal { position: fixed; z-index: 9999; }
.outside { position: fixed; z-index: 1; }
/* .modal은 .parent의 컨텍스트 안에 갇혀서 .outside 아래로 갈 수 있음 */해결: .parent에서 transform을 제거하거나, .modal을 <body> 직속으로 portal.
isolation: isolate의 발견
.card { isolation: isolate; }이 한 줄은 부작용 없이 새 stacking context만 만든다. opacity나 transform을 만지지 않고도 z-index 격리를 얻는다. 2017년 이후 Baseline.
What — 4개 컨텍스트의 비교 표
| 컨텍스트 | 결정하는 것 | 트리거 | 디버깅 |
|---|---|---|---|
| Containing Block | top/left/width/% 기준 | position + 조상의 position/transform | DevTools Layout 탭 |
| BFC | 블록 흐름 격리 (float/margin) | flow-root, flex, overflow ≠ visible 등 | Layout > “Contained” 표시 |
| IFC | 텍스트 라인 박스 | block 안의 inline-only | line-height 시각화 |
| Stacking Context | Z축 정렬 | z-index, opacity<1, transform, isolation | DevTools “Layers” 탭 |
실무 예시 모음
예시 1: 모달이 부모에 갇힌다
.app-shell {
transform: translateZ(0); /* GPU 가속 의도 */
}
.modal {
position: fixed; inset: 0; z-index: 9999;
}
/* 결과: 모달이 viewport가 아니라 .app-shell에 갇힘 */transform이 fixed의 containing block과 stacking context를 모두 가로챈다. 해결: 모달을 portal로 <body> 직속에 두거나, .app-shell의 transform을 다른 방법(GPU 가속)으로 대체.
예시 2: float가 부모를 안 늘리는 옛 버그
<div class="parent">
<div style="float: left">A</div>
</div>
<style>
.parent { background: red; }
/* .parent 높이가 0 — float는 BFC를 안 만들면 부모 높이에 기여 안 함 */
</style>해결: .parent { display: flow-root; }.
예시 3: 마진이 부모를 뚫고 나간다
<div class="card">
<h1 style="margin-top: 40px">Title</h1>
</div>.card가 BFC가 아니면, <h1>의 margin-top이 .card의 위로 새어 합쳐진다. 해결: .card에 padding-top: 1px, border-top: 1px solid transparent, 또는 display: flow-root.
예시 4: position: sticky가 안 먹힌다
.aside { position: sticky; top: 0; }
.parent { overflow: hidden; } /* ← 여기 */sticky는 가장 가까운 scrolling ancestor를 기준 삼는데, overflow: hidden이 새 scroll context를 만들면 그 안에 갇힌다. 해결: 부모의 overflow를 풀거나, 부모를 overflow: clip(scroll context 만들지 않음)으로 바꾸거나, sticky 요소를 적절한 부모로 이동.
What-if — 잘못 쓰면
1) transform을 GPU 가속 hack으로 남용
will-change: transform 또는 transform: translateZ(0)은 합성기 힌트다. 하지만 containing block과 stacking context를 모두 가로채기 때문에 모달·포털 시스템을 깨트린다. 꼭 필요한 곳에만 한정.
2) z-index: 9999 군비 경쟁
.modal { z-index: 100; }
.tooltip { z-index: 101; }
.dropdown{ z-index: 999; }
.toast { z-index: 9999; }
.banner { z-index: 99999; }
/* 카지노 영수증이 되어 가는 z-index */원인은 거의 항상 stacking context 격리 부재. 해결:
- 컴포넌트 루트에
isolation: isolate. - Z-index를 토큰화 —
--z-modal: 50; --z-tooltip: 60;. - Layered z-index —
@layer base, components, overlays.
3) overflow: hidden을 BFC 만드는 용도로 쓰기
부작용으로 그림자·툴팁·드롭다운이 잘림. display: flow-root가 의도가 명확.
4) position: fixed가 부모에 갇히는 줄 모르고 디버깅 1시간
transform/filter/backdrop-filter/perspective/will-change/contain: paint — 이 6가지 조상 속성 중 하나가 fixed를 가둔다. DevTools에서 부모 체인을 거꾸로 올라가며 확인.
5) display: contents로 만든 부모는 stacking context 없음
display: contents인 요소는 박스가 없으므로 stacking context도 만들 수 없다. z-index 격리가 필요하면 다른 박스를 둬야 한다.
Insight — isolation: isolate는 디자인 시스템 시대의 작은 영웅
z-index 군비 경쟁의 종말
2015~2020년의 큰 SPA는 거의 모두 “z-index가 99999인 컴포넌트” 를 어딘가에 갖고 있었다. 원인은 같았다 — 각 컴포넌트가 자기 stacking context를 갖지 못해서, 모든 컴포넌트의 z-index가 글로벌 좌표에서 경쟁했기 때문이다.
isolation: isolate는 2017년 즈음 Baseline에 들어왔지만, 5~6년이 지나서야 디자인 시스템 진영에서 컴포넌트 루트의 표준 declaration으로 자리잡았다 (Radix UI, Headless UI, shadcn 등).
/* shadcn/ui Dialog 컴포넌트의 일부 */
.dialog-content {
position: fixed;
isolation: isolate;
z-index: 50;
/* ... */
}isolation: isolate 한 줄로 — 컴포넌트 내부의 z-index가 외부와 완전히 격리된다. 컴포넌트 안에서는 z-index: 1, 2, 3만 써도 되고, 다른 컴포넌트와 충돌하지 않는다. 컴포넌트 단위로 z-index를 캡슐화하는 사실상 표준.
또 하나의 진화 — contain 속성 (2019년 이후 Baseline). contain: layout은 layout의 영향 범위를 자식까지로 가둔다. contain: paint는 paint를 컴포넌트 박스 안으로 클리핑하면서 새 stacking context를 만든다. 그리고 contain: strict는 layout + paint + size를 모두 가둔다.
.list-item { contain: layout paint; }수천 개의 리스트 아이템이 있어도, 한 아이템의 변경이 다른 아이템의 layout/paint에 전파되지 않도록 브라우저에게 명시적으로 알려주는 것. 컨테인먼트는 성능과 컨텍스트 격리를 동시에 주는 모던 CSS의 핵심 발명 중 하나다.
CSS의 진화는 “컴포넌트가 자기 컨텍스트를 명시적으로 선언하게 만드는 방향” 이다 — isolation, contain, @container, @scope. 모두 전역 좌표계에서 지역 좌표계로의 이동이다.
요약 + Mermaid
- 같은 요소는 4개의 컨텍스트에 동시 소속 — containing block / BFC / IFC / stacking.
- BFC는
display: flow-root로 의도적으로 만든다 (clearfix 폐기). - Stacking context는
isolation: isolate로 부작용 없이 만든다. transform/filter/will-change는 fixed의 containing block을 가로챈다.- z-index 군비 경쟁의 해법은 컴포넌트마다 격리.
contain: layout/paint로 모던 컨테인먼트.