- Published on
Safari에서 position - sticky 깨짐 원인·해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버/백엔드 이슈처럼 보이진 않지만, position: sticky 깨짐은 “조건이 하나라도 어긋나면 조용히 실패”한다는 점에서 운영 장애 디버깅과 닮았습니다. 특히 Safari(iOS 포함)는 스크롤 컨테이너 처리와 합성(compositing) 최적화가 다른 브라우저와 달라서, 크롬에서는 멀쩡한데 Safari에서만 스티키가 풀리거나 점프하는 케이스가 자주 나옵니다.
이 글에서는 Safari에서 position: sticky가 깨지는 대표 원인을 “증상 → 원인 → 해결” 순서로 정리하고, 마지막에 JS 폴백(IntersectionObserver) 까지 제공합니다. 디버깅 관점은 인프라 장애 분석처럼 “재현 조건을 최소화하고 체크리스트로 좁혀가는 방식”이 제일 빠릅니다. 비슷한 방식의 문제 해결 글로는 OpenAI Responses API 415 Unsupported Media Type 해결도 참고할 만합니다.
position: sticky의 동작 조건(핵심만)
position: sticky는 다음 조건을 만족해야 안정적으로 동작합니다.
- 스티키 요소에
top/bottom/left/right중 하나가 지정되어야 함 - 스티키 요소는 자신의 스크롤 컨테이너(가장 가까운 스크롤 가능한 조상) 기준으로 붙음
- 스티키 요소의 조상 중 일부 CSS 속성(특히
overflow,transform)이 스티키 기준/합성을 바꿔 동작을 깨뜨릴 수 있음 - 스티키는 부모의 박스 영역을 벗어나지 못함(부모 높이가 낮으면 “붙을 공간” 자체가 없음)
Safari에서 깨지는 대부분의 케이스는 2~4번에서 발생합니다.
증상별 원인·해결 체크리스트
1) Safari에서만 sticky가 아예 동작하지 않음 (그냥 static처럼 보임)
원인 A: top(또는 bottom 등) 미지정
스티키는 기준 오프셋이 없으면 동작하지 않습니다. 크롬에서 “우연히” 기대한 것처럼 보였더라도 Safari에서는 더 엄격하게 실패하는 사례가 있습니다.
.header {
position: sticky;
/* 필수 */
top: 0;
}
원인 B: 조상에 overflow: hidden/auto/scroll이 있어 스크롤 컨테이너가 바뀜
position: sticky는 가장 가까운 스크롤 컨테이너를 기준으로 동작합니다. Safari에서는 특히 overflow: hidden도 스티키 계산에 영향을 주는 경우가 있어, “스크롤은 body인데 기준은 중간 div”가 되어버리면 스티키가 기대와 다르게 보이거나 아예 안 붙습니다.
해결 방향
- 스티키 요소의 조상에서 불필요한
overflow를 제거 - 정말로 내부 스크롤 영역에 붙여야 한다면, 스티키 요소를 그 스크롤 컨테이너 내부로 이동
/* 나쁜 예: 상위 래퍼가 overflow를 잡아 sticky 기준이 바뀜 */
.page {
overflow: hidden; /* Safari에서 sticky 깨짐 트리거가 되기 쉬움 */
}
/* 좋은 예: overflow는 필요한 곳(실제 스크롤 컨테이너)에만 */
.scroll-area {
overflow-y: auto;
-webkit-overflow-scrolling: touch; /* iOS에서 관성 스크롤 */
}
.scroll-area .sticky {
position: sticky;
top: 0;
}
2) 스티키가 스크롤 중 “점프”하거나 떨림(jitter)이 발생
원인 A: 조상에 transform, filter, perspective가 있음
Safari는 transform: translateZ(0) 같은 합성 유도 속성이 들어가면, 스티키 레이아웃 계산이 어긋나거나 스크롤 중 떨리는 케이스가 있습니다.
특히 흔한 패턴
- 애니메이션 최적화하려고 부모에
transform: translate3d(0,0,0) - 모달/레이아웃 전환 때문에
transform으로 컨테이너를 이동
해결 방향
- 스티키의 조상에서
transform제거 또는 스티키를 transform 영향 밖으로 이동 - 불가피하다면 transform을 스티키 자신에게 적용하지 말고, 다른 래퍼로 분리
<div class="transformed">
<div class="sticky">I should stick</div>
<div class="content">...</div>
</div>
.transformed {
transform: translateZ(0); /* Safari에서 sticky jitter 유발 가능 */
}
/* 해결 1: transform 제거 */
/* .transformed { transform: none; } */
/* 해결 2: 구조 분리(스티키를 transform 밖으로) */
3) iOS Safari에서 주소창/툴바 접힘에 따라 sticky 위치가 어긋남
원인: vh 기반 높이 계산과 iOS의 동적 뷰포트
iOS Safari는 스크롤 시 주소창이 접히면서 실제 뷰포트 높이가 변동합니다. 이때 height: 100vh 같은 값이 레이아웃 기준을 흔들어 스티키가 밀리거나 “붙었다 떨어졌다” 할 수 있습니다.
해결 방향
- 가능하면
100vh대신100dvh(dynamic viewport) 사용(지원 브라우저 확인) - JS로
--vh커스텀 프로퍼티를 계산해 대체
// iOS 대응: 실제 innerHeight 기반으로 vh 변수 설정
function setVh() {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
window.addEventListener('resize', setVh);
setVh();
/* 100vh 대신 */
.full {
height: calc(var(--vh) * 100);
}
4) sticky가 부모 영역 끝에서 너무 빨리 멈추거나, 아예 붙을 “공간”이 없음
원인: 부모/조상 높이가 스티키보다 작거나 레이아웃이 display: flex에서 꼬임
스티키는 부모 박스 안에서만 움직입니다. Safari에서 특히 flex 레이아웃과 결합될 때 높이 계산이 달라 “부모가 생각보다 낮게 계산”되는 경우가 있습니다.
해결 방향
- 스티키의 부모에 명확한 높이/패딩/컨텐츠 플로우를 보장
- flex 컨테이너에서 스티키 아이템이 있다면
align-self: flex-start;를 주거나, 스티키를 flex 밖으로 빼기
.layout {
display: flex;
}
.sidebar {
/* Safari에서 flex 아이템 sticky가 흔들릴 때 완화 */
align-self: flex-start;
}
.sidebar .sticky {
position: sticky;
top: 16px;
}
5) overflow: auto 내부에서 sticky가 iOS에서만 무시됨
원인: iOS의 관성 스크롤(-webkit-overflow-scrolling: touch)과 조합 이슈
내부 스크롤 컨테이너에서 sticky를 쓰는 경우, iOS에서 관성 스크롤과 함께 동작이 불안정해질 수 있습니다.
해결 방향(우선순위 순)
- 내부 스크롤을 피하고 body 스크롤로 설계(가능하면 최선)
- 꼭 내부 스크롤이어야 하면, sticky 대신 고정 헤더(absolute + padding-top) 패턴 고려
- 최후에는 JS 폴백
.scroll-area {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* iOS에서 sticky가 불안정하면 아래 대체 패턴을 고려 */
Safari에서 빠르게 원인 찾는 디버깅 절차
Safari 전용 버그는 “한 번에 다 고치려다 더 망가지는” 경우가 많습니다. 운영에서 CrashLoopBackOff를 체크리스트로 좁혀가듯(예: K8s CrashLoopBackOff 원인별 진단·해결 체크리스트), 프론트도 최소 재현으로 접근하는 게 빠릅니다.
- 스티키 요소만 남기고 가장 단순한 HTML/CSS로 축소
- 조상 DOM을 위로 올라가며 아래 속성이 있는지 확인
overflow: hidden/auto/scrolltransform,filter,perspective,backdrop-filtercontain,will-change
- 레이아웃이 flex/grid라면 스티키 요소/부모의 높이 계산을 확인
- iOS라면
vh사용 여부 확인
Safari DevTools에서 Elements 탭으로 조상 스타일을 빠르게 훑고, 의심 속성을 하나씩 끄면서 재현이 사라지는 지점을 찾으면 됩니다.
실전 해결 패턴 3가지
패턴 1) “sticky는 body 스크롤 기준”으로 고정하기
가장 안정적인 해법은 sticky를 body 스크롤 기준으로 만들고, 중간 래퍼에서 overflow/transform을 없애는 것입니다.
<header class="topbar">Top</header>
<main class="page">
<section>...</section>
</main>
.topbar {
position: sticky;
top: 0;
z-index: 1000;
background: white;
border-bottom: 1px solid #eee;
}
/* page에는 overflow/transform을 가급적 두지 않는다 */
.page {
overflow: visible;
}
패턴 2) sticky를 포기하고 “고정 헤더 + 여백”으로 대체
내부 스크롤 컨테이너에서 sticky가 불안정하면, 헤더를 position: absolute로 두고 컨텐츠에 패딩을 주는 방식이 더 예측 가능합니다.
<div class="panel">
<div class="panel-header">Header</div>
<div class="panel-body">...</div>
</div>
.panel {
position: relative;
height: 400px;
overflow: auto;
}
.panel-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 48px;
background: #fff;
border-bottom: 1px solid #eee;
z-index: 1;
}
.panel-body {
padding-top: 48px; /* 헤더 높이만큼 */
}
패턴 3) JS 폴백(IntersectionObserver)로 sticky 흉내내기
CSS sticky가 깨지는 환경(iOS 특정 버전/특정 레이아웃)에서는 폴백이 필요할 수 있습니다. 아래는 sentinel을 두고, 헤더를 position: fixed로 전환하는 간단한 방식입니다.
<div class="wrap">
<div id="sentinel"></div>
<div id="sticky" class="sticky">Sticky</div>
<div class="content">...</div>
</div>
.sticky {
position: sticky;
top: 0;
background: white;
}
/* 폴백 상태 */
.sticky.is-fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
#sentinel {
height: 1px;
}
const sentinel = document.getElementById('sentinel');
const sticky = document.getElementById('sticky');
// sticky가 깨지는 Safari에서만 강제 적용하고 싶다면
// navigator.userAgent 기반 분기 대신 feature test/버전 조건을 권장하지만,
// 운영에서는 최소 리스크로 제한 적용하는 전략이 필요할 때가 많습니다.
const io = new IntersectionObserver(
([entry]) => {
// sentinel이 화면 위로 사라지면 고정
if (entry.boundingClientRect.top < 0 && entry.intersectionRatio === 0) {
sticky.classList.add('is-fixed');
} else {
sticky.classList.remove('is-fixed');
}
},
{ threshold: [0] }
);
io.observe(sentinel);
이 방식은 “정확히 sticky와 동일”하진 않습니다(부모 경계에서 멈추는 동작 등). 하지만 Safari에서 특정 화면만 깨지는 상황에서 긴급 패치로 유효합니다. 장애 대응처럼(예: OpenAI Responses API 408 타임아웃 재현과 해결 실전 가이드), 우선 사용자 영향도를 줄이고 이후 구조 개선을 하는 전략이 현실적입니다.
자주 묻는 함정(Q&A)
Q1. -webkit-sticky를 써야 하나요?
과거에는 position: -webkit-sticky;가 필요했지만, 최신 Safari에서는 대부분 position: sticky로 충분합니다. 다만 레거시 대응이 필요하면 아래처럼 병기할 수 있습니다.
.sticky {
position: -webkit-sticky;
position: sticky;
top: 0;
}
Q2. z-index를 올렸는데도 sticky가 뒤로 깔려요
스티키 자체의 z-index도 중요하지만, 조상에 transform 등이 있으면 새 스태킹 컨텍스트가 생겨 예상과 다르게 겹침이 결정됩니다. z-index 싸움으로 해결하기 전에 조상의 스태킹 컨텍스트를 의심하세요.
결론: Safari sticky는 “조상 CSS”를 먼저 의심하라
Safari에서 position: sticky가 깨질 때 가장 흔한 원인은 다음 3가지입니다.
- 조상
overflow로 스크롤 컨테이너가 바뀜(특히overflow: hidden포함) - 조상
transform/filter로 합성/스태킹 컨텍스트가 바뀜 - iOS에서
vh/내부 스크롤 컨테이너로 레이아웃 기준이 흔들림
해결은 대체로 “스티키를 body 스크롤 기준으로 단순화”하거나, 내부 스크롤이 필수라면 “고정 헤더 패턴/JS 폴백”으로 가는 것이 안정적입니다. 체크리스트로 조상 스타일을 하나씩 제거하며 최소 재현을 만드는 것이 가장 빠른 길입니다.