
웹 개발을 하다 보면 z-index: 9999를 설정했는데도 요소가 원하는 위치에 쌓이지 않아 당황한 경험이 있을 것입니다. 이는 대부분 쌓임 맥락(Stacking Context)을 제대로 이해하지 못해서 발생하는 문제입니다. 오늘은 CSS에서 가장 헷갈리는 개념 중 하나인 쌓임 맥락을 완전히 정복해보겠습니다.
쌓임 맥락(Stacking Context)은 HTML 요소들이 가상의 Z축 상에서 어떻게 배치되는지를 결정하는 3차원 렌더링 개념입니다. 마치 투명한 유리판 여러 개를 겹쳐 놓은 것처럼, 각 요소가 어느 층에 위치할지를 정하는 규칙이라고 생각하면 됩니다.
쌓임 맥락을 이해하기 위해 책상 위의 서류 더미를 생각해보세요:
📄 최상위 서류 (Z-index가 가장 높음)
📄 중간 서류
📄 바닥 서류 (Z-index가 가장 낮음)
🖥️ 책상 (HTML 문서)
먼저 z-index가 없을 때의 기본 쌓임 규칙을 알아보겠습니다.
<div class="box1">첫 번째 박스</div>
<div class="box2">두 번째 박스</div>
<div class="box3">세 번째 박스</div>
.box1, .box2, .box3 {
width: 100px;
height: 100px;
position: absolute;
}
.box1 { background: red; top: 0; left: 0; }
.box2 { background: green; top: 20px; left: 20px; }
.box3 { background: blue; top: 40px; left: 40px; }
결과: box3(파란색) → box2(초록색) → box1(빨간색) 순으로 쌓임 (나중에 온 것이 위에)
| 요소 타입 | 쌓임 순서 | 설명 |
|---|---|---|
position: static | 가장 아래 | 기본값, z-index 적용 불가 |
position: relative/absolute/fixed | 위쪽 | z-index 적용 가능 |
float 요소 | 중간 | static보다 위, positioned보다 아래 |
/* 쌓임 순서 예시 */
.static-box { position: static; } /* 맨 아래 */
.float-box { float: left; } /* 중간 */
.positioned-box { position: relative; } /* 맨 위 */
쌓임 맥락은 특정 CSS 속성이 적용될 때 자동으로 생성됩니다. 이는 마치 새로운 방을 만드는 것과 같습니다.
/* 1. Position + Z-index 조합 */
.stacking-context-1 {
position: relative; /* 또는 absolute */
z-index: 1; /* auto가 아닌 값 */
}
/* 2. Fixed/Sticky Position */
.stacking-context-2 {
position: fixed; /* 또는 sticky */
/* z-index 없어도 쌓임 맥락 생성 */
}
/* 3. Flexbox/Grid 컨테이너 */
.stacking-context-3 {
display: flex; /* 또는 grid */
z-index: 1; /* z-index가 설정된 경우만 */
}
/* 4. 투명도 */
.stacking-context-4 {
opacity: 0.99; /* 1 미만의 값 */
}
/* 5. Transform 속성 */
.stacking-context-5 {
transform: translateZ(0); /* 모든 transform 값 */
}
/* 6. 필터 효과 */
.stacking-context-6 {
filter: blur(5px); /* 모든 filter 값 */
}
/* 7. CSS 마스크 */
.stacking-context-7 {
mask: url(mask.svg); /* 모든 mask 값 */
}
/* 8. 클립 패스 */
.stacking-context-8 {
clip-path: circle(50%); /* 모든 clip-path 값 */
}
/* 9. 믹스 블렌드 모드 */
.stacking-context-9 {
mix-blend-mode: multiply; /* normal이 아닌 값 */
}
/* 10. CSS 컨테인먼트 */
.stacking-context-10 {
contain: layout; /* layout 또는 paint 포함 */
}
// 쌓임 맥락 생성 여부를 확인하는 함수
function createsStackingContext(element) {
const style = getComputedStyle(element);
return (
// Position + z-index
(style.position !== 'static' && style.zIndex !== 'auto') ||
// Fixed/Sticky
['fixed', 'sticky'].includes(style.position) ||
// Opacity
parseFloat(style.opacity) < 1 ||
// Transform
style.transform !== 'none' ||
// Filter
style.filter !== 'none' ||
// 기타 여러 조건들...
);
}
<div class="parent-a">
<div class="child-high-z">높은 Z-Index (999)</div>
</div>
<div class="parent-b">
<div class="child-low-z">낮은 Z-Index (1)</div>
</div>
/* ❌ 문제가 되는 CSS */
.parent-a {
position: relative;
z-index: 1; /* 쌓임 맥락 생성 */
}
.parent-b {
position: relative;
z-index: 2; /* 더 높은 쌓임 맥락 */
}
.child-high-z {
position: absolute;
z-index: 999; /* 부모 맥락 내에서만 유효 */
}
.child-low-z {
position: absolute;
z-index: 1; /* 더 높은 부모 맥락 안에 있음 */
}
결과: child-low-z가 child-high-z보다 위에 표시됨!
왜 이런 일이 발생할까요?
최상위 쌓임 맥락
├── parent-a (z-index: 1)
│ └── child-high-z (z-index: 999) ← 부모 맥락 내에서만 유효
└── parent-b (z-index: 2) ← 더 높은 맥락
└── child-low-z (z-index: 1)
/* ✅ 해결책 1 */
.parent-a {
position: relative;
z-index: 3; /* parent-b보다 높게 설정 */
}
.parent-b {
position: relative;
z-index: 2;
}
/* ✅ 해결책 2 */
.parent-a {
position: relative;
/* z-index 제거 → 쌓임 맥락 생성하지 않음 */
}
.parent-b {
position: relative;
/* z-index 제거 */
}
.child-high-z {
position: absolute;
z-index: 999; /* 이제 최상위 맥락에서 비교됨 */
}
// React Portal 예시
function HighZIndexComponent() {
return createPortal(
<div className="modal">높은 우선순위 컴포넌트</div>,
document.body // 다른 쌓임 맥락으로 이동
);
}
<div class="header">
<div class="dropdown">드롭다운</div>
</div>
<div class="main-content">
<div class="modal">모달</div>
</div>
/* ❌ 문제가 되는 CSS */
.header {
position: relative;
z-index: 1000; /* 높은 z-index */
}
.modal {
position: fixed;
z-index: 999; /* 더 낮은 z-index지만 다른 맥락 */
}
/* ✅ Z-Index 계층 시스템 */
:root {
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
--z-toast: 1080;
}
.dropdown { z-index: var(--z-dropdown); }
.modal { z-index: var(--z-modal); }
.tooltip { z-index: var(--z-tooltip); }
// _z-index.scss
$z-indexes: (
'dropdown': 1000,
'sticky': 1020,
'fixed': 1030,
'modal-backdrop': 1040,
'modal': 1050,
'popover': 1060,
'tooltip': 1070,
'toast': 1080,
);
@function z($name) {
@return map-get($z-indexes, $name);
}
// 사용법
.modal {
z-index: z('modal'); // 1050
}
class ZIndexManager {
constructor() {
this.baseIndex = 1000;
this.currentMaxIndex = this.baseIndex;
}
getNextIndex(layer = 'default') {
const layerOffsets = {
'dropdown': 0,
'modal': 50,
'tooltip': 70,
'toast': 80
};
const offset = layerOffsets[layer] || 0;
return this.baseIndex + offset + (++this.currentMaxIndex);
}
// 사용법
applyModalIndex(element) {
element.style.zIndex = this.getNextIndex('modal');
}
}
const zManager = new ZIndexManager();
/* ❌ 의도하지 않은 쌓임 맥락 생성 */
.card {
transform: translateZ(0); /* GPU 가속을 위해 추가 */
/* → 의도치 않게 쌓임 맥락 생성! */
}
.card .tooltip {
position: absolute;
z-index: 9999; /* 카드 맥락 내에서만 유효 */
}
/* ✅ 더 나은 GPU 가속 방법 */
.card {
will-change: transform; /* 필요시에만 쌓임 맥락 생성 */
}
.card:hover {
transform: scale(1.05); /* 호버시에만 transform 적용 */
}
/* 애니메이션 완료 후 will-change 제거 */
.card.animation-complete {
will-change: auto;
}
// 쌓임 맥락 문제를 해결하는 Portal 컴포넌트
function Portal({ children, container = document.body }) {
return createPortal(children, container);
}
// 모달 컴포넌트
function Modal({ isOpen, children }) {
if (!isOpen) return null;
return (
<Portal>
<div className="modal-backdrop" style={{ zIndex: 1040 }}>
<div className="modal-content" style={{ zIndex: 1050 }}>
{children}
</div>
</div>
</Portal>
);
}
// 사용법
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div style={{ position: 'relative', zIndex: 999 }}>
{/* 높은 z-index를 가진 컨테이너 */}
<button onClick={() => setShowModal(true)}>모달 열기</button>
<Modal isOpen={showModal}>
<p>이 모달은 항상 최상위에 표시됩니다!</p>
</Modal>
</div>
);
}
<template>
<div class="app">
<div class="high-z-container" style="z-index: 999;">
<button @click="showModal = true">모달 열기</button>
</div>
<!-- Teleport로 body에 직접 렌더링 -->
<Teleport to="body">
<div v-if="showModal" class="modal" style="z-index: 1050;">
<div class="modal-content">
<p>항상 최상위에 표시되는 모달</p>
<button @click="showModal = false">닫기</button>
</div>
</div>
</Teleport>
</div>
</template>
// 쌓임 맥락 생성 요소 찾기
function findStackingContexts() {
const elements = document.querySelectorAll('*');
const stackingContexts = [];
elements.forEach(el => {
const style = getComputedStyle(el);
if (
(style.position !== 'static' && style.zIndex !== 'auto') ||
['fixed', 'sticky'].includes(style.position) ||
parseFloat(style.opacity) < 1 ||
style.transform !== 'none' ||
style.filter !== 'none'
) {
stackingContexts.push({
element: el,
zIndex: style.zIndex,
position: style.position,
opacity: style.opacity,
transform: style.transform
});
}
});
return stackingContexts;
}
// 콘솔에서 실행
console.table(findStackingContexts());
/* 개발용: 모든 positioned 요소에 z-index 표시 */
[style*="position: relative"]::before,
[style*="position: absolute"]::before,
[style*="position: fixed"]::before {
content: "z:" attr(style);
position: absolute;
top: 0;
left: 0;
background: rgba(255, 0, 0, 0.8);
color: white;
padding: 2px 4px;
font-size: 10px;
pointer-events: none;
}
// 특정 요소의 쌓임 순서 계산
function calculateStackingOrder(element) {
const path = [];
let current = element;
while (current && current !== document.documentElement) {
const style = getComputedStyle(current);
const stackingContext = (
(style.position !== 'static' && style.zIndex !== 'auto') ||
['fixed', 'sticky'].includes(style.position) ||
parseFloat(style.opacity) < 1 ||
style.transform !== 'none'
);
path.unshift({
element: current.tagName + (current.className ? '.' + current.className : ''),
zIndex: style.zIndex,
position: style.position,
createsStackingContext: stackingContext
});
current = current.parentElement;
}
return path;
}
// 사용법
const element = document.querySelector('.problematic-element');
console.table(calculateStackingOrder(element));
/* ✅ 효율적인 GPU 레이어 생성 */
.optimized-animation {
/* 애니메이션 전에 미리 레이어 생성 */
will-change: transform;
/* 또는 transform: translateZ(0); */
}
.optimized-animation:hover {
transform: scale(1.1);
transition: transform 0.3s ease;
}
/* ❌ 과도한 레이어 생성 */
.every-element {
transform: translateZ(0); /* 모든 요소에 적용하면 메모리 낭비 */
}
// 메모리 효율적인 Z-Index 풀 관리
class ZIndexPool {
constructor() {
this.pools = {
dropdown: { base: 1000, current: 1000, max: 1010 },
modal: { base: 1050, current: 1050, max: 1060 },
tooltip: { base: 1070, current: 1070, max: 1080 }
};
}
allocate(type) {
const pool = this.pools[type];
if (pool.current >= pool.max) {
console.warn(`Z-Index pool for ${type} is exhausted`);
return pool.max;
}
return ++pool.current;
}
deallocate(type, index) {
const pool = this.pools[type];
if (index === pool.current) {
pool.current--;
}
}
}
/* ✅ 명확한 계층 구조 정의 */
:root {
--z-base: 0;
--z-dropdown: 1000;
--z-modal: 1050;
--z-tooltip: 1070;
}
/* ✅ 의미 있는 네이밍 */
.modal-backdrop { z-index: var(--z-modal); }
.tooltip { z-index: var(--z-tooltip); }
/* ✅ 필요한 경우에만 쌓임 맥락 생성 */
.card:hover {
transform: translateY(-4px); /* 호버시에만 */
}
/* ✅ 문서화된 Z-Index 사용 */
.header {
z-index: var(--z-dropdown); /* 드롭다운을 위한 레이어 */
}
/* ❌ 임의의 큰 숫자 사용 */
.element {
z-index: 999999; /* 의미 없는 큰 숫자 */
}
/* ❌ 불필요한 쌓임 맥락 생성 */
.every-div {
transform: translateZ(0); /* 모든 요소에 적용 */
}
/* ❌ 문서화되지 않은 Z-Index */
.mystery-element {
z-index: 47; /* 왜 47인지 알 수 없음 */
}
/* ❌ 중첩된 높은 Z-Index */
.parent { z-index: 1000; }
.child { z-index: 9999; } /* 부모 맥락 내에서만 유효 */
layer() 함수CSS의 미래에는 쌓임 맥락을 더 직관적으로 관리할 수 있는 방법들이 제안되고 있습니다.
/* 미래의 CSS (제안 단계) */
.modal {
z-index: layer(modal); /* 명시적 레이어 지정 */
}
.tooltip {
z-index: layer(tooltip); /* 더 높은 레이어 */
}
쌓임 맥락은 CSS에서 가장 복잡하면서도 중요한 개념 중 하나입니다. 올바른 이해 없이는 예측하기 어려운 렌더링 버그를 만들어낼 수 있습니다.
핵심 포인트 요약:
마치 건물을 지을 때 각 층의 높이와 구조를 미리 계획하는 것처럼, 웹 개발에서도 쌓임 순서를 체계적으로 관리하는 것이 중요합니다. 오늘 배운 내용을 바탕으로 더 이상 z-index: 999999로 문제를 해결하려 하지 마시고, 근본적인 원인을 파악하여 깔끔하게 해결하시기 바랍니다.