z-index가 안 먹을 때
왜 이 문제를 만났나?
모달을 만들었는데 다른 요소 뒤에 숨어버렸습니다:
.modal {
z-index: 9999;
}
.header {
z-index: 10;
}
분명 모달의 z-index가 훨씬 높은데 헤더 뒤에 숨었어요!
처음엔 뭐가 이해가 안 갔나?
제가 가진 오개념은 이거였습니다: "z-index가 높으면 무조건 위에 온다"
하지만 stacking context가 문제였습니다. z-index는 같은 stacking context 안에서만 비교돼요!
어떤 포인트에서 이해가 됐나?
이 문제를 이해한 건 이런 비유를 들었을 때였습니다:
"각 방(stacking context)에서 사람들(요소)이 줄을 선다. 방 안에서는 키(z-index)로 순서가 정해지지만, 다른 방의 사람과는 비교 안 된다."
해결책은 position을 설정하는 겁니다:
.modal {
position: fixed; /* ✅ position 필수! */
z-index: 9999;
}
z-index는 position: static이 아닌 요소에만 작동합니다!
깊이 파고들기
Stacking Context 생성 조건
다음 속성들이 새로운 stacking context를 만듭니다:
/* 1. position + z-index */
.element {
position: relative;
z-index: 1;
}
/* 2. opacity < 1 */
.element {
opacity: 0.99;
}
/* 3. transform */
.element {
transform: translateZ(0);
}
/* 4. filter */
.element {
filter: blur(1px);
}
/* 5. will-change */
.element {
will-change: transform;
}
문제 예시
<div class="parent" style="position: relative; z-index: 1;">
<div class="child" style="position: relative; z-index: 9999;">
Child
</div>
</div>
<div class="other" style="position: relative; z-index: 2;">
Other
</div>
child의 z-index가 9999여도 other 뒤에 숨습니다! parent의 z-index가 1이기 때문이에요.
해결 방법
/* 1. 모달을 body 직속으로 이동 */
ReactDOM.createPortal(<Modal />, document.body)
/* 2. parent의 z-index 제거 */
.parent {
position: relative;
/* z-index 제거 */
}
/* 3. parent의 z-index를 더 높게 */
.parent {
position: relative;
z-index: 10000;
}
4. isolation: isolate (새로운 방법)
최신 CSS 속성인 isolation: isolate를 사용하면 명시적으로 새로운 Stacking Context를 만들 수 있습니다.
transform이나 opacity 같은 부작용 없이 컨텍스트만 분리하고 싶을 때 유용합니다.
.card-wrapper {
isolation: isolate;
/* 내부 요소들의 z-index 계산이 이 안에서만 이루어짐 */
}
6. 브라우저 렌더링 파이프라인과 Stacking Context
사실 Stacking Context는 브라우저가 화면을 그리는 "Painting" 단계와 관련이 깊습니다. 브라우저는 DOM 트리를 해석한 후, Layer Tree를 만듭니다.
- DOM Tree: HTML 태그의 구조
- Render Tree: 스타일이 적용된 구조 (
display: none제외) - Layer Tree: Stacking Context에 따라 층을 나눔 (포토샵 레이어처럼)
- Paint: 각 레이어를 픽셀로 변환
- Composite: 레이어들을 하나로 합침
자식 요소(Child)가 아무리 z-index: 9999를 외쳐도, 부모(Parent)가 생성한 레이어 안에 갇혀 있다면, 그 부모보다 낮은 레이어 위에 올라갈 수 없는 것입니다.
이 원리를 이해하면 opacity나 transform이 왜 새로운 Stacking Context를 만드는지 이해할 수 있습니다. (GPU 가속을 위해 별도의 레이어로 분리되기 때문입니다!)
7. 체계적인 관리 - SCSS/Sass Map 사용하기
프로젝트가 커지면 z-index: 9999 같은 매직 넘버가 난무하게 됩니다.
Sass Map으로 중앙 관리하세요.
/* _variables.scss */
$z-indexes: (
'toast': 9000,
'modal': 8000,
'dropdown': 7000,
'header': 1000,
'default': 1
);
@function z($key) {
@return map-get($z-indexes, $key);
}
/* 사용 */
.modal {
z-index: z('modal');
}
이렇게 하면 "헤더보다 모달이 높아야지"라는 계층 구조가 한눈에 보입니다.
내 코드에 어떻게 적용했나?
Portal 사용
모달을 Portal로 렌더링했습니다:
import { createPortal } from 'react-dom';
function Modal({ children, isOpen }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay">
<div className="modal-content">
{children}
</div>
</div>,
document.body
);
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: relative;
z-index: 1001;
}
한 줄 요약
z-index는 position이 static이 아닌 요소에만 작동하고, 같은 stacking context 안에서만 비교되므로, 모달 같은 요소는 Portal로 body에 직접 렌더링해야 한다.