한 줄 요약position은 요소를 보통 흐름에 그대로 둘지(static), 제자리를 남긴 채 살짝 밀지(relative), 흐름에서 빼내 특정 기준에 박을지(absolute·fixed), 평소엔 흐르다 스크롤 중 한 지점에 달라붙을지(sticky)를 정한다. 막히는 곳은 늘 "무엇을 기준으로 움직이는가"인데, absolute는 가장 가까운 'position 지정 조상', fixed는 화면(뷰포트), sticky는 자기 부모 구역이 기준이다.

학습 목표#

  • static·relative·absolute·fixed·sticky가 각각 무엇을 기준으로 자리를 잡는지 설명할 수 있다.
  • relative는 자기 자리를 남기고, absolute는 자리를 비우는 차이를 안다.
  • absolute가 어느 조상을 기준으로 잡는지 그 규칙을 안다.
  • sticky의 작동 조건과, 안 먹을 때 무엇부터 의심할지 안다.
  • z-index가 왜 가끔 안 먹는지(stacking context)를 한 문장으로 말할 수 있다.

오늘의 비유 — 전시실 벽에 무언가를 거는 다섯 가지 방식#

전시실 하나를 떠올려 보자. 벽에는 안내문이 줄지어 붙어 있고, 바닥엔 이젤이 서 있고, 벽 한쪽엔 액자가 걸려 있고, 입구엔 시계와 안내판이 있다. position의 다섯 값은 이 다섯 가지를 '어디에, 무엇을 기준으로 거는가'와 똑 닮았다.

  • static(벽보) — 벽에 정해진 줄을 따라 차례차례 붙는 안내문. "위에서 몇 cm" 같은 좌표를 따로 주지 않는다. 그냥 순서대로 제자리.
  • relative(이젤) — 바닥의 제자리는 그대로 차지하면서, 받침을 조금 밀어 원래 위치에서 살짝 옆으로 옮긴 이젤. 원래 자리는 비지 않는다.
  • absolute(벽 액자) — 바닥 흐름에서 빠져나와(자리를 안 차지) 어느 벽의 한 지점에 못으로 박은 액자. 단, 어느 벽을 기준 삼을지가 정해져 있어야 한다.
  • fixed(붙박이 시계) — 전시실을 둘러보며 걸어 다녀도(스크롤) 늘 같은 화면 자리에 보이는 시계. 화면 자체에 고정된 셈이다.
  • sticky(움직이는 안내판) — 평소엔 제 줄을 따라 흐르다가, 스크롤하다 화면 맨 위에 닿으면 거기 딱 붙어 따라오는 안내판. 자기 구역을 벗어나면 다시 흐름으로 돌아간다.

오늘은 이 다섯을 하나씩, "무엇을 기준으로 자리를 잡는가"를 중심으로 본다.

핵심 개념#

먼저 큰 그림. 모든 요소는 기본적으로 보통 흐름(normal flow) 위에 놓인다. 위에서 아래로, 글은 줄을 따라 차곡차곡 쌓이는 그 흐름이다. position은 이 흐름에서 요소를 어떻게 다룰지 정하는 속성이고, 값에 따라 top·right·bottom·left(그리고 z-index)라는 좌표 손잡이가 켜진다.

static — 기본값, 보통 흐름 그대로#

모든 요소의 기본값은 static이다. 흐름이 놓아 주는 자리에 그대로 있고, top·left·z-index가 전혀 먹지 않는다. 좌표를 줘도 무시된다. 벽보가 줄을 따라 제자리에 붙는 것과 같다.

.poster {
  position: static;
  top: 40px; /* static에서는 무시된다 */
}

top: 40px은 아무 효과가 없다. 좌표로 위치를 옮기려면 먼저 positionstatic이 아닌 값으로 바꿔야 한다.

relative — 제자리를 남기고 살짝 민다#

relative는 요소를 원래 있던 자리 기준으로 옮긴다. 핵심은 두 가지다. 첫째, top·left로 옮겨도 원래 자리는 그대로 남는다(다른 요소가 그 빈자리를 메우지 않는다). 둘째, 자식 absolute 요소의 기준 벽이 되어 준다(바로 아래에서 이어진다).

.easel {
  position: relative;
  top: 12px;
  left: 12px;
}

이젤을 원래 발자국에서 오른쪽 아래로 12px씩 민 셈이다. 바닥의 원래 자국은 그대로 남아 다른 물건이 그 자리에 들어오지 못한다.

absolute — 흐름에서 빼내 '기준 벽'에 박는다#

absolute는 요소를 보통 흐름에서 완전히 빼낸다. 자리를 차지하지 않으므로 뒤 요소가 그 빈자리를 메운다. 그리고 top·left로 정한 좌표는 가장 가까운, positionstatic이 아닌 조상을 기준으로 잡는다. 그런 조상이 하나도 없으면 페이지 전체(정확히는 최초 컨테이닝 블록)가 기준이 된다.

