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는 문제를 고치지 않으면서 새 버그만 하나 추가한 셈이었습니다.

증상이 사라졌다고 원인을 고친 게 아닙니다. 여러 변경을 한 커밋에 몰아넣으면 어느 쪽이 효과가 있었는지 나중에 알 수 없게 됩니다.

같은 작업에서 겪은 다른 문제:

크롬에서는 멀쩡한데 아이폰에서만 흰 화면이 뜨는 이유

모바일에서만 가로 스크롤이 생기는 이유