Published on

iOS Safari 스크롤 잔상·깜빡임 해결 가이드

Authors

서버나 자바스크립트 로직이 멀쩡한데도 iOS Safari에서만 스크롤할 때 화면이 잔상처럼 남거나, 특정 섹션이 깜빡이거나, 순간적으로 하얗게 번쩍(white flash)하는 현상을 겪는 경우가 많습니다. 특히 position: sticky 헤더, backdrop-filter 블러, overflow 스크롤 컨테이너, transform 애니메이션이 섞이면 재현 확률이 급격히 올라갑니다.

이 글은 “무조건 will-change를 넣자” 같은 처방이 아니라, iOS Safari(WebKit)의 합성(compositing)과 페인팅(painting) 특성을 기준으로 원인별로 증상을 분해하고 안전한 해결책을 적용하는 흐름으로 정리합니다.

1) 증상 유형부터 분류하기

iOS Safari 스크롤 문제는 겉으로 비슷해 보여도 원인이 다릅니다. 아래 중 무엇에 가까운지 먼저 분류하세요.

1-1. 잔상(ghosting)처럼 이전 프레임이 남는다

  • 스크롤 중 텍스트가 흐릿해지거나 이전 위치가 잠깐 보임
  • 고정 헤더/푸터 주변에서 특히 심함

1-2. 깜빡임(flicker) 또는 순간적인 white flash

  • 특정 섹션에 진입할 때만 번쩍임
  • 이미지/비디오/캔버스가 포함된 영역에서 자주 발생

1-3. 스크롤이 끊기거나, 터치 스크롤이 "잡아당겨"지는 느낌

  • overflow: auto 내부 스크롤에서 체감이 큼
  • 스크롤 성능 문제(INP/Long Task)와 섞여 보이기도 함

스크롤 성능 이슈가 의심되면 프론트에서 Long Task를 먼저 제거해야 합니다. 이 관점은 Chrome INP 점수 급락 원인 - Long Task 추적법 글의 접근이 iOS 디버깅에도 그대로 도움이 됩니다(원인은 다르지만 “메인 스레드가 막히면 스크롤이 이상해 보인다”는 점은 동일).

2) iOS Safari에서 유독 잘 터지는 원인

WebKit은 특정 조건에서 레이어 합성 경계가 바뀌거나, 스크롤 중 리페인트가 과도하게 발생하면 화면이 번쩍이는 문제가 생길 수 있습니다. 실무에서 가장 흔한 트리거는 다음입니다.

2-1. position: fixed 또는 sticky + 복잡한 효과

  • backdrop-filter: blur(...)
  • 반투명 배경(rgba) + 그림자(box-shadow)
  • 큰 영역의 filter, mix-blend-mode

이 조합은 합성 레이어가 자주 바뀌며 깜빡임을 유발합니다.

2-2. overflow 스크롤 컨테이너와 중첩된 transform

  • 부모 또는 조상에 transform: translateZ(0) 같은 GPU 힌트가 들어가 있거나
  • 자식이 애니메이션 transform을 수행하는 경우

특히 “스크롤 컨테이너 내부에서 sticky 요소”는 iOS에서 가장 문제를 많이 일으키는 패턴 중 하나입니다.

2-3. 큰 이미지, 비디오, background-attachment: fixed

  • iOS Safari는 background-attachment: fixed 지원이 제한적이며, 대체 동작이 렌더링 버그처럼 보일 수 있습니다.
  • 큰 이미지가 스크롤 중 디코딩/리사이즈되면 white flash처럼 보이기도 합니다.

3) 재현과 진단: 먼저 "원인을 좁히는" 방법

3-1. iOS 원격 디버깅으로 레이어/리페인트 의심 구간 찾기

  1. macOS Safari에서 Develop 메뉴 활성화
  2. iPhone Safari를 연결해 원격 Web Inspector 열기
  3. 문제 화면에서 스크롤하면서 다음을 확인
    • 깜빡이는 요소가 특정 컴포지팅 레이어로 분리되는지
    • 스크롤 중 스타일 변경이 과도한지