.gallery {
  position: relative; /* 이 박스가 기준 벽이 된다 */
}

.frame {
  position: absolute;
  top: 0;
  right: 0; /* .gallery의 오른쪽 위 모서리에 박힌다 */
}

.galleryposition: relative를 줬기 때문에, 그 안의 .frame.gallery의 오른쪽 위 모서리에 걸린다. 만약 .gallery에서 position을 빼면 액자는 기준 벽을 잃고 페이지 전체의 오른쪽 위로 날아가 버린다. 이게 아래 흔한 실수 2번의 정체다.

fixed — 화면(뷰포트)에 고정#

fixed도 흐름에서 빠지지만 기준이 다르다. 화면(뷰포트) 자체가 기준이라, 스크롤해도 같은 화면 위치에 머문다. 전시실을 걸어 다녀도 늘 같은 자리에 보이는 붙박이 시계처럼. 스크롤을 따라다니는 상단 배너나 화면 구석의 '맨 위로' 버튼이 대표적이다.

.clock {
  position: fixed;
  top: 16px;
  right: 16px;
}

sticky — 흐르다가 한 지점에서 달라붙는다#

sticky는 두 성격을 합친다. 평소엔 relative처럼 보통 흐름을 따라 움직이다가, 스크롤이 지정한 문턱(예: top: 0)에 닿는 순간 fixed처럼 그 자리에 달라붙는다. 단, 자기 부모 구역 안에서만 붙어 있고, 부모가 화면 밖으로 밀려나면 함께 사라진다.

.signboard {
  position: sticky;
  top: 0; /* 이 문턱 값이 반드시 있어야 한다 */
}

여기서 top: 0이 없으면 sticky는 붙을 지점을 몰라 그냥 흐름대로 흘러가 버린다. "sticky가 안 됨"의 첫 번째 원인이 보통 이 빠진 top이다(두 번째 원인은 흔한 실수 1번).

z-index — 누가 위에 보이는가#

요소가 겹칠 때 누가 위로 올라올지는 z-index가 정한다. 숫자가 클수록 앞이다. 단, z-indexpositionstatic이 아닐 때만 먹는다. 그리고 함정이 하나 있는데, positionz-index가 함께 걸린 요소는 **stacking context(쌓임 맥락)**라는 독립된 층을 새로 만든다. 이 층 안의 자식들은 아무리 큰 z-index를 줘도 층 자체의 순위를 넘지 못한다(흔한 실수 3번).

함께 따라하기 — 스크롤을 따라오는 sticky 헤더#

sticky가 가장 빛나는 자리인 '스크롤해도 따라오는 헤더'를 직접 만들어 본다.

<header class="topbar">전시 안내</header>
<main>
  <p>아주 긴 본문…</p>
  <!-- 스크롤이 생기도록 본문을 충분히 길게 둔다 -->
</main>
.topbar {
  position: sticky;
  top: 0;
  padding: 16px 24px;
  background: #1f2937;
  color: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}

main {
  min-height: 200vh; /* 스크롤이 생기도록 */
}

저장하고 스크롤해 보면, 전시 안내 헤더가 화면 맨 위에 닿는 순간 거기 딱 붙어 본문 위를 따라 내려온다. 아래에 옅은 그림자를 깔아 둬서 헤더와 본문의 층이 분리돼 보인다.

여기서 한 걸음 더 — "스크롤을 시작한 뒤에만 그림자가 생기게" 하고 싶을 때가 있다. 그건 '지금 스크롤됐는지'를 감지해야 하는 일이라, 보통 약간의 JavaScript(또는 최신 스크롤 기반 CSS)가 필요하다. 이 시리즈 범위를 넘으니, 지금은 그림자를 항상 켜 두는 것으로 충분하다.

흔한 실수 3가지#

1. sticky의 부모에 overflow:hidden을 줘서 작동을 막는다#

sticky 헤더를 분명히 맞게 짰는데 꼼짝도 안 하는 상황. 십중팔구 조상 어딘가에 overflow가 걸려 있다.

/* (이 .wrap 때문에 안쪽 sticky가 작동하지 않습니다) */
.wrap {
  overflow: hidden;
}

.topbar {
  position: sticky;
  top: 0;
}

부모(또는 더 윗 조상)에 overflow: hidden(또는 auto·scroll)이 걸리면, sticky는 그 조상을 스크롤 기준으로 잡으려다 엉켜서 붙지 못한다. 무심코 넣은 overflow: hidden 한 줄이 범인인 경우가 정말 많다.

.wrap {
  /* overflow를 빼거나, 정말 필요한 자식에만 따로 준다 */
}

해결은 단순하다 — sticky가 동작해야 하는 경로의 조상에서 불필요한 overflow를 걷어낸다. 그래도 안 되면 top 값이 빠지지 않았는지 다시 확인한다.

