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

CSS로 레이아웃을 만들다 보면 요소들이 겹치는 상황이 생긴다. 모달, 툴팁, 드롭다운 같은 UI가 대표적이다. 이때 어떤 요소가 위에 올라올지를 제어하는 속성이 `z-index`다.

그런데 `z-index`는 단순히 "숫자가 크면 위에 온다"는 규칙으로 동작하지 않는다. **스태킹 컨텍스트(Stacking Context)** 라는 경계 안에서만 비교되는 값이기 때문이다.

`z-index: 9999`를 줬는데 다른 요소 뒤에 가려진 적이 있다. 분명히 숫자는 더 큰데, 원하는 대로 쌓이지 않았다. 한참을 디버깅하다가 결국 찾아낸 원인은 스태킹 컨텍스트를 이해하지 못한 채로 z-index를 쓰고 있었던 것이었다.

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

**스태킹 컨텍스트(Stacking Context)** 는 z축(화면 깊이) 방향으로 요소를 쌓는 독립적인 단위다. 각 스태킹 컨텍스트는 내부적으로 고유한 레이어 순서를 가지며, 외부 스태킹 컨텍스트와 완전히 독립된다.

쉽게 말하면, z-index 비교는 같은 스태킹 컨텍스트 안에서만 유효하다. 부모가 다른 스태킹 컨텍스트에 속해 있다면, 자식의 z-index 값이 아무리 커도 그 경계를 넘어갈 수 없다.

```javascript
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 속성을 가진 요소가 새로운 스태킹 컨텍스트를 만든다.

```javascript
/* 스태킹 컨텍스트를 생성하는 속성들 */

/* 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가 꼬이는 경우가 많다.

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

스태킹 컨텍스트 안에서도 요소들은 일정한 순서로 쌓인다.

```javascript
같은 스태킹 컨텍스트 내 쌓임 순서 (아래에서 위로)

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: 모달이 다른 요소 뒤에 가려지는 경우**

```javascript
<div class="sidebar" style="transform: translateX(0);">  <!-- ← 스태킹 컨텍스트 생성! -->
  <div class="tooltip" style="z-index: 100;">툴팁</div>
</div>

<div class="modal" style="z-index: 50;">모달</div>
```

```javascript
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가 자식을 가두는 경우**

```javascript
.parent {
  opacity: 0.99;       /* ← 이것만으로 스태킹 컨텍스트 생성 */
}

.child {
  z-index: 9999;       /* ← 의미 없음. parent 안에 갇혀 있음 */
  position: absolute;
}
```

fade-in 애니메이션을 위해 opacity를 건드리면, 그 시점에 스태킹 컨텍스트가 생성되면서 자식의 z-index 동작이 바뀔 수 있다.

**패턴 3: CSS 애니메이션 중에만 레이어가 꼬이는 경우**

```javascript
.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처럼 시각적 변화를 수반하지 않는다.

```javascript
.component {
  isolation: isolate;   /* ← z-index, transform, opacity 변화 없이 스태킹 컨텍스트 생성 */
}
```

컴포넌트 기반 개발에서 유용하다. 외부 환경의 z-index가 어떻게 설정되어 있든, 컴포넌트 내부의 레이어 순서를 독립적으로 관리할 수 있다.

```javascript
<!-- 외부 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를 활용하는 것이다.

```javascript
DevTools → Elements 탭 → 요소 선택 → Computed 탭
→ "Stacking context" 항목 확인
```

또는 Elements 탭 우측의 **3D View**를 활용하면 스태킹 컨텍스트 구조를 시각적으로 확인할 수 있다.

```javascript
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에서 스태킹 컨텍스트 구조를 확인해보자.

For the site tree, see the [root Markdown](https://slashpage.com/timmy.md).
