CSS position 심화 | 포함 블록, 쌓임 맥락, 레이아웃 영향
이 글의 핵심
position은 좌표를 붙이는 선언이지만, 그 좌표가 어떤 사각형을 기준으로 하는지(포함 블록), 누가 위에 그려지는지(쌓임 맥락)까지 한 세트로 이해해야 합니다.
들어가며: HTML·CSS 시리즈 #04
이 글은 CSS 기초와 박스 모델에 이어, position 속성을 포함 블록(containing block), 쌓임 맥락(stacking context), 일반 문서 흐름(normal flow)과 묶어서 정리한 심화 편이다. top / right / bottom / left는 “좌표를 몇 px 이동”보다, 기준이 되는 사각형이 어디인지를 먼저 정한 뒤 읽는 편이 실무에서 오해가 적다.
아래는 본문에서 사용하는 용어를 짧게 맞춘다.
- 일반 흐름(normal flow):
static·relative가 따르는 희석되지 않은 문서의 배치 규칙(블록·인라인·플로팅은 별도). - 포지셔닝 박스(positioned box):
position이static이 아닌 요소. - 초기 포함 블록(initial containing block): 뷰포트(연속 미디어)에 대응하는 최상위 기준 사각형(개념적).
1. position 개요 — CSS 레이아웃 모델에서의 위치
CSS 레이아웃은 큰 틀에서 “흐름에 둘지, 흐름 밖으로 뺄지, 스크롤에 반응시킬지”를 position으로 제어하는 경우가 많다. Flexbox·Grid는 자식의 배치 방식을 정하고, position은 그 흐름 안·밖과 좌표계에 대한 힌트를 더한다. 두 계층을 섞을 때 z-index·overflow·transform이 교차하면서, “코드는 맞는데 화면만 이상하다”는 현상이 자주 난다.
1.1 값과 역할(한눈에)
| 값 | 일반 흐름 | 오프셋(top 등) | 기준(개요) |
|---|---|---|---|
static | 유지 | 무시 | 좌표 개념 없음(초기값) |
relative | 유지(자리는 그대로) | 자기 원래 위치에서 시각적 이동 | 레이아웃이 차지하는 공간은 이동 전과 동일 |
absolute | 이탈 | 포함 블록 기준 | 가장 가까운 “포지셔닝 조상” 등 |
fixed | 이탈 | 보통 뷰포트 | 특정 조건에서 조상 좌표계에 묶임 |
sticky | 유지하다가 스티키 | 스크롤 박스·임계값에 따라 | relative와 fixed 사이의 하이브리드 |
position은 명세 CSS Positioned Layout Module에 정의돼 있으며, 브라우저는 렌더링 파이프라인의 레이아웃 단계에서 “박스의 기준”과 “겹침 순서”를 이 규칙에 맞춰 계산한다.
2. static — 기본 흐름과 문서 순서
static은 포지셔닝되지 않은 기본값이다. 이 상태에서는 top / right / bottom / left·z-index가 효과가 없다(또는 명세에 따라 적용 대상이 아니다). 박스는 문서 순서·부모의 블록/인라인 포매팅에 따라 쌓인다.
실무에서의 인상: “아무것도 안 건드린 요소”가 static이다. 레이아웃을 absolute로 풀기 전, 먼저 부모-자식 높이·마진 병합이 예상과 같은지 확인하면 디버깅이 빨라진다. static인 형제 둘 사이의 세로 마진이 병합되지만, absolute로 빠진 자식은 그 계산에 기여하지 않는 식의 차이를 이 시리즈 박스 모델과 함께 읽는 것이 좋다.
<!-- static: 문서 순서가 곧 겹침(동일 z-index)의 기반 -->
<article>
<p class="a">A 문단</p>
<p class="b">B 문단</p>
</article>
/* 오프셋을 줘도 static에서는 무시되는 것이 정상 */
.a {
position: static;
top: 100px; /* 효과 없음 */
}
3. relative — 원래 위치를 기준으로 한 이동
relative는 일반 흐름에 남는다. 다만 top / right / bottom / left를 주면, 박스는 자기가 원래 있었을 자리(원래의 자리에 해당하는 점/사각형)를 기준으로 시각적으로 이동한다. 레이아웃이 예약한 공간(대개 원래의 공간)은 그대로라서, 주변 콘텐츠는 “이동 후의 실제 윤곽”이 아니라 이동 전을 기준으로 배치된다.
.item {
position: relative;
top: 8px; /* '원래 있던 곳'에서 위로(일반적 관례) */
left: 12px;
}
3.1 상세 동작(왜 “겹쳐 보이나”)
relative로 옮긴 요소는 흐름의 슬롯은 그대로 두고, 그림(페인트)만 옮긴다고 이해할 수 있다. 그래서 아래에 있던 인접 박스가 relative 요소의 이동 뒤 시각적 영역과 겹쳐 보이는 것이 흔하다. “간격이 왜 이상하지?”는 질문의 상당수가 이 원래 자리·시각적 자리의 괴리에서 출발한다.
3.2 포함 블록으로서의 relative
position: relative는 오프셋이 0이든 아니든, 자기 자손에 대해 “포지셔닝된 조상”이 될 수 있다. absolute 자식을 부모 영역 안에 맞추고 싶다면, 부모에 position: relative를 주는 앵커링(anchoring) 패턴이 널리 쓰인다(아래 [9절](#9-포함 블록-containing-block—기준-사각형-정리)과 연결).
4. absolute — 가장 가까운 positioned 조상 기준
absolute는 일반 흐름에서 제거된다(부모의 자동 높이 계산에 직접 기여하지 않음). top / right / bottom / left는 포함 블록에 대해 오프셋을 지정하며, 그 포함 블록의 후보는 대략 “가장 가까운 position: static이 아닌 조상” 쪽(명세 § Containing block for 절)으로 찾는다. 조상이 없다면 초기 포함 블록에 가깝게 잡힌다(연속 미디어에서 뷰포트에 대응하는 루트의 개념).
<div class="card u-anchor">
<div class="badge u-abs">New</div>
<h2>제목</h2>
<p>본문</p>
</div>
.u-anchor { position: relative; } /* absolute 자식의 '기준' 후보 */
.u-abs {
position: absolute;
top: 10px;
right: 10px;
}
4.1 absolute + 크기(퍼센트)
width / height에 퍼센트가 오면, 그 기준은 “임의의 부모”가 아니라 포함 블록이다. absolute의 포함 블록이 어디인지 틀리면 50%가 화면 절반이 아니라 엉뚱한 박스의 절반이 될 수 있다. 이는 12절의 반응형 설계에서도 핵심이다.
5. fixed — 뷰포트 기준과 고정 헤더/푸터
fixed는 일반 흐름에서 벗어나 대부분 뷰포트에 고정된 것처럼 보인다. 고정 헤더·하단 CTA·떠다니는 채팅 버튼에 자주 쓰인다.
.site-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
/* background, border 등으로 본문과 대비 */
}
/* 본문이 헤더 뒤로 깔리지 않도록 패딩 보정(대표 패턴) */
body {
padding-top: 56px;
}
.site-footer-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 90;
}
5.1 transform·filter 등에 “끌려가는” fixed (중요)
통상 fixed는 뷰포트 기준이지만, 조상에 transform, filter, perspective, will-change: transform 등이 걸리면, 구현·명세 흐름에 따라 fixed가 그 조상의 좌표계(스택/콘텐핑) 안에 묶이는 사례가 있다. “모달 안의 fixed 헤더가 뷰가 아니라 모달 박스에 붙는다”는 증상은 이 조합을 의심한다. 해결은 (1) transform을 모달 바깥으로 빼기, (2) 포털(React createPortal 등)로 DOM을 루트에 두기, (3) position: fixed 대신 absolute + 스크롤 래퍼 맞추기 등 케이스별로 선택한다.
6. sticky — 스크롤 임계값과 실전 활용
sticky는 relative와 fixed의 절충에 가깝다. 먼저 relative처럼 흐름에 남다가, 스크롤이 일정 임계(예: top: 0)에 도달하면 fixed에 가깝게 “붙는” 동작을 한다(정확한 수학은 [CSS Position 3]의 sticky 제약). 섹션의 소제목 바, 가로·세로의 테이블 헤더, 영역 안에서만 도는 Sticky nav에 잘 맞는다.
.toc {
position: sticky;
top: 64px; /* 위쪽 고정 헤더가 있다면 그 높이만큼 */
}
자주 막는 조건:
- 조상에
overflow: hidden/auto/scroll이 있으면, 그 조상이 스크롤 박스가 되어 기대한 “전체 페이지 기준” sticky가 아닐 수 있다. sticky요소의 감싸는 부모가 스크롤로 끝까지 가지 못하거나(높이 부족) 끝을 만나면, sticky도 함께 “빠진다”.
sticky는 FAQ의 네 번째 답(오버플로, 높이, 테이블 셀 맥락)을 함께 읽는 것이 좋다.
7. top, right, bottom, left — 오프셋의 의미
네 속성은 포함 블록의 해당 가장자리로부터, 박스의 대응하는 마진 가장자리까지의 거리(대개) 를 지정한다(명세의 정의에 따름). 동시에 top과 bottom을 지정하거나, left와 right를 지정하면, width/height가 auto일 때 끌어 늘려지는(stretched) 효과가 날 수 있다(명세/레이아웃 모드에 의존).
| 속성 | 읽는 방향(연속 미디어, 일반적) | 메모 |
|---|---|---|
top | 포함 블록 위 가장자리 → 요소 | 음수면 위로 “넘어감” |
right | 포함 블록 오른쪽 → 요소 | RTL 환경에서 UI 설계와 함께 고려 |
bottom | 포함 블록 아래 → 요소 | 하단 바·토스트 |
left | 포함 블록 왼쪽 → 요소 | 사이드바·도킹 UI |
inset: 0는 네 면 동시 지정(현대 CSS 축약)으로, absolute를 가용 영역 꽉 채우기에 쓰기 좋다.
.cover {
position: absolute;
inset: 0; /* top/right/bottom/left: 0 과 동일 */
}
static이 아닌 relative·absolute·fixed·sticky는 오프셋을 해석할 수 있지만(명세), 실무에서 가장 흔한 실수는 static에 오프셋을 줬다가 “왜 안 움직이지?”로 이어지는 사례다.
8. z-index — 쌓임 맥락(Stacking Context)과 겹침
z-index는 전역 z 순서가 아니다. 부모가 연 쌓임 맥락(Stacking context) 내부의 depth 정렬로 이해하는 것이 안전하다. 부모 맥락의 z-index가 앞서 있지(kid가 아무리 9999여도) 부모-조부의 맥락에서 밀리면, 모달·드롭다운이 헤더/배경 아래에 깔리는 “유명한” 현상이 난다.
8.1 새 쌓임 맥락(대표 조건, 비완전 목록)
position이static이 아님 +z-index가auto가 아님opacity가 1 미만transform/filter/perspective/isolation: isolate등(명세 § ‘Stacking the element’ 참고)- Flex/Grid 자식 +
z-index조합(명세)
8.2 예: 같은 부모, 형제끼리
<div class="stack">
<div class="a">A (아래 느낌)</div>
<div class="b">B (위 느낌)</div>
</div>
.stack { position: relative; }
.a, .b { position: absolute; left: 0; top: 0; }
.a { z-index: 1; }
.b { z-index: 2; } /* A 위에 */
stack 바깥에 또 맥락이 있고 그쪽 z-index가 낮다면, b를 아무리 키워도 다른 맥락의 형제를 ‘뒤집는’ 효과는 기대할 수 없다(그 경우는 15절 참고).
9. 포함 블록(Containing block) — 기준 사각형 정리
[CSS2.1/Position 3]에서 containing block은 속성(박스 모델, 퍼센트, absolute·fixed 오프셋)마다 조금씩 정의가 다를 수 있어, “항상 부모 콘텐츠 박스”로 통일할 수는 없다. 대신 실무 루브릭:
absolute는 가장 가까운 positioned 조상(대개static이 아닌relative/absolute/fixed/sticky) 를 포함 블록 후보로 쓰는 흐름이 많다(명세: positioned ancestor의 패딩 박스 등).fixed는 뷰포트에 대응하는 초기 포함 블록에 가깝되, 5.1절의transform등 특이 조상이 있으면 그 안이 고정의 기준이 될 수 있다.relative는 이동 전 자리를 공간에 남기므로, 절대 자식의 “앵커”로 쓰기 좋다.
시리즈 #04 박스 모델에서 콘텐츠/패딩/보더 박스의 차이를 익혀 두면, “왜 이 좌표가 패딩 안쪽/바깥으로 보이지?”에 대한 감이 빨리 잡힌다.
10. 실전 패턴
10.1 모달/팝업(오버레이 + 패널)
fixed + 반투명 오버레이 + 중앙 정렬이 전형적이다(스크롤 잠그기는 body에 overflow: hidden 등은 접근성·모바일과 함께 검토).
<div class="modal" hidden>
<div class="modal__backdrop" data-close></div>
<div class="modal__panel" role="dialog" aria-modal="true">
<h2>제목</h2>
<p>내용</p>
</div>
</div>
.modal {
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
padding: 16px; /* 뷰포트 가장자리와 간격 */
}
.modal__backdrop {
position: absolute; /* .modal(=containing block) 꽉 채움 */
inset: 0;
background: rgba(0, 0, 0, 0.45);
}
.modal__panel {
position: relative; /* z-index, 포커스 링, 내부 absolute 기준 */
z-index: 1;
max-width: min(720px, 100%);
max-height: min(80vh, 100%);
overflow: auto;
background: #fff;
border-radius: 12px;
padding: 20px;
}
10.2 툴팁(호버/포커스, 작은 풍선)
absolute + bottom: 100%/left: 50% + translate(-50%) 조합이 흔하다. pointer-events: none으로 겹침을 피하거나, 충분한 z-index와 쌓임 맥락을 염두에 둔다.
.tooltip { position: relative; display: inline-block; }
.tooltip__label {
position: absolute;
left: 50%;
bottom: 100%;
transform: translate(-50%, -6px);
white-space: nowrap;
z-index: 5;
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease;
}
.tooltip:hover .tooltip__label,
.tooltip:focus-within .tooltip__label { opacity: 1; }
10.3 드롭다운 메뉴
트리거에 position: relative를 주고, 메뉴는 absolute; top: 100%; left: 0; min-width: ...로 아래로 늘인다. overflow: hidden인 헤더 안에 붙이면 잘리므로(overflow 절), 필요하면 메뉴를 body 포털로 빼서 fixed + 좌표 계산(JS)으로 처리한다(대용량·모바일·키보드).
.menu { position: relative; }
.menu__list {
position: absolute;
top: 100%;
left: 0;
z-index: 20;
min-width: 200px;
background: #fff;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
10.4 Sticky 헤더/사이드바
- Sticky 헤더는 5절
fixed와 경쟁. 스크롤 구간에서만 강조하고 싶다면sticky가, 항상 보이게 하면fixed. - 사이드바는
position: sticky; top: 0;+align-self: start가 그리드/플렉스에선 자주 쓰인다(부모align-items: stretch의 함정 방지). 부모overflow를 점검할 것(6절).
10.5 Fixed 네비게이션(모바일 하단 탭)
fixed; bottom: 0; + safe-area-inset을 함께 쓰면 iOS 홈 인디케이터와 겹침이 줄어든다. 본문에 padding-bottom으로 빈 공간을 확보한다.
.nav-bar {
position: fixed;
left: 0; right: 0; bottom: 0;
z-index: 200;
padding-bottom: max(8px, env(safe-area-inset-bottom, 0px));
background: #fff;
border-top: 1px solid #e5e5e5;
}
11. 레이아웃 조합 — position + Flexbox/Grid
Flex/Grid는 배치 알고리즘이고, position: absolute 자식은, 그 flex/grid 아이템으로서 배치될 수 있지만(명세) 별도의 position 규칙(포함 블록)이 같이 돈다. 실무 루브릭:
- Grid 셀 안에
absolute자식을 “셀 꽉 채움”에 쓰는 패턴(오버레이, 배지)은, 부모(셀)에position: relative를 주고 자식inset: 0로 일치시킨다. - Sticky 사이드바는
grid-template-columns: 240px 1fr;+ 오른쪽 열min-width: 0;(오버플로)와 같이 Flexbox/Grid 시리즈의 제약(최소 폭)과 함께 본다.
.layout {
display: grid;
grid-template-columns: 260px 1fr;
gap: 24px;
align-items: start;
}
.sidebar { position: sticky; top: 12px; }
.main { min-width: 0; } /* flex/grid child overflow */
12. 반응형 position — 미디어 쿼리와 조합
작은 뷰포트에선 fixed 헤더를 sticky로 완화하거나, bottom: 0 내비를 끄고 top 햄버거로 전환하는 식이 흔하다. position 자체는 대개 그대로 두고, 높이/패딩/가시성을 미디어 쿼리로 토글한다.
@media (max-width: 640px) {
.site-header {
position: sticky; /* 모바일에선 '전역 고정' 부담 감소 */
top: 0;
}
body { padding-top: 0; } /* fixed 전용 본문 보정 제거 */
}
env(safe-area-inset-*)는 notch/홈 대응에 유용. dvh/svh 등 뷰포트 단위는 모바일 주소창 출입에 따른 가시 높이 변화를 줄이는 데 쓰인다(주제: 반응형).
13. 성능 고려 — reflow(레이아웃) / repaint(페인트)
브라우저는 스타일 변경 시 Layout → Paint → Composite(엔진 별로 단계명은 다를 수 있음)로 처리한다. top / left 직접 애니메이션은 layout을 자주 일으킬 수 있고, transform / opacity는 합성 단계에 맡기는 편이 스무스하다.
| 변경 | 러닝(대략) | 권장 |
|---|---|---|
left / top / width / height / font-size | 레이아웃·리페인트 | 가능하면 피하거나, 드문 레이아웃으로 |
transform / opacity | 합성 위주(일반적) | 이동·페이드에 우선 |
z-index | 맥락/레이어에 따라 다름 | 불필요한 맥락·큰 z-index 남용 자제 |
filter / clip-path (무거운 경우) | 비용↑ | 케이스별 프로파일 |
will-change: transform은 잘 쓰면 이득, 남발하면 메모리·맥락 부작용(5.1절의 fixed 끌림)이 생긴다. 꼭 필요한 요소에 한시적으로 제한하라.
14. 브라우저 호환성 — Sticky 위주
position: sticky는 최신 데스크톱·모바일 대부분에서 실사용이 가능하다(구형 IE는 지원 불가). 그래도:
overflow: hidden조상·높이 0 부모·테이블 셀 등 레이아웃 맥락에서 “안 붙는” 것이 버그가 아니라 조건인 경우가 많다.- Safari는 prefix
-webkit-sticky가 과거엔 쓰였고, 현대 프로젝트는 대개 표준sticky로 충분(팀의 타겟 범위에 따라 Can I use 확인).
fixed는 iOS 과거 position: fixed + 키보드/주소창 조합의 이슈가 잘 논의됐다(현재도 기기/버전·뷰포트 단위와 함께 QA 권장).
15. 일반적 문제 — z-index가 “안 먹는” 이유, 부모-자식
- 비교군이 다름:
z-index는 같은 쌓임 맥락의 형제(또는 맥락 규칙이 허용한 범위)끼리. 부모-삼촌이 다른 맥락이면 자식9999는 삼촌의 1에도 못 이긴다(부모의 맥락 먼저). position: static+z-index:z-index는static에서 쌓임 맥락 생성 조건이 다르다(맥락은 여전히 다른 이유로 생긴다).relative+z-index로 명시하는 편이 직관적.- 새 맥락을 만드는
opacity/transform/filter: 의도치 않은 최소 맥락이 생겨 순서가 뒤집힌 것처럼 보인다. - 부모
overflow: hidden:absolute메뉴/툴팁이 잘려 “z는 맞는데 안 보인다”로 착각.
절차: (1) DevTools로 쌓임 맥락 추적(아래 16절) (2) 막는 조상 overflow / transform / filter (3) 포털로 루트에 모달/메뉴 (4) isolation: isolate로 맥락 의도 명시(감팀·디자인 시스템).
16. 디버깅 팁 — DevTools 활용
- Elements(요소):
position/z-index/offset/inset을 Live로 바꿔 포함 블록이 바뀌는지 확인. - Computed(계산됨):
position의 최종 값(상속/미디어쿼리) 추적.z-index: auto인지. - Layers(레이어): Chrome 등에서 합성 레이어/겹침 시각화(버전/플래그에 따라).
- Layout: Grid/Flex 오버레이로 앵커 셀·트랙 확인.
- 3D view / stacking(제공 시):
z겹침 직관 확인(실험 기능일 수 있음).
작은 절차: “위에 오게”가 목표면, 먼저 부모-조부까지 z-index·transform·opacity를 아래→위로 훑는다. 전역 9999 대신 계층 설계(디자인 토큰): z-header, z-dropdown, z-modal 같은 명명이 유지보수에 유리하다.
17. Best Practices — 체크 테이블
| 주제 | 권장 | 지양(또는 주의) |
|---|---|---|
absolute 앵커 | 조상 position: relative 등 명시 | 암묵적 body앵커(대형 페이지) |
| 쌓임 | z 레벨 체계 + 최소 transform/opacity | 큰 z-index·맥락 남발 |
| Sticky | 스크롤 부모·overflow·top | 테이블/알 수 없는 overflow |
fixed | 뷰포트 고정 UI | transform 조상과의 끌림 미확인 |
| 애니메이션 | transform / opacity | left/top 고빈도 트윈 |
| 반응형 | 미디어쿼리로 모드 전환 | 모바일에서 fixed 남용 |
| 접근성 | 모달 포커스 트랩, 스크롤 락 검토 | 키보드로 도달 불가 |
| iOS/안드 | dvh/env(safe-area...) | 안전 영역 무시 |
실무 한 줄: 포지션을 바꾸면, 함께 읽을 것은 포함 블록·쌓임 맥락·overflow 세 가지다.
17.1 inset·논리 속성(Logical properties) — 다국어·RTL
top / right / bottom / left는 물리 방향(위·오·아·왼)이다. 다국어·RTL(우→좌) UI에서는 inline/block 축으로 쓰는 논리 속성이 읽기 쉽다.
| 물리 | 논리(요약) | 비고 |
|---|---|---|
left / right | inset-inline-start / inset-inline-end | LTR/RTL에서 시작·끝이 뒤바뀜 |
top / bottom | inset-block-start / inset-block-end | 수직 쓰기 모드에서 축이 달라질 수 있음 |
inset: 0 | inset-inline: 0; inset-block: 0; (개념적) | 축약은 상황에 맞게 |
position: absolute + 논리 inset는 국제화(i18n) 팀·디자인 시스템에서 점차 표준이 되고 있으며, “왼쪽 고정=항상 LTR” 가정이 깨지는 아랍어·히브리어 레이아웃에서 실수를 줄인다. 물리 속성이 틀린 것은 아니다. 제품이 RTL을 지원한다면, 트리거·드롭다운·사이드 패널의 left: 0 일괄이 아니라, start/end나 transform + logical로 검토할 가치가 있다.
17.2 Sticky — 스크롤 박스(scrollport) 개념도
sticky는 “뷰포트에 붙는다”는 감이 있지만, 실제는 가장 가까운 스크롤을 만드는 조상의 스크롤 박스에 대해 top / left 임계를 만든다(명세: sticky positioning). overflow: auto 부모가 있으면 그 박스 안에서만 붙는다.
flowchart TB
subgraph V["뷰포트(또는 루트 스크롤)"]
subgraph S["section (overflow: visible)"]
subgraph P[".panel (overflow: auto ← 스크롤 박스)"]
C[".sticky (position: sticky; top:0)"]
T["(긴 콘텐츠)"]
end
end
end
- 뷰 스크롤만 쓰는 문서:
.panel이 없다면(대개) 루트 스크롤이 스크롤 박스로 인식돼, 기대한 “페이지 Sticky”에 가깝다. - 카드/패널 안만 스크롤: Sticky는 그 패널의 상단(또는
top에 맞춘 축)에 “붙는” 것이 정상이다.
부모 높이가 Sticky 요소보다 짧으면(부모에 남는 스크롤 거리가 없다면), Sticky는 끝까지 가기 전에 같이 스크롤되어 사라질 수 있다(“잠깐 붙다가” 사라짐). 이는 버그가 아니라, 끝(클램프) 조건에 가깝다.
<div class="section">
<h3 class="section__head">이 섹션의 제목</h3>
<div class="section__body">…</div>
</div>
.section { /* 충분한 세로 스크롤 “거리”를 확보 */ }
.section__head {
position: sticky;
top: 0;
background: #fff; /* 뒤 콘텐츠와 겹칠 때 읽힘 */
z-index: 1;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
17.3 페인팅(painting) 순서와 z-index — 7·8단 맥락(개념)
z-index는 “큰 수가 위”가 전제이지만, 쌓임 맥락이 트리처럼 겹쳐 있으면 맥락끼리 먼저 맞는다. CSS 2.1의 Appendix E의 끌어올리기(Appendix E)·[CSS Position 3]의 “layer” 설명이 근거이다. 실무에선 맥락을 “상자”로 생각한다.
- 맥락은 부모-자식이 아니라, 같은 스택 안 형제끼리
z를 비교한다(단,auto/레이어 규칙이 있음). position+z-index로 맥락이 생기면, 그 안의 자손z는 다른 맥락과 섞이지 않는다(비유: 여권 구역).
(보다 뿌리에 가까운) 맥락 A
├─ z=0: 헤더(맥락 내부 자식)
└─ (자식 A1의 z=10000은) …동일 맥락 내에서만 최상위
(형제) 맥락 B (z=1)
├─ 본문
└─ (맥락 A의 A1=10000은) … B 전체(맥락)보다 "아래"일 수 있음(부모 맥락 z가 정함)
17.4 Clip·Mask·contain 과 겹침(보충)
clip-path, mask, overflow: clip 등 클리핑은 가시/히트(클릭) 영역에 영향을 주며, Sticky/absolute가 “보이지 않는” 이유로 z와 혼동되기 쉽다(특히 overflow: hidden 헤더). DevTools Layers·3D 뷰로 “잘렸다”는 사실이 먼저인지, “z에 졌다”는 사실이 먼저인지 분리하라. content-visibility·contain: paint는 최적화와 페인트 경계에 영향을 주므로, 툴팁/드롭다운이 갑자기 잘리면 containment도 의심한다.
17.5 접근성 — position과 키보드·리더 모달, 드롭다운, 툴팁
- 드롭다운:
absolute/fixed+aria-expanded,aria-controls, 포커스가 리스트로 이동하는지(로밴) 확인. - 모달:
role="dialog",aria-modal="true", 포커스 트랩,Esc로 닫기(팀 합의), 스크롤 락은 스크롤이 튀는 iOS/안드 QA. - 툴팁:
pointer-events: none은 가리기에 좋지만, 롱-프레스·스크린 리더 대안(aria-describedby/ 별도 접근)을 디자인과 합의.
<div class="menu">
<button class="menu__btn" type="button" aria-expanded="false" aria-controls="m1" id="m1-b">
메뉴
</button>
<ul class="menu__list" id="m1" role="menu" hidden>
<li role="menuitem"><a href="/a">항목 A</a></li>
<li role="menuitem"><a href="/b">항목 B</a></li>
</ul>
</div>
.menu { position: relative; }
.menu__list[hidden] { display: none; }
.menu__list {
position: absolute;
top: 100%;
left: 0;
z-index: 30;
min-width: 220px;
background: #fff;
}
(열고 닫는 토글·포커스 전진·aria-expanded 갱신은 JS가 맡는다 — 본문 범위를 넘어가므로 구조만 맞춘다.)
17.6 인쇄(@media print) — fixed / sticky
fixed 요소(헤더·광고·내비)는 인쇄에서 모든 페이지에 반복되거나(구현·브라우저), 쓸모없이 낭비될 수 있다. @media print에서 .no-print로 display: none, Sticky/Fixed를 해제하거나 static으로 되돌리는 팀이 많다.
@media print {
.site-header, .nav-bar, .ad-slot {
position: static !important; /* 필요 시 none 대신 */
}
body { padding-top: 0 !important; }
}
17.7 그리드·플렉스에서 absolute 오버레이 — 풀 예제(카드)
그리드 셀·플렉스 아이템을 채우는 absolute 자손은, 부모(셀)에 position: relative를 둬 포함 블록을 고정한다. 비율은 aspect-ratio + object-fit이 자주 쓰인다.
<article class="card">
<a class="card__link" href="/p/1">본문은 링크</a>
<img class="card__img" src="hero.webp" alt="" width="800" height="500" />
<span class="card__badge">BEST</span>
</article>
.card {
position: relative;
display: block;
border-radius: 12px;
overflow: hidden;
aspect-ratio: 16 / 9;
background: #f3f3f3;
}
.card__img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card__link {
position: absolute; inset: 0; z-index: 1;
/* 콘텐츠 위 클릭: 텍스트는 시각적 숨김 또는 별도 요소 */
font-size: 0;
}
.card__link::after { content: ""; position: absolute; inset: 0; }
.card__badge {
position: absolute; top: 10px; right: 10px; z-index: 2;
background: #111; color: #fff; font-size: 12px; padding: 4px 8px; border-radius: 999px;
}
overflow: hidden+border-radius:absolute배지는 둥근 모서리 안에 잘린다(의도). 반대로 드롭다운을 같은 헤더에 두면 잘림이 된다(10.3절).- 링크가
inset: 0이면, 이미지·배지 위z를 쪼개지 않으면 배지 클릭이 링크로 가는지 QA한다.
17.8 성능(보충) — requestAnimationFrame + 레이아웃 스로틀
scroll / resize 리스너에서 top / left을 자주 바꾸면(팝오버 follow), layout thrashing(읽기·쓰기 교차)이 난다. 따라다니는 fixed UI는, 가능하면 transform: translate3d로 합성하거나, Popper·Floating UI로 뷰포트 클램프를 쓰고, 스크롤 루프는 rAF로 묶는다(프레임 단위 1회).
// 개념: rAF로 위치 갱신을 한 프레임에 합친다(실제는 라이브러리 권장)
let raf = 0;
function onScroll() {
if (raf) return;
raf = requestAnimationFrame(() => {
raf = 0;
// next: getBoundingClientRect() 한 번, 그 다음 스타일 한 번(가능한 한)
});
}
window.addEventListener("scroll", onScroll, { passive: true });
17.9 브라우저 지원(발췌) — Sticky / inset / dvh
| 기능 | 대략적 지원(2026년 전후, 팀의 타겟에 따라 Can I use 확인) |
|---|---|
position: sticky (표준) | 모던 브라우저 광범위; IE 없음 |
inset | Chromium/Safari/Firefox 현행 |
dvh / svh / lvh | 모바일 뷰포트 이슈 완화에 유효; 반응형과 병행 |
-webkit-sticky | 과거 Safari; 신규는 표준 sticky + 폴리필(필요 시) |
position: -webkit-sticky는 레거시 코드베이스·내부 툴에서만 우선 고려한다. 신규는 표준+테스트.
17.10 문제·트리플 체크 — “안 보임 / 안 붙음 / 잘림”
| 증상 | 1차 의심 | 2차 의심 |
|---|---|---|
| Sticky 안 붙음 | overflow 조상 | 부모 높이·display: table-cell 맥락 |
| fixed가 뷰가 아님 | transform 조상 | filter / perspective / will-change |
z 최댄데 아래 | 다른 쌓임 맥락 | 부모 z / opacity / isolation |
| 메뉴 하단 잘림 | overflow: hidden | 뷰포트에 포털·fixed |
| 인쇄에 광고만 반복 | @media print 누락 | 벤더별 끄기 |
17.11 DevTools(보강) — “왜 잘렸는지 / 왜 뒤에 있는지”
- Emulation: 모바일·RTL·접힌 주소창(뷰포트)에서
fixed/dvh/safe-area동시 검증. - Scroll: Sticky 스크롤 박스를 찾을 때, Elements에서
overflow아닌visible조상을 거슬러 올라 첫auto/scroll을 찾는다(그 박스가 스크롤 박스). - Performance(프로파일):
Layout시간이 크다면, 애니메이션이left/top인지 확인(13절). - Accessibility:
aria-*/ contrast / tab order — 위치는 맞는데 포커스가 엉뚱한 곳(모달)으로 가는지.
18. position 값별 동작(원문 요약 테이블)
| 값 | 일반 흐름 | 오프셋(top 등) | 기준(개요) |
|---|---|---|---|
static | 유지 | 무시 | 초기값. 좌표 개념 없음 |
relative | 유지(자리는 그대로) | 자기 원래 위치에서 이동 | 시각적만 이동, 레이아웃이 차지하는 공간은 이동 전과 동일 |
absolute | 이탈 | 포함 블록 기준 | 가장 가까운 포지셔닝 조상 등 |
fixed | 이탈 | 보통 뷰포트 | 단, 특정 조건에서 조상에 묶임 |
sticky | 유지하다가 스티키 | 스크롤 컨테이너·임계값에 따라 | relative와 fixed 사이의 하이브리드 |
19. 일반 흐름 이탈과 높이/스크롤(보충)
absolute / fixed는 문서 흐름에서 제거되므로, 부모의 자동 높이가 “떠 있는” 자식만으로는 늘지 않는다가 기본. 세로 스크롤 영역·빈 래퍼·겹침 문제의 상당수가 이 규칙에서 출발한다. 해결책은 (1) 흐름에 플레이스홀더 둔다, (2) 부모에 고정/최소 높이를 준다, (3) Flex/Grid로 칸을 만든 뒤 그 칸을 기준으로 absolute를 겹친다.
19.1 마진 병합과 position (보충)
박스 모델에서 다루듯, 인접 블록의 세로 마진은 병합될 수 있다. 포지셔닝된 요소(relative / absolute / fixed / sticky)는, 맥락에 따라 병합이나 BFC와의 상호작용이 달라진다(명세: float·absolute·etc.). 실무 루브릭:
absolute/fixed: 흐름에서 빠지므로, 형제 블록과 세로 마진 병합에 끼지 않는다고 보면 대략 맞다(“왜 margin이 붙는 것처럼 보이지?”의 원인이 흐름이 아닌 다른 요소일 수 있음).relative: 흐름에 남으므로, 이동 전 슬롯을 기준으로 병합 논의가 가능하다(시각은 옮겼지만, 공간·병합은 다른 규칙).- 부모-첫/마지막 자식 병합(부모-자식 collapse):
overflow·padding·border·BFC 트릭(예:display: flow-root)이 섞이면 예상과 다를 수 있다. “absolute자식만 있는 부모 높이 0”은 19절 본문이 설명한 것과 합쳐 읽는다.
19.2 Float(플로트)와 absolute (역사·실무)
float는 원래 문단 옆에 그림을 두기 위한 메커니즘이다. absolute는 흐름을 빼앗는다. 둘 다 옛날 2·3칼 레이아웃에 쓰였지만, 현대는 Flex/Grid가 주류다. 여전히 CMS·WYSIWYG HTML에 float: left가 남아 있을 수 있고, 새 레이아웃에 absolute 오버레이를 씌울 때 float BFC와 엉킴이 생긴다면, 콘텐츠 루트에 ::after{clear: both}(클리어픽스) 대신 플렉스/그리드로 담는 편이 낫다(유지보수).
/* 레거시 래퍼를 건드리지 않을 때(임시) */
.legacy-float-root::after { content: ""; display: table; clear: both; }
19.3 @supports로 Sticky 폴백(선택)
sticky를 광범위하게 쓰는 환경이면(내부 레거시 WebView), 없을 때는 relative+top 시각만, 있을 때 sticky로 다르게 줄 수 있다(팀·타겟에 따라).
.section__head--sticky {
position: relative; /* 기본(폴백) */
}
@supports (position: sticky) {
.section__head--sticky { position: sticky; top: 0; }
}
19.4 relative + 음수 오프셋 + 겹침(태그, 배지)
top: -4px; left: 8px; 처럼 음수는 “원래 자리”에서 밖으로 나가게 그린다(시각). 콘텐츠·다른 카드와 겹치면 z-index·맥락이 필요하다(8절). 접근성상, 최소 44px·명도 대비를 UI 합의와 맞출 것.
.tag {
position: relative;
top: -2px; /* "카드 밖"으로 약간 튀기기(시각) */
display: inline-block;
margin-left: 4px;
z-index: 1; /* 겹침: 형제/부모 맥락 확인 */
}
19.5 테이블 내부 sticky / position
<table>·display: table·캡션·콜그룹은 렌더링이 특수해서, Sticky·absolute의 동작이 div 레이아웃과 다를 수 있다(명세/브라우저 버전). 가능하면 table 위에 div 래퍼로 감싸 Sticky/오버레이를 쓰거나, CSS Tables 이슈·caniuse로 지원을 확인한다. 핀(pin) 컬럼/로우는 position: sticky(가로·세로 축) 혹은 가상화·라이브러리+ARIA 데이터 그리드 패턴을 쓰는 팀이 많다.
19.6 position: revert / revert-layer (빠른 메모)
캐스케이드 레이어(@layer)나 써드파티 CSS와 섞일 때, 초기화·의도 복원에 revert·revert-layer를 쓰는 경우가 있다(주제: CSS 기초·명세 CSS Cascade). position: revert는 UA → 사용자 → 작성자 순으로 “원래” 값이 돌아간다(단순 unset과 다름). 디자인 시스템 오버라이드 경계에서 쓸 수 있다.
20. relative·absolute·fixed·sticky의 포함 블록(다시)
absolute의 포함 블록은 대략: 조상 중static아닌 것의 패딩 박스(흐름) 등. 없으면 초기 포함 블록에 가깝다.fixed는 주로 뷰포트, 단 5.1절 참고.relative는 이동해도 원래 공간을 남기며,absolute자식의 앵커가 될 수 있다.
21. z-index — 그래서 자주 생기는 문제(재정리)
- 모달이 헤더 아래에 깔임: 서로 다른 쌓임 맥락 + 부모
z가 낮음. z-index: 9999인데 무시: 비교군이 형제/같은 맥락 아님.
디버깅은 DOM을 올라가며 opacity / transform / isolation / z-index: auto 여부로 “맥락이 새로 열렸는지”를 추적한다(8절).
22. 실전 체크리스트(빠른 복습)
absolute를 쓴다 → 포함 블록을 만들 조상에position: relative등을 명시했는가.z-index가 안 먹는다 → 같은 쌓임 맥락인지, 부모 맥락z가 막고 있지 않은지.fixed가 이상하다 → 조상transform/filter/will-change없는지.sticky가 안 붙는다 → 중간overflow/ 부모 높이 / 테이블 맥락 없는지.- 높이가 0 → 흐름에서 빠진 자식만 없는지.
23. position + 성능(간단 요약)
fixed 헤더·sticky는 합성 이점이 있을 수 있으나, 과도한 z-index·will-change·불필요한 레이어는 메모리를 쓴다(13절). 스크롤·드래그에선 transform: translate3d(0,0,0) 류의 핵은 절제—쓰면 5.1절 fixed 끌림 부작용을 재확인한다.
24. 정리
- 포함 블록은 좌표·퍼센트·오프셋의 기준이며,
absolute/fixed는 일반 블록과 정의가 다름. z-index는 쌓임 맥락 내부의 순서이며,opacity·transform·isolation등과 한 세트로 읽어야 한다.- 흐름 이탈은 부모 높이·마진 병합·스크롤 영역을 함께 바꾼다(박스 모델 글 병행).
이어 Flexbox·CSS Grid에서 아이템의 z-index·position 상호작용을 이어가면, 컴포넌트 단위로 레이아웃·겹침·스크롤을 한 번에 설계하기 쉬워진다.
25. 참고·용어(이 글에서의 사용)
| 용어 | 짧은 정의(이 글 맥락) |
|---|---|
| 초기 포함 블록 | 연속 미디어에서, 루트에 대응하는 최상위 기준(뷰포트 개념과 연결) |
| 스크롤 박스(scrollport) | overflow: auto/scroll 등으로 스크롤이 생기는 박스의 가시 영역 |
| 포지셔닝(positioned) | static 아닌 position |
| 쌓임 맥락 | z-index·투명도·transform 등으로 한 겹 겹침 순서가 새로 정해지는 맥락 |
| 합성(Composite) | 페인트된 텍스처를 GPU 등으로 겹쳐 표시하는 단계(엔진별 명칭 상이) |
바깥으로 더 읽을 명세·문서:
- CSS Positioned Layout Module Level 3 —
position, 포함 블록, sticky 수식 - CSS 2.1 — Layered presentation — 겹침(Appendix E)의 역사적 기준
- CSS Box Model / Display —
display·박스 종류와position의 교차
25.1 “어떤 값을 켤까?” 빠른 선택(실무 루브릭)
| 목적 | 먼저 떠올릴 값 | 곁들일 것 |
|---|---|---|
| 문서 순서만, 좌표 불필요 | static | (기본) |
| 살짝 밀기, 흐름 유지 | relative | 음수 오프셋·z-index(맥락) |
| 부모 칸 안에 딱 붙이기(배지, 오버레이) | absolute + 조상 relative | inset·퍼센트(포함 블록) |
| 뷰포트에 항상 | fixed | env(safe-area-*)·dvh·5.1 transform |
| 스크롤 구간에서만 “붙는” 느낌 | sticky | 스크롤 박스·top·부모 높이 |
| 맨 앞에 겹침(모달·드롭다운) | (위) + z-index | 쌓임 맥락·isolation·8·15절 |
이 표는 절대 규칙이 아니라, 팀에서 UI 키트를 정리할 때 문서에 한 장 붙여 두기 좋은 초안이다.
25.2 엔진 관점 한 문장(용어로만)
레이아웃 엔진은 스타일·DOM에서 “이 박스는 흐름에 있는가, 좌표만 바뀌는가, 떨어지는가”를 position으로 분기한 뒤, 스크롤·클리핑·페인팅(레이어) 파이프라인에 넣는다. 그래서 같은 width: 100%라도 퍼센트의 기준이 포함 블록에 따라 달라지는(9절) 현상이 나온다.
- 흐름에 남는 박스는 다음 형제·부모 높이·쌓임 맥락에 주로 기여한다.
- 흐름에서 떨어지는 박스(
absolute/fixed)는 형제의 자리를 빼앗지 않는 대가로, 부모의 자동 높이에도 끼지 않는다(19절). - 둘을 섞는 UI(카드+배지)는 9·10·11절의 “앵커, 오버레이, z”를 한 묶음으로 읽으면 실수가 줄어든다.
이 절의 세 줄은 위 긴 본문을 한 화면에 압축한 메모다. (시리즈 #04 position 핵심 복기에 쓰기 좋다.)
명세·엔진의 모든 분기를 외울 필요는 없고, “흐름/좌표/떨어짐”으로 먼저 보면 나머지는 표와 절차(17·22절)에 자연스럽게 수렴한다.
팀 온보딩용으로, 이 글 19절을 하루, 1017절을 하루로 끊어 읽는 일정을 잡는 것도 한 방법이다. (독서 시간 readingMinutes는 frontmatter에 맞춰 두었고, 본문을 확장한 뒤에는 실제 체감 시간이 더 길 수 있다.)
26. 시리즈 #04 — 배운 뒤 바로 해볼 만한 실습(자가 점검)
아래는 필수 과제가 아니라, 이해를 손으로 확인하는 자가 점검 절차다.
- 최소 HTML로
header(고정)와 긴main을 만든 뒤header에만position: fixed를 적용한다. 스크롤할 때 본문 첫 줄이 헤더 뒤에 가리는지 본다.body에padding-top을 얼마로 두면 겹침이 사라지는지 수를 잡는다(5절). - 같은 DOM에서
header내부 래퍼에transform: translateZ(0)(또는will-change: transform)을 준 뒤, 그 안에position: fixed버튼을 두고 뷰포트 고정이 깨지는지 확인한다(5.1절). overflow: auto인 카드 안에position: sticky제목을 두고, 카드만 스크롤할 때와 전체 페이지를 스크롤할 때 동작이 어떻게 달라지는지 비교한다(6·17.2절).- 형제 두 박스에
z-index를 다르게 준 뒤, 부모에opacity: 0.99한 줄을 추가했을 때 앞뒤 순서가 뒤집히는지(쌓임 맥락) 관찰한다(8·15절). 끝나면opacity는 원래대로 되돌린다.
이 네 가지는 DevTools의 Elements, 스크롤, 가능하면 Layers 패널만으로 충분하다. 팀에 공유할 때는 스크린샷 한 장에 “포함 블록과 쌓임 맥락이 어떻게 달랐는지” 한 문장만 덧붙여도 복기에 도움이 된다.
관련 글
- CSS 박스 모델 | Margin, Padding, Border 완벽 정리
- CSS 기초 | 선택자, 속성, 색상, 폰트
- CSS Flexbox | 플렉스박스 레이아웃 완벽 가이드
- 반응형 웹 디자인 | 미디어 쿼리와 모바일 최적화
자주 묻는 질문 (FAQ) — 본문 보강
Q. 이 포스트는 어떤 순서로 읽으면 좋나요?
A. CSS 기초 → 박스 모델 → 이 글(position) → Flexbox 순이 자연스럽다. position은 Flex/Grid와 겹쳐 쓰일 때 읽는 시야가 넓어진다.
Q. z-index: 9999는 왜 “안 먹는” 것처럼 보이나요?
A. 전역이 아니라 부모 쌓임 맥락 안의 값이다. 8절·15절을 보며 조상의 z-index·transform·opacity를 함께 점검하라. 프런트엔드 시스템에선 z 토큰으로 단계를 관리하는 팀이 많다.
Q. Sticky는 어디에 쓰는 게 좋고, fixed와 어떻게 나누나요?
A. sticky는 스크롤 구간 안에서 “일정 위치”에 들러붙이기, fixed는 항상 화면의 한 축에 고정할 때(단, 5.1 transform 주의)다. 6·10.4·14절을 참고한다.
이 글에서 다루는 키워드 (검색·내부 링크)
CSS, position, static, relative, absolute, fixed, sticky, top/right/bottom/left, inset, z-index, stacking context, containing block, normal flow, overflow, transform, Flexbox, Grid, 미디어 쿼리, reflow, repaint, scrollport, safe-area 등.