CSS Stacking Context — z-index: 99999를 줬는데 왜 안 올라올까
z-index: 99999를 줬는데 요소가 올라오지 않는다. 값을 999999로 올려도 마찬가지다. 이 상황에서 대부분 숫자를 더 키우거나, !important를 붙이거나, HTML 구조를 뒤집기 시작한다. 하지만 문제의 원인은 숫자의 크기가 아니다. 쌓임 맥락(Stacking Context)을 이해하지 못했기 때문이다.
쌓임 맥락이란
브라우저는 HTML 요소들을 2차원 평면에 배치하는 것처럼 보이지만, 실제로는 z축이라는 깊이 차원도 관리한다. 쌓임 맥락은 이 z축 위에서 요소들이 어떤 순서로 겹치는지를 결정하는 독립적인 레이어 그룹이다.
비유하면, 쌓임 맥락은 봉투와 같다. 봉투 안에 여러 장의 종이(자식 요소)가 들어 있고, 종이들 사이의 순서는 봉투 내부에서만 유효하다. 아무리 봉투 안의 종이에 "나는 맨 위야!"라고 적어놔도, 봉투 자체가 다른 봉투 아래에 있으면 절대 위로 올라올 수 없다.
.parent-a {
position: relative;
z-index: 1;
}
.parent-b {
position: relative;
z-index: 2;
}
.child-of-a {
position: absolute;
z-index: 99999;
}.child-of-a의 z-index가 99999여도, .parent-a의 z-index가 1이므로 .parent-b(z-index: 2) 아래에 머문다. z-index는 같은 쌓임 맥락 안에서만 비교된다.
[💡 잠깐! 이 용어는?] 쌓임 맥락(Stacking Context): z축 위에서 요소들의 렌더링 순서를 결정하는 독립적인 그룹. 쌓임 맥락이 생성되면 그 안의 모든 자식 요소는 해당 맥락 내에서만 z-index로 순서가 매겨진다.
쌓임 맥락을 생성하는 조건들
z-index를 명시적으로 설정했을 때만 생기는 게 아니다. 의외로 많은 CSS 속성이 암묵적으로 쌓임 맥락을 만든다. 이게 버그의 주범이다.
/* 1. 기본 — position + z-index */
.context-1 {
position: relative;
z-index: 1;
}
/* 2. opacity가 1 미만이면 생성 */
.context-2 {
opacity: 0.99;
}
/* 3. transform 값이 있으면 생성 */
.context-3 {
transform: translateZ(0);
}
/* 4. 의도적 생성 전용 속성 */
.context-4 {
isolation: isolate;
}
/* 5. will-change도 생성 */
.context-5 {
will-change: transform;
}
/* 6. filter도 생성 */
.context-6 {
filter: blur(0);
}전체 조건 목록
| 조건 | 설명 |
|---|---|
position: relative/absolute + z-index 값 지정 | 가장 기본적인 생성 방식 |
position: fixed 또는 sticky | z-index 없어도 생성 |
opacity < 1 | opacity: 0.99만으로도 생성 |
transform 값 존재 | transform: none이 아닌 모든 값 |
filter 값 존재 | filter: blur(0)도 포함 |
will-change | 특정 속성을 명시하면 생성 |
isolation: isolate | 의도적 생성 전용 속성 |
contain: layout 또는 paint | 컨테인먼트 관련 |
Flex/Grid 자식 + z-index | position 없이도 작동 |
mix-blend-mode 값 존재 | normal이 아닌 값 |
clip-path, mask | 클리핑/마스킹 관련 |
주의할 점은 Flex/Grid 자식 요소다. 일반적으로 z-index가 작동하려면 position이 static이 아니어야 하지만, flex 또는 grid 컨테이너의 자식이라면 position: relative 없이도 z-index가 동작하고 쌓임 맥락이 생성된다.
[💡 잠깐! 이 용어는?]
isolation: isolate: 쌓임 맥락을 의도적으로 생성하기 위한 CSS 속성. 다른 부수 효과 없이 오직 쌓임 맥락만 만든다. opacity: 0.99나 transform: translateZ(0) 같은 핵(hack) 대신 사용하는 정식 방법.
쌓임 순서 규칙
같은 쌓임 맥락 안에서 요소들은 다음 순서로 쌓인다(아래부터 위로):
1. 쌓임 맥락의 배경과 테두리
2. z-index가 음수인 자식
3. in-flow, non-positioned 블록 요소
4. non-positioned 플로팅 요소
5. in-flow, non-positioned 인라인 요소
6. z-index: 0 또는 z-index: auto인 positioned 요소
7. z-index가 양수인 자식"왜 z-index: 0을 준 요소가 z-index를 안 준 요소보다 위에 있지?"라고 헷갈릴 수 있다. positioned 요소(z-index: auto 포함)는 non-positioned 요소보다 항상 위에 쌓인다.
실전 디버깅 전략
1단계: 쌓임 맥락 트리 파악하기
브라우저 DevTools에서 해당 요소의 부모 체인을 추적하며, 어떤 요소가 쌓임 맥락을 생성하고 있는지 확인한다.
.modal-overlay {
position: fixed;
z-index: 1000;
}
.header {
position: sticky;
top: 0;
z-index: 100;
}
.sidebar {
transform: translateX(0);
/* ← 이 한 줄이 쌓임 맥락을 만든다 */
}이 예시에서 .sidebar는 transform 때문에 쌓임 맥락이 생성된다. .sidebar 안의 드롭다운에 아무리 높은 z-index를 줘도, .sidebar의 쌓임 맥락 밖으로 나갈 수 없다.
2단계: isolation: isolate 활용하기
의도적으로 쌓임 맥락의 경계를 만들 때는 isolation: isolate를 사용한다. 마치 칸막이를 세워서 "여기서부터는 별도 구역이다"라고 선언하는 것과 같다.
.card {
isolation: isolate;
}
.card__badge {
position: absolute;
z-index: 1;
}
.card__image {
position: relative;
z-index: 0;
}.card에 isolation: isolate를 주면, .card__badge의 z-index는 카드 내부에서만 유효하다. 다른 카드나 외부 요소의 z-index와 충돌하지 않는다.
[💡 잠깐! 이 용어는?] will-change: 브라우저에 "이 속성이 곧 변할 것이니 미리 최적화해두라"고 힌트를 주는 CSS 속성. 성능 최적화 목적이지만, 부작용으로 쌓임 맥락을 생성한다. 남용하면 오히려 메모리 사용량이 증가한다.
z-index 디버깅 체크리스트
| 증상 | 원인 | 해결책 |
|---|---|---|
| z-index를 올려도 안 올라옴 | 부모의 쌓임 맥락이 낮음 | 부모의 z-index 확인/조정 |
| opacity 애니메이션 후 레이어 꼬임 | opacity < 1이 새 맥락 생성 | animation 종료 후 opacity 제거 |
| transform 적용 후 드롭다운 잘림 | transform이 새 맥락 생성 | isolation: isolate로 구조 분리 |
| 모달이 헤더 아래에 깔림 | 모달과 헤더가 다른 맥락 | 같은 쌓임 맥락에서 비교되도록 구조 변경 |
| Flex 자식의 z-index가 의도대로 안 됨 | Flex 자식은 position 없이 맥락 생성 | 명시적으로 position: relative 추가 |
가장 확실한 해결 전략은 "z-index 숫자를 키우는 것"이 아니라, 쌓임 맥락의 구조를 제어하는 것이다.
마무리
z-index 문제의 99%는 숫자가 작아서가 아니라, 쌓임 맥락의 구조를 파악하지 못해서 발생한다.
- 봉투 안의 종이가 아무리 높은 번호표를 가져도, 봉투 자체의 순서를 바꾸지 않으면 소용없다
isolation: isolate를 적극 활용해 의도적으로 쌓임 맥락의 경계를 관리하자- DevTools에서 어떤 속성이 암묵적으로 쌓임 맥락을 생성하는지 추적하는 습관을 들이면, z-index 전쟁에서 벗어날 수 있다
참고:
- MDN Stacking Context: https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Positioned_layout/Stacking_context
- Josh W. Comeau: https://www.joshwcomeau.com/css/stacking-contexts/
관심 있을 만한 포스트
CSS @property — 커스텀 속성에 타입을 부여하는 방법
CSS @property at-rule로 커스텀 속성에 타입 정의, 상속 제어, 폴백을 추가해 렌더링 안정성과 애니메이션 가능성을 확보하는 방법을 다룬다.
CSS :near() — 마우스가 '가까이' 오면 반응하는 새로운 의사 클래스
CSS Working Group에 제안된 :near() 의사 클래스는 포인터 근접성을 감지해 호버 전에 UI를 활성화하는 새로운 상호작용 패턴을 연다.
CSS만으로 커스텀 셀렉트 박스 — JavaScript 150줄이 사라지는 순간
Chrome 135에 도입된 appearance: base-select와 sibling-index()로 JavaScript 없이 완전한 커스텀 드롭다운을 구현하는 방법을 분석한다.
sibling-index()로 만드는 CSS 스크롤 소용돌이 — JavaScript 없이 수백 개 요소 애니메이션
CSS sibling-index()와 scroll-driven animations를 결합해 순수 CSS만으로 텍스트 보텍스 효과를 구현하는 기법을 다룬다.
Interop 2026 — 브라우저 전쟁이 끝나고 표준 전쟁이 시작됐다
Chrome, Safari, Firefox가 합의한 20개 웹 표준 집중 영역과 프론트엔드 개발자가 주목해야 할 핵심 기능을 정리한다.
Next.js 블로그 만들기 — 스크롤 프로그레스 바와 Canvas 렌더링 이슈 해결
스크롤 진행률 프로그레스 바 구현과 Canvas 커서 효과가 GNB backdrop-blur와 충돌하며 발생한 깜빡임 이슈 해결기.
OHP 필름을 겹치듯 — conic-gradient와 attr()로 순수 CSS 파이 차트 만들기
conic-gradient, CSS 커스텀 프로퍼티, 새로운 attr() 타입 구문을 활용해 JavaScript 없이 시맨틱한 파이 차트를 구현하는 방법을 다룬다.
배경색이 바뀌면 글자색도 따라가야 한다 — contrast-color() 없이 살아남는 법
브라우저 지원이 부족한 CSS contrast-color()를 color-mix(), relative color syntax, custom properties로 근사 구현하는 방법을 정리한다.