सदस्यता लें

브라우저 스태킹 컨텍스트란?

작성자
현우
작성시각
수정시각
레퍼런스
खाली
CSS로 레이아웃을 만들다 보면 요소들이 겹치는 상황이 생긴다. 모달, 툴팁, 드롭다운 같은 UI가 대표적이다. 이때 어떤 요소가 위에 올라올지를 제어하는 속성이 z-index다.
그런데 z-index는 단순히 "숫자가 크면 위에 온다"는 규칙으로 동작하지 않는다. 스태킹 컨텍스트(Stacking Context) 라는 경계 안에서만 비교되는 값이기 때문이다.
z-index: 9999를 줬는데 다른 요소 뒤에 가려진 적이 있다. 분명히 숫자는 더 큰데, 원하는 대로 쌓이지 않았다. 한참을 디버깅하다가 결국 찾아낸 원인은 스태킹 컨텍스트를 이해하지 못한 채로 z-index를 쓰고 있었던 것이었다.

스태킹 컨텍스트는 무엇일까?

스태킹 컨텍스트(Stacking Context) 는 z축(화면 깊이) 방향으로 요소를 쌓는 독립적인 단위다. 각 스태킹 컨텍스트는 내부적으로 고유한 레이어 순서를 가지며, 외부 스태킹 컨텍스트와 완전히 독립된다.
쉽게 말하면, z-index 비교는 같은 스태킹 컨텍스트 안에서만 유효하다. 부모가 다른 스태킹 컨텍스트에 속해 있다면, 자식의 z-index 값이 아무리 커도 그 경계를 넘어갈 수 없다.
Document (root stacking context)
  ├── A (z-index: 1, 새 stacking context 생성)
  │     └── A-1 (z-index: 9999)  ← A 안에 갇혀 있음
  └── B (z-index: 2, 새 stacking context 생성)
        └── B-1 (z-index: 1)
위 구조에서 A-1의 z-index가 9999여도, B보다 아래에 쌓인다. A 자체가 z-index: 1로 B(z-index: 2)보다 낮기 때문이다. A-1은 A의 스태킹 컨텍스트 안에서만 의미가 있다.

스태킹 컨텍스트는 언제 만들어질까?

모든 요소가 스태킹 컨텍스트를 생성하는 건 아니다. 특정 CSS 속성을 가진 요소가 새로운 스태킹 컨텍스트를 만든다.
/* 스태킹 컨텍스트를 생성하는 속성들 */

/* 1. position + z-index */
.el { position: relative; z-index: 1; }    /* ← z-index가 auto가 아니면 생성 */
.el { position: absolute; z-index: 0; }
.el { position: fixed; }                   /* ← z-index 없어도 생성 */
.el { position: sticky; }                  /* ← z-index 없어도 생성 */

/* 2. opacity */
.el { opacity: 0.99; }                     /* ← 1 미만이면 생성 */

/* 3. transform */
.el { transform: translateX(0); }          /* ← none이 아니면 생성 */

/* 4. filter */
.el { filter: blur(0px); }                 /* ← none이 아니면 생성 */

/* 5. isolation */
.el { isolation: isolate; }               /* ← 명시적으로 생성 */

/* 6. will-change */
.el { will-change: transform; }            /* ← GPU 레이어로 분리하면서 생성 */

/* 7. mix-blend-mode */
.el { mix-blend-mode: multiply; }

/* 8. clip-path, mask */
.el { clip-path: inset(0); }
이 중에서 실수가 가장 많이 발생하는 경우는 transform과 opacity다. 애니메이션 성능 최적화를 위해 transform: translateZ(0) 이나 will-change: transform을 추가했다가, 의도치 않게 스태킹 컨텍스트가 생성되어 z-index가 꼬이는 경우가 많다.

같은 스태킹 컨텍스트 안에서의 쌓임 순서

스태킹 컨텍스트 안에서도 요소들은 일정한 순서로 쌓인다.
같은 스태킹 컨텍스트 내 쌓임 순서 (아래에서 위로)

1. 스태킹 컨텍스트를 만든 요소 자체의 배경과 테두리
2. z-index가 음수인 자식 스태킹 컨텍스트
3. position이 없는 블록 요소 (div, p 등)
4. float 요소
5. position이 없는 인라인 요소
6. z-index가 auto 또는 0인 position 요소
7. z-index가 양수인 자식 스태킹 컨텍스트
z-index를 명시하지 않아도, position: relative를 추가하는 것만으로도 일반 블록 요소보다 위에 올라온다. z-index가 auto인 경우 새 스태킹 컨텍스트를 만들지 않으면서도 쌓임 순서에 영향을 준다.

실제로 문제가 되는 패턴들

패턴 1: 모달이 다른 요소 뒤에 가려지는 경우
<div class="sidebar" style="transform: translateX(0);">  <!-- ← 스태킹 컨텍스트 생성! -->
  <div class="tooltip" style="z-index: 100;">툴팁</div>
</div>

<div class="modal" style="z-index: 50;">모달</div>
Document
  ├── .sidebar (transform으로 스태킹 컨텍스트 생성, z-index: auto)
  │     └── .tooltip (z-index: 100)  ← sidebar 안에 갇힘
  └── .modal (z-index: 50)           ← sidebar보다 위에 올 수 있음