3-2. CSS를 토글하며 범인 후보를 빠르게 제거

가장 빠른 방법은 문제 요소에 대해 아래 속성을 “하나씩” 꺼보는 것입니다.

  • backdrop-filter
  • filter
  • box-shadow
  • transform
  • overflow: hidden/auto

이 과정에서 어떤 속성을 끄면 즉시 증상이 사라지는지를 기록하면 해결이 매우 빨라집니다.

4) 해결책 체크리스트(원인별)

아래는 실전에서 성공률이 높은 순서로 정리했습니다. 단, GPU 힌트는 과하게 쓰면 메모리와 배터리를 소모하고 오히려 버그를 늘릴 수 있으니 “필요한 곳에만” 적용하세요.

4-1. 스크롤 컨테이너에 iOS 관용 설정 적용

내부 스크롤을 쓰는 경우 다음을 먼저 적용합니다.

.scroll-area {
  overflow: auto;
  -webkit-overflow-scrolling: touch;
}

이 설정은 관성 스크롤을 활성화하지만, 상황에 따라 sticky/transform과 충돌하기도 합니다. 충돌이 의심되면 내부 스크롤 구조를 단순화(가능하면 body 스크롤로 회귀)하는 것이 가장 확실합니다.

4-2. 깜빡이는 요소를 "안전하게" 레이어로 분리

transform: translateZ(0) 또는 will-change: transform은 레이어 분리를 유도합니다. 다만 무분별한 적용은 금물입니다.

.flicker-prone {
  will-change: transform;
  transform: translateZ(0);
  backface-visibility: hidden;
}
  • backface-visibility: hidden은 텍스트/이미지 깜빡임 완화에 도움이 되는 경우가 있습니다.
  • 레이어 분리는 “페인트를 줄이는” 목적이지, 모든 버그를 해결하는 만능키가 아닙니다.

4-3. stickyoverflow 조합을 끊기

position: sticky는 조상에 overflow: hidden/auto/scroll이 있으면 동작이 바뀌거나 버그가 쉽게 납니다. 가능하면 sticky의 조상 체인을 정리하세요.

나쁜 예(조상에 overflow):

.wrapper {
  overflow: hidden; /* sticky에 악영향 */
}
.header {
  position: sticky;
  top: 0;
}

개선 예(overflow를 sticky 조상에서 제거):

.wrapper {
  overflow: visible;
}
.content-scroll {
  overflow: auto;
  -webkit-overflow-scrolling: touch;
}
.header {
  position: sticky;
  top: 0;
}

핵심은 sticky가 붙어야 하는 영역의 조상에는 overflow를 두지 않는 것입니다.

4-4. backdrop-filter는 iOS에서 "격리"해서 쓰기

블러 헤더는 멋지지만 iOS Safari에서 가장 흔한 깜빡임 원인입니다. 대안은 두 가지입니다.

  1. iOS에서만 블러를 끄고 반투명 배경으로 대체
.header {
  position: sticky;
  top: 0;
  background: rgba(20, 20, 20, 0.7);
}

@supports ((-webkit-backdrop-filter: blur(12px)) or (backdrop-filter: blur(12px))) {
  .header {
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
  }
}
  1. 블러 영역을 작게 만들고(높이/면적 최소화), 그림자/필터를 줄이기
  • box-shadow를 약하게
  • 블러 반경을 줄이기
  • 헤더 높이를 줄이기

4-5. 스크롤 중 레이아웃을 흔드는 속성 변경 피하기

스크롤 이벤트에서 다음 작업을 하면 iOS에서 깜빡임이 더 심해질 수 있습니다.

  • offsetHeight 같은 레이아웃 읽기와 스타일 쓰기 혼합
  • top/left/width/height를 자주 변경

대신 transform 기반으로 바꾸고, 업데이트는 requestAnimationFrame으로 묶습니다.

let ticking = false;

