🎨 Frontend CSS0. CSS의 기초05 — Position Context (BFC·IFC·Stacking)

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이 무엇이냐에 따라 기준이 되는 박스가 다르다.

positionContaining Block
static (기본)가장 가까운 block-level 조상의 content box
relative자신의 normal flow 위치 (top/left는 이동량)
absolute가장 가까운 positionstatic이 아닌 조상
fixedviewport (또는 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-blockinline 외부 흐름 + 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만 만든다. opacitytransform을 만지지 않고도 z-index 격리를 얻는다. 2017년 이후 Baseline.


What — 4개 컨텍스트의 비교 표

컨텍스트결정하는 것트리거디버깅
Containing Blocktop/left/width/% 기준position + 조상의 position/transformDevTools Layout 탭
BFC블록 흐름 격리 (float/margin)flow-root, flex, overflow ≠ visibleLayout > “Contained” 표시
IFC텍스트 라인 박스block 안의 inline-onlyline-height 시각화
Stacking ContextZ축 정렬z-index, opacity<1, transform, isolationDevTools “Layers” 탭

실무 예시 모음

예시 1: 모달이 부모에 갇힌다

.app-shell {
  transform: translateZ(0); /* GPU 가속 의도 */
}
.modal {
  position: fixed; inset: 0; z-index: 9999;
}
/* 결과: 모달이 viewport가 아니라 .app-shell에 갇힘 */

transformfixed의 containing blockstacking 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위로 새어 합쳐진다. 해결: .cardpadding-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: layoutlayout의 영향 범위를 자식까지로 가둔다. contain: paintpaint를 컴포넌트 박스 안으로 클리핑하면서 새 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-changefixed의 containing block을 가로챈다.
  • z-index 군비 경쟁의 해법은 컴포넌트마다 격리.
  • contain: layout/paint로 모던 컨테인먼트.