- Published on
Safari iOS 스크롤 잔상·깜빡임 해결 - Compositing 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버/백엔드 이슈처럼 보이지 않는데 iOS Safari에서만 스크롤 시 화면이 번쩍이거나(white flash), 텍스트/이미지가 잔상처럼 남았다가 사라지거나, fixed/sticky 헤더가 떨리는 현상을 겪는 경우가 있습니다. 대부분 렌더링 파이프라인(레이아웃 → 페인트 → 합성) 중, 특히 Compositing(레이어 합성) 단계에서 레이어 분리 기준이 애매해지거나, 스크롤과 페인트가 서로 발목을 잡으면서 발생합니다.
이 글은 “무작정 translateZ(0)를 박는다” 수준을 넘어서, 어떤 요소를 왜 합성 레이어로 올려야 하는지, 그리고 **iOS Safari에서 특히 위험한 조합(overflow, sticky, filter, backdrop-filter 등)**을 어떻게 정리해야 하는지에 초점을 맞춥니다.
> 성능/안정성 튜닝은 관찰 → 가설 → 제한된 변경 → 검증의 루프가 중요합니다. 웹에서도 마찬가지입니다. 장애 진단 루프가 익숙하다면, 예를 들어 systemd 서비스가 계속 재시작될 때 진단 체크리스트 같은 방식으로 “원인 후보를 좁히는” 접근을 그대로 가져오면 효과가 좋습니다.
iOS Safari에서 잔상·깜빡임이 생기는 대표 패턴
iOS Safari(WebKit)는 스크롤을 부드럽게 만들기 위해 스크롤을 별도 스레드/프로세스로 처리하려고 합니다. 그런데 아래 조건이 섞이면 스크롤 중에 페인트가 자주 발생하거나, 레이어가 스크롤 컨테이너와 함께 잘못 합성되면서 번쩍임/잔상이 생깁니다.
1) position: sticky/fixed + overflow 조합
- sticky가 들어있는 조상에
overflow: hidden/auto/scroll이 걸리면, Safari가 sticky를 스크롤 컨테이너 내부에서 “재페인트”로 처리하는 경우가 있습니다. - 특히 중첩 스크롤 컨테이너(body 스크롤 + 내부 div 스크롤)에서 흔합니다.
2) backdrop-filter, filter, mix-blend-mode, mask 등 고비용 효과
- 이들은 종종 별도 합성 레이어를 만들거나, 반대로 레이어 병합을 방해합니다.
- 스크롤 중 지속적으로 샘플링/블러가 다시 계산되면 white flash가 나기 쉽습니다.
3) 큰 이미지/비디오 + 동적 리사이즈/리플로우
- 스크롤 이벤트에서 DOM 크기를 바꾸거나(특히 height), 폰트 로딩/라인브레이크가 바뀌면 레이아웃이 흔들리고 페인트가 증가합니다.
4) -webkit-overflow-scrolling: touch의 함정
- iOS에서 관성 스크롤을 켜는 전통적인 속성인데, 특정 레이어(특히 fixed/sticky)와 섞이면 합성 경계가 꼬일 수 있습니다.
핵심 개념: “페인트를 줄이고, 합성 레이어 경계를 안정화”
해결의 방향은 대체로 둘 중 하나(또는 둘 다)입니다.
- 스크롤 중 페인트가 일어나지 않게 만든다.
- 레이아웃 변화를 없애고, 애니메이션/전환을
transform/opacity로 제한
- 레이아웃 변화를 없애고, 애니메이션/전환을
- 합성 레이어를 의도적으로 분리해 스크롤과 독립적으로 합성되게 만든다.
- sticky/fixed, 헤더, 오버레이, 카드 리스트 등 “스크롤 위에 떠있는 느낌”의 요소를 레이어 승격
다만 레이어를 무작정 늘리면 메모리/타일 캐시 비용이 증가합니다. 즉, 필요한 곳만 최소로 올리는 것이 중요합니다.
재현 가능한 최소 케이스 만들기 (디버깅의 출발점)
현상이 특정 페이지에서만 난다면, 먼저 “문제 구간만 남긴” 최소 HTML로 줄이세요. 아래는 sticky 헤더 + 내부 스크롤 + blur 배경이 있을 때 흔히 문제를 일으키는 구조입니다.
<div class="page">
<header class="topbar">Title</header>
<div class="scroller">
<div class="card">...</div>
<div class="card">...</div>
<!-- many cards -->
</div>
</div>
.page { height: 100vh; }
.topbar {
position: sticky;
top: 0;
backdrop-filter: blur(12px);
}
.scroller {
height: 100%;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
이 조합은 iOS Safari에서 합성 레이어/스냅샷 처리가 불안정해질 가능성이 높습니다.
진단: “어떤 요소가 페인트를 유발하는지”부터 확인
1) Safari Web Inspector에서 레이어/페인트 관찰
- macOS Safari → Develop 메뉴 → iPhone 연결 → 해당 페이지 Inspect
- Timelines/Rendering 관련 패널에서 페인트/레이아웃 스파이크를 봅니다.
실무 팁:
- 스크롤만 할 때도 Paint가 계속 튄다면, “스크롤이 합성만으로 처리되지 못하고 있다”는 신호입니다.
- 특정 요소(헤더/오버레이) 근처에서만 깜빡이면 그 요소를 우선 분리 대상으로 봅니다.
2) CSS/JS 변경을 단계적으로 꺼보기
backdrop-filter제거 →filter제거 →overflow구조 변경 → sticky 제거- 가장 먼저 사라지는 변경이 ‘원인 축’입니다.
이 과정은 HTTP 타임아웃/버퍼 설정을 하나씩 바꿔가며 원인을 좁히는 것과 유사합니다. (관찰-가설-검증 루프는 프론트/인프라 모두 동일합니다.)
해결 전략 1: sticky/fixed 요소를 “의도적으로” 합성 레이어로 승격
1) transform: translateZ(0) / will-change는 “정확히 필요한 요소에만”
sticky 헤더가 스크롤 중 깜빡이거나 잔상이 남는다면, 헤더를 별도 합성 레이어로 올려 스크롤과 독립적으로 합성되게 만듭니다.
.topbar {
position: sticky;
top: 0;
z-index: 10;
/* 레이어 승격: 둘 중 하나만 선택 */
transform: translateZ(0);
/* 또는 */
/* will-change: transform; */
/* iOS에서 텍스트/레이어 뒤집힘 방지 */
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
주의:
will-change는 “곧 변할 것”을 브라우저에 힌트로 주는 것이므로 남발하면 오히려 메모리/성능에 악영향입니다.translateZ(0)도 마찬가지로 레이어를 늘립니다. 헤더/툴바/오버레이처럼 ‘항상 위에 떠야 하는’ 요소에만 적용하세요.
2) contain으로 페인트/레이아웃 영향 범위 제한
카드 리스트나 섹션이 스크롤 중 자주 다시 그려지는 경우, contain으로 격리해 주변 레이아웃에 영향을 덜 주게 만들 수 있습니다.
.card {
contain: layout paint;
}
contain: paint는 페인트 범위를 잘라내어 합성 시 오염을 줄이는 데 도움이 됩니다.- 레이아웃이 외부에 영향을 줘야 하는 컴포넌트(예: 자동 높이로 부모를 늘려야 하는 구조)에는 부작용이 있을 수 있으니 테스트가 필요합니다.
해결 전략 2: backdrop-filter/filter가 원인이라면 “대체 렌더링”으로 우회
backdrop-filter는 디자인적으로 매력적이지만 iOS Safari에서 스크롤 시 깜빡임을 유발하는 단골입니다.
1) blur를 포기하지 않되, 스크롤 중에는 비활성화
스크롤 중에만 blur를 끄고, 스크롤이 멈추면 다시 켜는 방식입니다. UX 타협이지만 체감 안정성이 크게 좋아집니다.
let t;
const root = document.documentElement;
window.addEventListener('scroll', () => {
root.classList.add('is-scrolling');
clearTimeout(t);
t = setTimeout(() => root.classList.remove('is-scrolling'), 120);
}, { passive: true });
.topbar {
backdrop-filter: blur(12px);
}
.is-scrolling .topbar {
backdrop-filter: none;
}
2) blur를 이미지/그라데이션 오버레이로 대체
실시간 blur 대신 반투명 그라데이션/노이즈 텍스처를 얹어 유사한 느낌을 냅니다.
.topbar {
background: rgba(255,255,255,0.85);
}
.topbar::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(
to bottom,
rgba(255,255,255,0.95),
rgba(255,255,255,0.75)
);
}
해결 전략 3: 스크롤 컨테이너 구조를 단순화 (가능하면 body 스크롤로 회귀)
iOS Safari에서 가장 안정적인 스크롤은 여전히 문서(body) 스크롤입니다. 내부 overflow: auto 스크롤 영역은 제스처/합성/고정요소 처리에서 변수가 많습니다.
가능하면:
- 내부 스크롤 div를 없애고 body가 스크롤하도록 변경
- sticky는 body 스크롤 기준으로 동작하게 만들기
불가피하게 내부 스크롤을 써야 한다면:
- 스크롤 컨테이너에 과도한 효과(filter/backdrop) 금지
- fixed/sticky 요소는 스크롤 컨테이너 바깥으로 빼고 포털(Portal)로 띄우는 구조 고려
해결 전략 4: 애니메이션/전환은 transform/opacity로 제한
스크롤 중 또는 스크롤 직후에 UI가 반응하면서 top/left/height 등을 바꾸면 레이아웃과 페인트가 연쇄적으로 발생합니다. iOS Safari에서 특히 민감합니다.
/* 나쁜 예: height 변경은 레이아웃/페인트 유발 */
.bad {
transition: height 200ms;
}
/* 좋은 예: transform/opacity는 합성에서 처리될 가능성이 큼 */
.good {
transition: transform 200ms, opacity 200ms;
will-change: transform, opacity;
}
실전 체크리스트: “이 조합이면 의심”
아래 항목이 2개 이상 겹치면 iOS Safari 스크롤 깜빡임/잔상을 의심하세요.
- sticky/fixed 요소가 있고, 조상에
overflow가 있음 backdrop-filter/filter/mix-blend-mode사용- 스크롤 컨테이너에
-webkit-overflow-scrolling: touch - 큰 이미지/비디오가 많고, lazy-load로 레이아웃이 흔들림
- 스크롤 이벤트에서 DOM 스타일을 자주 변경
이때의 우선순위:
backdrop-filter제거/대체로 증상 사라지는지 확인- sticky/fixed를 레이어 승격 + backface hidden
- 내부 스크롤 제거(가능하면)
- contain으로 페인트 범위 제한
“레이어 승격”의 부작용과 안전장치
레이어를 늘리면:
- GPU 메모리/타일 캐시 사용량 증가
- 오래된 기기(iPhone 8 등)에서 오히려 프레임 드랍
- 텍스트가 살짝 흐려 보이는 현상(서브픽셀 렌더링 변화)
따라서 다음 원칙을 권합니다.
- 승격 대상은 상단바/하단바/오버레이/모달처럼 “항상 떠 있는” 요소 위주
- 리스트의 모든 카드에
translateZ(0)같은 처방을 일괄 적용하지 않기 will-change는 상시가 아니라, 필요할 때만 토글하는 방식도 고려
예시: 문제 구조를 안정화한 CSS 템플릿
아래는 iOS Safari에서 비교적 안정적으로 동작하도록 조정한 예시입니다.
:root {
/* 스크롤 중 blur 비활성화 같은 토글에 사용 */
}
.page {
min-height: 100vh;
}
/* 가능하면 body 스크롤을 사용하고, 내부 overflow 스크롤은 최소화 */
.topbar {
position: sticky;
top: 0;
z-index: 1000;
/* 합성 레이어 승격 */
transform: translateZ(0);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
/* 고비용 효과는 상황에 따라 토글 */
backdrop-filter: blur(12px);
background: rgba(255,255,255,0.8);
}
.is-scrolling .topbar {
backdrop-filter: none;
}
.section {
/* 섹션 단위로 페인트 격리 */
contain: paint;
}
.card {
/* 카드가 외부 레이아웃을 건드리지 않는다면 */
contain: layout paint;
}
마무리: “합성 경계를 설계”하면 iOS Safari가 조용해진다
iOS Safari 스크롤 잔상·깜빡임은 대개 특정 속성 하나의 버그라기보다, 레이어 분리/합성 경계가 불안정해져서 생기는 현상입니다. 해결은 의외로 정석적입니다.
- 스크롤 중 페인트를 유발하는 요소를 줄이고
- sticky/fixed/오버레이는 의도적으로 합성 레이어로 분리하고
backdrop-filter/filter같은 고비용 효과는 토글/대체하고- 스크롤 컨테이너 구조를 단순화
이렇게 “렌더링 파이프라인 관점”으로 접근하면, iOS Safari 특유의 까다로운 케이스도 재현-진단-해결의 루프로 안정적으로 수습할 수 있습니다.
추가로, 원인 좁히기 루프를 운영 관점에서 더 체계화하고 싶다면 장애 체크리스트 글도 참고할 만합니다: systemd 서비스가 계속 재시작될 때 진단 체크리스트