Published on

Safari iOS 스크롤 잔상·깜빡임 해결 - Compositing 튜닝

Authors

서버/백엔드 이슈처럼 보이지 않는데 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)와 섞이면 합성 경계가 꼬일 수 있습니다.

핵심 개념: “페인트를 줄이고, 합성 레이어 경계를 안정화”

해결의 방향은 대체로 둘 중 하나(또는 둘 다)입니다.

  1. 스크롤 중 페인트가 일어나지 않게 만든다.
    • 레이아웃 변화를 없애고, 애니메이션/전환을 transform/opacity로 제한
  2. 합성 레이어를 의도적으로 분리해 스크롤과 독립적으로 합성되게 만든다.
    • 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 스타일을 자주 변경

이때의 우선순위:

  1. backdrop-filter 제거/대체로 증상 사라지는지 확인
  2. sticky/fixed를 레이어 승격 + backface hidden
  3. 내부 스크롤 제거(가능하면)
  4. 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 서비스가 계속 재시작될 때 진단 체크리스트