2. absolute의 기준 조상에 position을 안 줘 body 기준이 된다#

액자를 카드 안 모서리에 박으려 했는데 엉뚱하게 페이지 구석으로 날아가는 상황이다.

/* (.badge가 .card가 아니라 페이지 기준으로 잡힙니다) */
.card {
  /* position 없음 */
}

.badge {
  position: absolute;
  top: 8px;
  right: 8px;
}

.cardposition이 없으니, .badge는 기준으로 삼을 조상을 못 찾고 페이지 전체를 기준으로 잡는다. 그래서 카드 모서리가 아니라 화면 구석에 붙는다. 기준이 되려는 박스에 position: relative 한 줄만 주면 해결된다.

.card {
  position: relative; /* 이 박스를 기준 벽으로 지정 */
}

.badge {
  position: absolute;
  top: 8px;
  right: 8px;
}

relative는 자기 자리를 그대로 차지하므로 레이아웃을 흔들지 않으면서, 자식 absolute의 기준만 잡아 준다. "absolute를 쓰면 부모에 relative" — 이 한 쌍을 거의 공식처럼 외워 두면 편하다.

3. z-index가 stacking context를 만든다는 사실을 모른다#

z-index: 9999를 줬는데도 어떤 요소가 자꾸 다른 것 뒤에 가려지는 상황이다.

.panel {
  position: relative;
  z-index: 1; /* 여기서 독립된 층이 생긴다 */
}

.tooltip {
  position: absolute;
  z-index: 9999; /* .panel 밖으로는 못 나간다 */
}

.tooltip.panel 안에 있다면, .panel이 이미 z-index: 1로 자기만의 stacking context(쌓임 맥락)를 만든 상태다. 그 안의 .tooltip은 9999든 99999든 .panel이라는 층 내부에서만 순위를 다툰다. 바깥에 있는 z-index: 2짜리 형제 요소를 이길 수 없다. "분명 9999인데 왜 가려지지?"의 정답이 거의 항상 여기다. 해결은 둘 중 하나 — 부모의 z-index를 키우거나, .tooltip을 그 층 바깥으로 꺼낸다.

오늘 배운 것 체크리스트#

  • static은 보통 흐름 그대로이고 top·left가 안 먹는다.
  • relative는 자기 자리를 남긴 채 옮기고, absolute의 기준 벽이 된다.
  • absolute는 가장 가까운 'position 지정 조상'을, fixed는 화면을 기준으로 잡는다.
  • stickytop 같은 문턱 값과, overflow 없는 조상이 있어야 작동한다.
  • z-index는 stacking context 안에서만 순위를 다툰다.

자주 묻는 질문#

Q. position: fixedabsolute는 뭐가 다른가요?

A. 둘 다 보통 흐름에서 빠져 자리를 차지하지 않는 건 같지만, 기준이 다릅니다. absolute는 가장 가까운 'position이 지정된 조상'을 기준으로 좌표를 잡고 그 조상과 함께 스크롤됩니다. fixed는 화면(뷰포트)을 기준으로 잡아, 스크롤해도 같은 화면 위치에 그대로 머뭅니다. 스크롤을 따라다녀야 하면 fixed, 특정 박스 안 모서리에 붙여야 하면 absolute입니다.

Q. sticky가 안 먹어요. 무엇부터 봐야 하나요?

A. 작동 조건을 차례로 확인하세요. 첫째, top(또는 bottom 등) 문턱 값이 있는지 — 이게 없으면 붙을 지점을 몰라 그냥 흐릅니다. 둘째, 조상 어딘가에 overflow: hidden·auto·scroll이 걸려 있지 않은지 — 이게 sticky를 가장 자주 망칩니다. 셋째, 부모의 높이가 충분한지 — sticky는 자기 부모 구역 안에서만 붙어 있으므로, 부모가 내용 높이만큼만 짧으면 붙어 있을 거리가 없습니다.

Q. z-index를 아주 크게 줬는데도 가려져요. 왜 그런가요?

A. 그 요소의 조상이 이미 stacking context(쌓임 맥락)를 만들고 있기 때문입니다. positionz-index가 함께 걸린 조상은 독립된 층이 되고, 그 안의 자식은 아무리 큰 z-index를 줘도 층 바깥의 순위를 넘지 못합니다. 또한 z-indexpositionstatic이 아닐 때만 작동한다는 점도 함께 확인하세요.

다음 시간 예고#

내일은 float의 시대는 끝났다 — 그래도 알아야 하는 이유와 clearfix를 다룬다. 요즘은 거의 안 쓰지만 옛 코드에 깔려 있는 float의 원리와, 그게 무너뜨린 부모 높이를 되살리는 clearfix를 짚고, 같은 배치를 flex로 다시 만들어 비교한다.

더 알아보기#