iOS 사파리에서 고정 헤더가 사라지거나 잘리는 이유
상단 고정 헤더는 거의 모든 사이트가 쓰는 패턴입니다. position: fixed 한 줄이면 끝나는 것처럼 보이고, 데스크톱에서는 실제로 그렇습니다. 그런데 iOS 사파리에서는 이 헤더가 스크롤 도중 통째로 사라지거나, 위쪽이 몇 픽셀 잘려 보이는 일이 생깁니다.
두 증상은 원인이 다릅니다. 그리고 첫 번째를 흔한 방법으로 고치면 두 번째가 따라옵니다. 저는 최근에 이 순서를 그대로 밟았습니다.
헤더가 사라지는 쪽
스크롤하다가 주소창이 접히거나 펴지는 순간 헤더가 없어집니다. 다시 스크롤하면 돌아오기도 하고, 그대로 안 보이기도 합니다.
원인은 대개 backdrop-filter입니다. 요즘 헤더는 반투명 배경에 블러를 얹는 경우가 많습니다.
.nav.scrolled {
background: var(--nav-bg);
backdrop-filter: saturate(180%) blur(14px);
}position: fixed와 backdrop-filter가 만나면 브라우저는 별도의 합성 레이어를 만듭니다. iOS 사파리는 주소창 높이가 바뀌면서 뷰포트가 리사이즈될 때 이 레이어를 다시 그리지 못하는 경우가 있습니다. 요소는 DOM에 그대로 있고 스타일도 정상인데 화면에만 안 나옵니다.
해결은 아쉽지만 단순합니다. iOS에서만 블러를 빼고 거의 불투명한 같은 색으로 채웁니다.
@supports (-webkit-touch-callout: none) {
.nav.scrolled {
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
[data-theme="light"] .nav.scrolled { background: rgba(255,255,255,0.96); }
[data-theme="dark"] .nav.scrolled { background: rgba(11,17,32,0.96); }
}@supports (-webkit-touch-callout: none)는 iOS WebKit만 걸러내는 관용적인 방법입니다. 블러가 아깝긴 한데 96% 불투명이면 육안 차이는 크지 않고, 헤더가 사라지는 것보다는 낫습니다.
같은 맥락에서 하나 더 있습니다. 모달을 열 때 헤더를 visibility: hidden으로 숨기는 코드가 있다면 그것도 의심해볼 만합니다. visibility를 껐다 켜는 사이에 사파리가 레이어를 복구하지 못하는 경우가 있습니다. 레이어를 유지한 채 뒤로 보내는 쪽이 안전합니다.
html[data-scroll-lock] .nav { z-index: 90; pointer-events: none; }헤더 위쪽이 잘리는 쪽
여기서 많은 사람이 택하는 다음 수순이 "fixed를 sticky로 바꾸기"입니다. 저도 그랬습니다. sticky는 문서 흐름 안에 있으니 합성 레이어 문제에서 자유로울 거라고 생각했습니다.
그런데 헤더를 히어로 위에 겹쳐 보이게 하려면 뒤 요소를 끌어올려야 합니다. 보통 이렇게 씁니다.
.nav {
position: sticky;
top: 0;
height: 72px;
margin-bottom: -72px; /* 다음 요소를 헤더 높이만큼 끌어올림 */
}이 조합이 사파리에서 헤더 상단을 잘라먹습니다.
WebKit은 스티키 요소가 움직일 수 있는 범위, 즉 스티키 제약 사각형을 마진 박스 기준으로 계산합니다. margin-bottom: -72px를 주면 헤더의 마진 박스 높이가 0이 됩니다. 움직일 범위를 그 박스로 잡다 보니 스크롤 중에 헤더가 위로 밀려 올라가고, 그만큼 상단이 잘려 보입니다. 크롬은 이 계산이 달라서 멀쩡합니다.
sticky를 유지하고 싶다면 음수 마진을 헤더가 아니라 다음 형제에 주면 됩니다.
/* 이렇게 말고 */
.nav { position: sticky; margin-bottom: -73px; }
/* 이렇게 */
.nav { position: sticky; }
.hero { margin-top: -73px; }레이아웃 결과는 같은데 스티키 박스의 마진을 건드리지 않습니다.
곁들여 나온 1px
height: 72px에 border-bottom: 1px를 같이 준 상태라면 한 번 확인해보시는 게 좋습니다. box-sizing: border-box 환경에서는 지정한 높이 안에 보더가 포함되므로 배경은 71px만 칠해집니다. 눈에 잘 안 띄지만 헤더 아래쪽에 1px 틈으로 나타납니다.
정리
저는 결국 fixed로 돌아왔습니다. 헤더 소실을 실제로 해결한 건 sticky 전환이 아니라 backdrop-filter 제거였는데, 두 변경을 한꺼번에 넣는 바람에 한동안 그걸 몰랐습니다. sticky는 문제를 고치지 않으면서 새 버그만 하나 추가한 셈이었습니다.
증상이 사라졌다고 원인을 고친 게 아닙니다. 여러 변경을 한 커밋에 몰아넣으면 어느 쪽이 효과가 있었는지 나중에 알 수 없게 됩니다.
같은 작업에서 겪은 다른 문제:
함께 읽기
- 모바일에서만 가로 스크롤이 생기는 이유모바일에서 화면을 옆으로 밀었더니 페이지가 밀리고 오른쪽에 빈 여백이 보인다면, 어떤 요소가 뷰포트보다 오른쪽으로 삐져나가 있다는 뜻입니다. 흔한 문제인데 범인을 찾기가 은근히 어렵습니다. 눈에 보이는 요소가 아닌 경우가 많기 때문입니다.
- 크롬에서는 멀쩡한데 아이폰에서만 흰 화면이 뜨는 이유개발하면서 가장 당황스러운 상황 중 하나가 내 브라우저에서는 되는데 다른 기기에서만 안 되는 경우입니다. 특히 화면이 아예 하얗게 나오면 어디서부터 봐야 할지 막막합니다. 콘솔도 못 보고 에러 메시지도 없습니다.
- iOS Safari 모바일 버그 3종 해결기: 헤더 잘림 · 흰 화면 · 가로 넘침같은 날 확인된 세 증상은 원인이 서로 달랐으며, 모두 해당 테스트 환경에서 데스크톱 Chrome과 다르게 나타난 WebKit 관련 동작이었습니다.
- Next.js 랜딩 페이지의 Lighthouse 점수가 폰트 하나로 해결되지 않는 이유성능 최적화에서 가장 당황스러운 순간은 개발 환경에서는 빠른데 배포 후 측정값만 크게 느릴 때입니다. 이번 랜딩 페이지가 그랬습니다. 개발 환경에서 관찰한 LCP는 약 1.68초였지만 PageSpeed Insights의 첫 측정은 5.5초까지 늘어났습니다. 이후 배포본을 다시 측정했을 때도 성능 점수 72점, LCP 4…
- Next.js 앱에서 클릭이 한 박자 느리게 느껴질 때 살펴본 것들서버가 멀면 응답이 느린 건 어쩔 수 없습니다. 제 경우 오리진 응답 지연에 CDN 우회 경로까지 겹쳐 페이지 응답에 1초 가까이 걸리는 상황이었고, 네트워크 쪽은 당장 손댈 수 없었습니다. 그래서 응답 속도 대신 "눌렀을 때 반응하는 속도"를 올리는 쪽으로 방향을 잡았습니다. 느린 것과 느리게 느껴지는 것은 생각보다 …