diff --git a/apps/blog/public/stacking-context-ex1.png b/apps/blog/public/stacking-context-ex1.png new file mode 100644 index 0000000..2cb2390 Binary files /dev/null and b/apps/blog/public/stacking-context-ex1.png differ diff --git a/apps/blog/public/stacking-context-ex2.png b/apps/blog/public/stacking-context-ex2.png new file mode 100644 index 0000000..8146cce Binary files /dev/null and b/apps/blog/public/stacking-context-ex2.png differ diff --git a/apps/blog/public/stacking-context-ex3.png b/apps/blog/public/stacking-context-ex3.png new file mode 100644 index 0000000..4b8f510 Binary files /dev/null and b/apps/blog/public/stacking-context-ex3.png differ diff --git a/apps/blog/src/app/(main)/posts/frontend/css-stacking-context/page.mdx b/apps/blog/src/app/(main)/posts/frontend/css-stacking-context/page.mdx new file mode 100644 index 0000000..481c5b2 --- /dev/null +++ b/apps/blog/src/app/(main)/posts/frontend/css-stacking-context/page.mdx @@ -0,0 +1,141 @@ +import { frontmatter } from '@libs/frontmatter'; + +export const metadata = frontmatter({ + title: 'z-index를 9999로 줬는데 왜 안 올라올까: 쌓임 맥락 완전 정복', + description: + 'z-index가 큰데도 요소가 아래에 깔리는 현상을 쌓임 맥락(stacking context)으로 설명합니다. 부모의 opacity가 만드는 함정, 새 맥락을 만드는 속성들, 그리고 isolation: isolate로 깔끔하게 다스리는 법을 실제 렌더링 결과로 정리합니다.', + seriesId: 'frontend', + postId: 'css-stacking-context', + tags: ['CSS', 'z-index', 'stacking context', '레이아웃'], + date: '2026-07-06 22:00', +}); + +## z-index: 9999인데 왜 안 올라올까 + +드롭다운이나 모달을 만들다 보면 한 번쯤 겪는 일이 있습니다. 분명 `z-index`를 `9999`까지 올렸는데도 요소가 다른 것 아래에 깔려 있는 상황입니다. 저도 처음엔 숫자를 더 키우면 되겠지 싶어 `99999`, `999999`까지 올려봤지만 소용없었습니다. + +원인은 `z-index` 값이 작아서가 아니었습니다. 그 요소가 **비교 자체가 안 되는 다른 세계**에 갇혀 있었기 때문입니다. 이 "세계"가 바로 쌓임 맥락(stacking context)입니다. 이 글에서는 눈으로 보이는 렌더링 결과부터 시작해, 왜 이런 일이 벌어지고 어떻게 다스리는지 정리합니다. + +## 같은 맥락에서는 z-index가 잘 동작한다 + +먼저 정상적인 경우입니다. 회색 상자와 빨강 상자를 겹쳐두고, 회색에만 `z-index: 10`을 줬습니다. + +```html +
+
+
회색 (z-index: 10)
+
+
+
빨강 (z-index: auto)
+
+
+``` + +```css +.stage { + position: relative; +} +.box { + position: absolute; +} +.gray { + z-index: 10; +} +/* .red 에는 z-index를 주지 않았다 (auto) */ +``` + +![회색 상자와 빨강 상자가 겹친 화면. 회색 상자에 z-index 10이 주어져 빨강 상자보다 위에 그려진다. 부모에는 특별한 속성이 없다.](/stacking-context-ex1.png) + +기대한 그대로입니다. `z-index`가 큰 회색이 빨강 위에 놓입니다. 두 상자가 **같은 쌓임 맥락**에 있어서 `z-index` 숫자를 곧바로 비교할 수 있기 때문입니다. 여기까지는 아무 문제가 없습니다. + +## z-index가 큰데도 아래에 깔리는 순간 + +이제 딱 한 가지만 바꿉니다. 회색 상자를 감싼 부모(`.wrap-gray`)에 `opacity: 0.6`을 줍니다. 상자 자체가 아니라 **부모**에 준 것이고, `z-index`는 건드리지 않았습니다. + +```css +.wrap-gray { + opacity: 0.6; /* 이 한 줄이 전부다 */ +} +``` + +![부모에 opacity 0.6이 적용된 화면. 회색 상자에 z-index 10이 있는데도 z-index가 없는 빨강 상자가 위에 그려지고, 회색 상자는 반투명하게 흐려져 있다.](/stacking-context-ex2.png) + +결과가 뒤집혔습니다. `z-index: 10`을 가진 회색이 `z-index`도 없는 빨강 아래로 내려가 버렸습니다. 회색이 살짝 흐려진 것 말고는 바꾼 게 없는데 말입니다. + +`opacity: 0.6`이 회색의 부모를 **새로운 쌓임 맥락의 루트**로 만들었기 때문입니다. 그 순간 회색의 `z-index: 10`은 부모가 만든 맥락 안에 갇힙니다. 바깥의 빨강과는 더 이상 숫자로 겨루지 못합니다. 앞에서 `z-index`를 아무리 키워도 소용없던 이유가 바로 이것입니다. + +## 쌓임 맥락이란 무엇인가 + +쌓임 맥락은 z축(화면에서 나를 향해 튀어나오는 방향)으로 요소를 쌓을 때 쓰는 **독립된 그룹**입니다. 페이지 전체는 최상위 맥락(루트, ``) 하나로 시작하고, 특정 조건이 갖춰지면 그 안에 자식 맥락이 새로 생깁니다. + +핵심 규칙은 두 가지입니다. + +1. `z-index`는 **같은 쌓임 맥락에 속한 형제끼리만** 비교된다. +2. 서로 다른 맥락에 속한 요소는, 각자의 **맥락 루트가 상위 맥락에서 어떤 순서인지**에 따라 통째로 결정된다. 자식의 `z-index`가 아무리 커도 맥락 밖으로 나가지 못한다. + +앞의 예제가 정확히 이 규칙입니다. 회색의 `z-index: 10`은 부모 맥락 안에서만 유효합니다. 부모(`.wrap-gray`)와 빨강 쪽(`.wrap-red`)은 둘 다 별도의 `z-index`가 없으니, 상위 맥락에서는 **DOM에 나온 순서**로 정해집니다. 나중에 나온 빨강이 위로 올라오는 것입니다. + +비유하자면 회사 조직도와 같습니다. 다른 팀의 팀원과 내 직급을 직접 비교할 수 없습니다. 먼저 두 팀(맥락 루트)의 서열이 정해지고, 그 안에서만 팀원끼리 순서를 다툽니다. 회색은 아무리 팀 내 1등이어도, 팀 자체가 뒤에 있으면 앞으로 나올 수 없습니다. + +## 무엇이 새 쌓임 맥락을 만드는가 + +문제의 절반은 "내가 언제 맥락을 만들었는지 모른다"는 데 있습니다. `opacity`처럼 `z-index`와 상관없어 보이는 속성도 맥락을 만들기 때문입니다. 자주 마주치는 조건을 모아두면 다음과 같습니다. + +- 루트 요소 `` (기본으로 존재하는 최상위 맥락) +- `position: relative` 또는 `absolute`이면서 `z-index`가 `auto`가 아닐 때 +- `position: fixed` 또는 `sticky` (이 둘은 `z-index` 없이도 항상 만든다) +- `opacity`가 1보다 작을 때 +- `transform`, `filter`, `backdrop-filter`, `perspective`, `clip-path`, `mask`가 `none`이 아닐 때 +- `mix-blend-mode`가 `normal`이 아닐 때 +- `isolation: isolate` +- `will-change`에 위 속성 중 하나를 지정했을 때 +- `contain: layout`, `paint`, `strict`, `content` +- `container-type`이 `size` 또는 `inline-size`일 때 (컨테이너 쿼리) +- flex/grid 컨테이너의 자식이면서 `z-index`가 `auto`가 아닐 때 + +목록이 길지만 요점은 하나입니다. `transform`으로 애니메이션을 걸거나, `opacity`로 페이드를 주거나, `filter`로 그림자를 넣는 **평범한 스타일이 조용히 맥락을 만든다**는 것입니다. 그래서 "분명 `z-index`만 만졌는데 안 된다" 싶을 때는, 대체로 조상 어딘가에 이런 속성이 하나 껴 있습니다. + +## isolation: isolate로 맥락을 의도적으로 만들기 + +맥락은 골칫거리이기만 한 게 아닙니다. 필요할 때 **깔끔하게** 만들 수도 있습니다. 이때 쓰는 속성이 `isolation: isolate`입니다. + +앞의 `opacity: 0.6` 대신 부모에 `isolation: isolate`를 줘봅니다. + +```css +.wrap-gray { + isolation: isolate; /* 렌더링은 그대로, 맥락만 새로 만든다 */ +} +``` + +![부모에 isolation isolate가 적용된 화면. 예제 2와 동일하게 빨강 상자가 위에 그려지지만, 회색 상자는 흐려지지 않고 또렷하게 유지된다.](/stacking-context-ex3.png) + +쌓임 결과는 `opacity` 예제와 똑같습니다. 회색의 `z-index: 10`이 부모 맥락에 갇혀 빨강이 위로 올라옵니다. 하지만 이번엔 회색이 흐려지지 않고 **또렷하게** 남아 있습니다. `isolation: isolate`는 오직 "여기에 새 쌓임 맥락을 만든다"는 일만 하고, 색이나 투명도 같은 시각적 결과는 건드리지 않기 때문입니다. + +이 속성이 유용한 진짜 상황은 반대 방향입니다. 어떤 컴포넌트 내부의 `z-index`들이 바깥으로 새어 나가 다른 요소와 충돌하는 걸 막고 싶을 때, 그 컴포넌트의 루트에 `isolation: isolate`를 한 줄 주면 됩니다. 그러면 내부 `z-index`는 그 안에서만 통용되어, 바깥 세계와 깔끔하게 분리됩니다. `opacity: 0.999`나 `transform: translateZ(0)` 같은 편법으로 맥락을 만드는 트릭을 종종 보는데, `isolation: isolate`가 그 의도를 그대로 드러내는 정공법입니다. + +## 실전: 버그를 어떻게 추적하나 + +"내 요소가 왜 안 올라오지?"를 만나면 순서는 이렇습니다. + +1. 문제의 요소에서 시작해 **부모를 타고 위로** 올라가며, 앞 목록의 속성(`opacity`, `transform`, `filter`, `position: fixed` 등)을 가진 조상을 찾는다. +2. 그 조상이 새 맥락을 만든 범인이다. 내 요소의 `z-index`는 거기 갇혀 있다. +3. 해결은 둘 중 하나다. 그 조상 밖으로 요소를 빼내(예: `portal`로 `body` 아래로 옮기기) 같은 맥락에서 겨루게 하거나, 맥락을 만든 조상 자체의 순서를 올린다. + +브라우저 개발자 도구에서 요소를 선택하고 Computed 탭에서 `opacity`, `transform` 등을 훑어보면 범인을 빠르게 좁힐 수 있습니다. 숫자를 더 키우는 대신, **어느 맥락에 갇혔는지**를 먼저 확인하는 습관이 시간을 아껴줍니다. + +## 정리 + +| 상황 | 무슨 일이 벌어지나 | 대응 | +| --------------------------- | ---------------------------------------------- | ------------------------------------------ | +| 같은 맥락 안 | `z-index` 숫자가 그대로 비교된다 | 값만 조정하면 된다 | +| 조상이 새 맥락을 만듦 | 내 `z-index`가 그 맥락 안에 갇힌다 | 범인 조상을 찾아 밖으로 빼거나 조상을 올림 | +| 맥락을 의도적으로 만들고 싶음 | 내부 `z-index`를 바깥과 분리하고 싶다 | `isolation: isolate` 한 줄 | + +- `z-index`는 같은 쌓임 맥락 안의 형제끼리만 비교된다. 다른 맥락과는 숫자로 겨루지 못한다. +- `opacity < 1`, `transform`, `filter`, `position: fixed/sticky` 같은 평범한 속성이 조용히 새 맥락을 만든다. +- 자식의 `z-index`는 맥락 밖으로 나가지 못한다. 그래서 `9999`도 소용없을 때가 있다. +- 맥락을 일부러 만들고 싶다면 부작용 없는 `isolation: isolate`가 정공법이다. + +## 마치며 + +`z-index`가 안 먹힐 때 숫자를 키우는 건 대부분 헛수고입니다. 진짜 질문은 "이 요소가 어느 쌓임 맥락에 있는가"입니다. 이 관점을 한 번 잡아두면, 모달이 사이드바 뒤로 숨거나 드롭다운이 헤더에 가려지는 문제를 훨씬 빠르게 풀 수 있습니다. 다음에 요소가 이유 없이 아래로 깔린다면, 숫자 대신 조상의 스타일부터 들여다보시길 권합니다. 긴 글 읽어주셔서 감사합니다.