.tooltip의 z-index가 100이어도, .sidebar 자체에 z-index가 없기 때문에 .modal과의 비교는 .sidebar 단위로 이뤄진다. .sidebar.modal보다 먼저 쌓이면 .tooltip도 같이 가려진다.
패턴 2: 부모의 opacity가 자식을 가두는 경우
.parent {
  opacity: 0.99;       /* ← 이것만으로 스태킹 컨텍스트 생성 */
}

.child {
  z-index: 9999;       /* ← 의미 없음. parent 안에 갇혀 있음 */
  position: absolute;
}
fade-in 애니메이션을 위해 opacity를 건드리면, 그 시점에 스태킹 컨텍스트가 생성되면서 자식의 z-index 동작이 바뀔 수 있다.
패턴 3: CSS 애니메이션 중에만 레이어가 꼬이는 경우
.card {
  transition: transform 0.3s;   /* transform 시작 전: 스태킹 컨텍스트 없음 */
}

.card:hover {
  transform: scale(1.05);       /* ← hover 시 스태킹 컨텍스트 생성 */
}
hover 전후로 z-index 동작이 달라진다. 평소엔 잘 쌓이다가 hover 순간에 다른 요소 뒤로 사라지는 버그가 이런 패턴에서 나온다.

isolation: isolate — 의도적으로 컨텍스트를 만드는 방법

스태킹 컨텍스트를 의도치 않게 생성하는 게 문제라면, 반대로 의도적으로 생성해서 외부의 z-index 영향을 차단할 수도 있다.
isolation: isolate는 부작용 없이 스태킹 컨텍스트만 만드는 속성이다. transform이나 opacity처럼 시각적 변화를 수반하지 않는다.
.component {
  isolation: isolate;   /* ← z-index, transform, opacity 변화 없이 스태킹 컨텍스트 생성 */
}
컴포넌트 기반 개발에서 유용하다. 외부 환경의 z-index가 어떻게 설정되어 있든, 컴포넌트 내부의 레이어 순서를 독립적으로 관리할 수 있다.
<!-- 외부 z-index 환경에 영향받지 않는 독립된 컴포넌트 -->
<div class="dropdown" style="isolation: isolate;">
  <div class="dropdown-trigger">메뉴</div>
  <div class="dropdown-menu" style="z-index: 10;">   <!-- ← 이 컴포넌트 안에서만 유효 -->
    <ul>...</ul>
  </div>
</div>

스태킹 컨텍스트를 만드는 속성 비교해보기

속성
스태킹 컨텍스트 생성
시각적 부작용
주의 상황
position + z-index (auto 제외)
O
없음
의도한 경우가 대부분
opacity < 1
O
반투명 처리
애니메이션 중 일시적으로 생성될 수 있음
transform (none 제외)
O
변형/이동
성능 최적화용 translateZ(0) 사용 시 주의
filter (none 제외)
O
필터 효과
blur(0)도 해당
will-change
O
없음
GPU 레이어 분리와 동반 생성
isolation: isolate
O
없음
의도적 격리 목적에 최적
position: fixed/sticky
O
없음
z-index 없어도 생성

스태킹 컨텍스트 디버깅하는 방법

z-index 문제가 생겼을 때 가장 빠른 방법은 Chrome DevTools를 활용하는 것이다.
DevTools → Elements 탭 → 요소 선택 → Computed 탭
→ "Stacking context" 항목 확인
또는 Elements 탭 우측의 3D View를 활용하면 스태킹 컨텍스트 구조를 시각적으로 확인할 수 있다.
DevTools → 우측 상단 점 세 개 → More tools → Layers
Layers 탭에서 각 레이어가 어떤 요소에 의해 생성되었는지, 어떻게 합성되는지 확인할 수 있다.

어떻게 접근하는 것이 바람직할까?

z-index 관련 버그가 생겼을 때의 접근 순서는 아래와 같이 접근하면 좋다 ^,^ ..
1. 문제가 되는 요소의 부모 체인을 확인한다
어떤 조상 요소가 스태킹 컨텍스트를 만들고 있는지 확인한다. DevTools에서 부모 요소를 하나씩 클릭하며 transform, opacity, filter, will-change 속성이 있는지 체크한다.
2. 의도치 않은 스태킹 컨텍스트를 제거하거나 격리한다
불필요한 transform: translateZ(0) 이나 will-change가 있다면 제거하는 것이 좋다. 제거가 어렵다면 isolation: isolate로 컴포넌트 경계를 명확히 하는 것도 방법이다.
3. z-index 값은 의미 있는 범위 내에서 관리한다
z-index를 9999 같은 큰 값으로 설정하는 것은 문제를 해결하는 게 아니라 덮는 것이다. 스태킹 컨텍스트 구조를 이해한 뒤, 같은 컨텍스트 안에서 1, 2, 3처럼 작은 값으로도 충분히 제어할 수 있다.
z-index 문제가 생겼다면 값을 키우기 전에, 먼저 DevTools에서 스태킹 컨텍스트 구조를 확인해보자.