window.addEventListener('scroll', () => {
  if (ticking) return;
  ticking = true;

  requestAnimationFrame(() => {
    const y = window.scrollY || 0;
    const header = document.querySelector('.header');

    // 레이아웃 영향이 적은 transform 사용
    header.style.transform = `translate3d(0, ${Math.min(0, -y)}px, 0)`;

    ticking = false;
  });
}, { passive: true });

주의: 위 패턴도 남용하면 UX가 나빠질 수 있습니다. 가능하면 “스크롤에 반응하는 UI” 자체를 단순화하는 것이 최선입니다.

4-6. iOS에서 100vh 대신 동적 viewport 단위 사용

주소창이 접히고 펼쳐지면서 100vh가 변동하면 레이아웃 점프와 깜빡임이 생깁니다. iOS 16+ 환경에서는 dvh 계열을 고려하세요.

.full {
  min-height: 100vh;
  min-height: 100dvh;
}

구형 iOS까지 커버해야 하면 JS로 --vh 커스텀 프로퍼티를 설정하는 방식도 여전히 유효합니다.

function setVh() {
  const vh = window.innerHeight * 0.01;
  document.documentElement.style.setProperty('--vh', `${vh}px`);
}

setVh();
window.addEventListener('resize', setVh);
.full {
  min-height: calc(var(--vh) * 100);
}

4-7. 이미지/비디오로 인한 white flash 줄이기

  • 이미지에 width/height를 명시해 레이아웃 시프트를 방지
  • 가능한 경우 decoding="async"loading="lazy" 사용
  • 큰 배경 이미지는 해상도를 줄이고, background-size를 과도하게 키우지 않기
<img
  src="/hero.jpg"
  width="1200"
  height="800"
  loading="lazy"
  decoding="async"
  alt="hero"
/>

5) "한 번에" 적용하는 실전 처방 세트

현장에서 가장 자주 성공하는 조합은 아래입니다.

  1. sticky 헤더의 조상에서 overflow 제거
  2. 헤더에서 backdrop-filter를 iOS에서만 비활성화하거나 면적 최소화
  3. 깜빡이는 요소에만 transform: translateZ(0)backface-visibility: hidden 제한 적용
  4. 스크롤 이벤트 기반 DOM 업데이트 제거 또는 requestAnimationFrame으로 제한
  5. 100vh 사용 구간을 100dvh 또는 --vh 방식으로 교체

이렇게 해도 남으면, 원인을 “성능” 쪽으로 돌려 메인 스레드 점유를 의심해야 합니다. 스크롤 중 실행되는 무거운 작업이 있으면 렌더링이 깨져 보이기 쉽습니다. 이때는 Chrome INP 점수 급락 원인 - Long Task 추적법처럼 Long Task를 먼저 잡는 접근이 효과적입니다.

6) 체크리스트: 배포 전 iOS Safari 검증 항목

  • sticky 요소의 모든 조상에 overflow가 있는지 확인
  • 스크롤 컨테이너를 꼭 써야 하는지 재검토(가능하면 body 스크롤)
  • backdrop-filter, filter, mix-blend-mode 사용 구간을 최소화
  • 애니메이션은 transformopacity 중심으로 제한
  • 100vh 기반 레이아웃은 100dvh 또는 --vh로 대체
  • 이미지 크기 명시로 레이아웃 시프트 방지

7) 마무리: iOS Safari는 "효과를 줄일수록" 안정적이다

iOS Safari의 스크롤 잔상·깜빡임은 단일 원인이라기보다, 합성 레이어 경계가 불안정해지는 CSS 조합에서 자주 발생합니다. 따라서 해결의 핵심은 “특정 속성을 마구 추가”하는 것이 아니라, 문제 조합을 끊고 레이아웃/페인트를 단순화하는 것입니다.

특히 stickyoverflow, backdrop-filter 조합은 가장 먼저 의심하고, 필요한 경우 iOS에서만 점진적 저하(블러 제거, 그림자 축소)를 적용하는 것이 현실적인 정답입니다.