포지셔닝 심화 — position 속성과 Stacking Context의 원리
분명
z-index: 9999를 줬는데, 왜 이 요소는 다른 요소 뒤에 숨어 있는 걸까?
포지셔닝 심화 — position 속성과 Stacking Context의 원리
CSS에서 요소를 원하는 위치에 배치하는 건 생각보다 까다롭습니다. position 속성 하나만 바꿔도 요소가 문서 흐름에서 빠지거나, 예상치 못한 곳에 자리를 잡기도 하죠. 특히 z-index가 왜 안 먹히는지 몰라서 삽질한 경험, 한 번쯤은 있지 않으신가요? 이 글에서는 position 5가지 속성의 동작 원리부터 Stacking Context까지 한 번에 정리해 보겠습니다.
1. position 속성 5가지
static — 기본값
모든 요소는 기본적으로 position: static입니다. 문서의 일반적인 흐름(normal flow)에 따라 배치되고, top, left 같은 오프셋 속성이 적용되지 않습니다.
.box {
position: static;
top: 20px; /* 무시됨 — static에서는 오프셋이 동작하지 않습니다 */
}
relative — 자기 자신 기준 이동
원래 위치를 기준으로 오프셋만큼 이동합니다. 핵심은 원래 자리가 보존된다 는 점입니다. 다른 요소들은 이 요소가 안 움직인 것처럼 배치됩니다.
.box {
position: relative;
top: 10px; /* 원래 위치에서 아래로 10px */
left: 20px; /* 원래 위치에서 오른쪽으로 20px */
}
주로 자기 자신을 이동시키기보다는, absolute 자식의 기준점을 만들기 위해 사용하는 경우가 많습니다.
absolute — positioned 조상 기준
문서 흐름에서 완전히 빠집니다. 가장 가까운 positioned 조상(position이 static이 아닌 조상)을 기준으로 배치됩니다.
.parent {
position: relative; /* absolute 자식의 기준점 역할 */
}
.child {
position: absolute;
top: 0;
right: 0; /* 부모의 오른쪽 상단에 배치 */
}
positioned 조상이 없으면 초기 containing block(보통 <html> 요소, 즉 뷰포트 크기)을 기준으로 합니다.
fixed — 뷰포트 기준
뷰포트를 기준으로 고정됩니다. 스크롤해도 위치가 변하지 않죠. 고정 헤더나 플로팅 버튼에 자주 사용합니다.
.header {
position: fixed;
top: 0;
left: 0;
width: 100%; /* 뷰포트 상단에 고정 */
}
주의할 점이 하나 있습니다. 조상 요소에
transform,filter,perspective속성이 있으면, fixed가 뷰포트가 아닌 그 조상을 기준으로 배치됩니다. 디버깅할 때 꽤 자주 겪는 문제입니다.
sticky — 스크롤 임계점 기준
relative와 fixed의 하이브리드입니다. 평소에는 일반 흐름에 따르다가, 스크롤이 지정한 임계점에 도달하면 고정됩니다.
.sidebar-title {
position: sticky;
top: 0; /* 뷰포트 상단에 닿으면 고정 */
}
2. top / right / bottom / left — 오프셋 동작
오프셋 속성은 position이 static이 아닌 요소에서만 동작합니다. 어떤 position 값이냐에 따라 기준이 달라집니다.
| position | 오프셋 기준 |
|---|---|
relative | 자기 자신의 원래 위치 |
absolute | containing block (positioned 조상) |
fixed | 뷰포트 |
sticky | 스크롤 임계점 |
top과 bottom을 동시에 지정하면? absolute 요소에서는 요소의 높이가 늘어납니다. relative에서는 top이 우선하고 bottom은 무시됩니다.
.stretch {
position: absolute;
top: 0;
bottom: 0; /* 부모의 높이 전체를 차지 */
}
3. Containing Block — absolute의 기준점
absolute 요소가 어디를 기준으로 배치되느냐는 containing block 이 결정합니다.
containing block을 형성하는 조건을 정리하면 이렇습니다.
position이relative,absolute,fixed,sticky인 요소transform,perspective,filter속성이none이 아닌 요소will-change가transform이나perspective인 요소contain이paint인 요소
<div class="grandparent" style="position: relative;">
<div class="parent">
<!-- parent는 position: static이므로 건너뜀 -->
<div class="child" style="position: absolute; top: 0;">
<!-- grandparent를 기준으로 배치 -->
</div>
</div>
</div>
공부하다 보니 이 부분에서 많이 헷갈렸습니다. absolute 요소의 부모가 기준이 아니라, 가장 가까운 positioned 조상 이 기준이라는 것을 확실히 기억해야 합니다.
4. sticky의 동작 원리와 주의점
sticky는 편리하지만 예상대로 동작하지 않는 경우가 꽤 있습니다.
반드시 임계값을 지정해야 합니다
top, bottom, left, right 중 하나 이상을 지정해야 동작합니다. 값이 없으면 브라우저가 언제 고정해야 하는지 알 수 없어서 static처럼 동작합니다.
overflow 속성의 영향
조상 요소에 overflow: hidden, overflow: auto, overflow: scroll이 있으면 sticky가 동작하지 않을 수 있습니다. sticky 요소는 가장 가까운 스크롤 가능한 조상 내에서만 고정되기 때문입니다.
/* sticky가 동작하지 않는 경우 */
.container {
overflow: hidden; /* 이 속성 때문에 sticky가 안 먹힙니다 */
}
.container .title {
position: sticky;
top: 0;
}
sticky의 고정 범위
sticky 요소는 부모 요소의 범위 안에서만 고정됩니다. 부모 요소가 뷰포트를 벗어나면 sticky 요소도 함께 사라집니다.
5. z-index와 Stacking Context
z-index는 요소의 쌓임 순서를 결정합니다. 숫자가 클수록 위에 표시되죠. 하지만 z-index만으로는 설명이 안 되는 상황이 있습니다.
Stacking Context란?
Stacking Context는 z축 방향의 독립적인 레이어 그룹입니다. 같은 stacking context 안의 요소들끼리만 z-index로 순서를 비교할 수 있습니다.
새로운 Stacking Context를 생성하는 조건
다음 중 하나라도 해당하면 새로운 stacking context가 만들어집니다.
position이relative/absolute/fixed/sticky이면서z-index가auto가 아닌 값position이fixed또는sticky(항상 새 context 생성)opacity가 1보다 작은 값transform이none이 아닌 값filter가none이 아닌 값perspective가none이 아닌 값isolation: isolatewill-change에 위 속성이 명시된 경우contain: layout또는contain: paint
Stacking Context 안의 쌓임 순서
하나의 stacking context 내에서 요소는 다음 순서로 쌓입니다 (아래부터 위로).
- 배경과 테두리 (stacking context를 형성하는 요소 자체)
- 음수
z-index를 가진 자식 - non-positioned, non-floated 블록 요소
- non-positioned float 요소
- non-positioned inline 요소
z-index: 0또는z-index: auto인 positioned 요소- 양수
z-index를 가진 자식
6. z-index가 안 먹히는 이유
이 부분이 가장 중요합니다. z-index: 9999를 줬는데 왜 안 되는 걸까요?
원인 1: position이 static
z-index는 positioned 요소(static이 아닌 요소)에서만 동작합니다.
/* z-index가 무시됩니다 */
.box {
z-index: 100; /* position이 static이라 적용 안 됨 */
}
/* 이렇게 해야 동작합니다 */
.box {
position: relative;
z-index: 100;
}
원인 2: 부모의 Stacking Context
부모가 낮은 z-index로 stacking context를 형성하면, 자식은 그 context 안에 갇힙니다.
.parent-a {
position: relative;
z-index: 1; /* 새로운 stacking context 생성 */
}
.parent-a .child {
position: relative;
z-index: 9999; /* 부모 context 밖으로 나갈 수 없음 */
}
.parent-b {
position: relative;
z-index: 2; /* parent-a보다 위에 표시됨 */
}
/* parent-a의 child(9999)는 parent-b(2) 아래에 표시됩니다 */
이건 비유하면 이렇습니다. stacking context는 봉투 같은 것이고, 봉투 안의 카드(z-index)를 아무리 높여도 봉투 자체의 순서를 바꿀 수는 없습니다.
원인 3: 의도치 않은 Stacking Context 생성
opacity: 0.99나 transform: translateZ(0) 같은 속성을 성능 최적화 목적으로 추가했는데, 이것이 새로운 stacking context를 만들어서 z-index 순서가 꼬이는 경우가 있습니다.
7. 실전 예제
드롭다운 메뉴
.nav-item {
position: relative; /* 드롭다운의 기준점 */
}
.dropdown {
position: absolute;
top: 100%; /* 네비게이션 아이템 바로 아래 */
left: 0;
z-index: 100; /* 다른 콘텐츠 위에 표시 */
display: none; /* 기본 숨김 */
}
.nav-item:hover .dropdown {
display: block; /* 호버 시 표시 */
}
고정 헤더
.header {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 1000; /* 대부분의 콘텐츠보다 위에 */
}
.main-content {
margin-top: 60px; /* 헤더 높이만큼 여백 확보 */
}
fixed 헤더를 사용하면 그 아래 콘텐츠가 헤더에 가려질 수 있습니다.
margin-top이나padding-top으로 여백을 확보하는 것을 잊지 마세요.
모달 오버레이
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5); /* 반투명 배경 */
z-index: 9000;
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
/* 오버레이 안에 있으므로 별도 z-index 불필요 */
background: white;
padding: 2rem;
border-radius: 8px;
max-width: 500px;
width: 90%;
}
모달은 isolation: isolate를 사용해 독립적인 stacking context를 만들어 관리하면 z-index 충돌을 줄일 수 있습니다.
.app {
isolation: isolate; /* 앱 전체를 하나의 stacking context로 */
}
정리
| 속성 | 기준 | 문서 흐름 | 스크롤 |
|---|---|---|---|
static | 일반 흐름 | 유지 | 따라감 |
relative | 자기 원래 위치 | 유지 | 따라감 |
absolute | positioned 조상 | 이탈 | 따라감 |
fixed | 뷰포트 | 이탈 | 고정 |
sticky | 스크롤 임계점 | 유지 | 조건부 고정 |
z-index가 안 먹히면 세 가지를 체크하세요.
position이static이 아닌지- 부모가 낮은
z-index의 stacking context를 만들고 있지는 않은지 opacity,transform,filter같은 속성이 의도치 않게 stacking context를 생성하고 있지는 않은지
Stacking context 개념만 제대로 이해하면 z-index 디버깅 시간을 크게 줄일 수 있습니다.