스크롤 트리거 애니메이션이 CLS를 유발하는 방법

스크롤 시 숨김 헤더가 조용히 CLS를 실패하게 만들 수 있습니다. 그 이유와 해결 방법을 소개합니다.

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2026-03-26

Cumulative Layout Shift 감사를 요청받을 때마다 저는 당연히 로딩 단계의 변경부터 시작합니다. 크기가 지정되지 않은 이미지, 삽입된 광고, 폰트 교체(FOUT)를 확인합니다.

하지만 모든 CLS가 페이지 로드 중에 발생하는 것은 아닙니다. 때로는 스크롤링과 같은 상호작용에 의해 트리거되기도 합니다. Lighthouse는 스크롤하지 않으며 정확히 어디를 봐야 할지 알아야 하므로 이러한 것들은 잡기 어렵습니다.

2026년 3월에 Arjen Karel이 마지막으로 검토함

최근 감사에서 CoreDash RUM 데이터를 추가했고 의심스러운 패턴을 보았습니다. #header 요소가 평소보다 훨씬 더 많은 레이아웃 변경을 유발하는 것 같았습니다.

coredash cls scroll header

여기서 우리가 보고 있는 것은 사용자가 아래로 스크롤할 때 숨겨지고 위로 스크롤할 때 다시 나타나는 고정 헤더입니다. 이는 꽤 자주 보는 패턴이며 제대로 수행되지 않으면 스크롤 방향이 바뀔 때마다 레이아웃 변경을 유발합니다.

스크롤 트리거 변경이 다른 이유

일반적으로 상호작용 후에는 CLS가 발생하지 않습니다. CLS는 예상치 못한 레이아웃 변경을 측정하며 상호작용 후에는 레이아웃이 변경될 것으로 예상할 것입니다. 그렇기 때문에 CLS 지표에는 500ms의 자동 유예 기간이 있습니다. 사용자 상호작용 후 500밀리초 이내에 발생하는 레이아웃 변경은 CLS 점수에서 제외됩니다. 브라우저는 이러한 변경에 대해 hadRecentInput 플래그를 설정하고 확인하여 이를 처리합니다.

문제는 이것이 클릭, 탭 및 키 누름과 같은 개별 이벤트에만 적용된다는 것입니다. 레이아웃 불안정성 사양에 따르면, 스크롤은 명시적으로 제외되는 입력이 아닙니다. 드래그나 핀치 줌 제스처도 마찬가지입니다. 개별 이벤트만 500ms 유예 기간을 얻습니다.

즉, 스크롤 이벤트에 의해 트리거된 모든 레이아웃 변경은 CLS 점수에 반영됩니다.

2025 Web Almanac에 따르면, 모바일 페이지의 40%가 여전히 합성되지 않은 애니메이션을 사용합니다. 이는 모든 프레임에서 레이아웃 재계산을 트리거하는 top, height 또는 margin과 같은 속성에 대한 애니메이션입니다. 스크롤 중에 이들 중 하나라도 실행되면 모든 단일 프레임이 CLS에 반영됩니다.

스크롤 시 숨김 헤더 문제

오늘 발견한 문제인 '스크롤 시 숨김 헤더 문제'부터 시작하겠습니다. 고정 헤더는 JavaScript를 사용하여 스크롤 시 가시성을 전환합니다. 구현은 다음과 같습니다:

/* CSS */
#header {
  position: fixed;
  top: 0;
  transition: top 0.3s ease;
}

/* JavaScript */
window.addEventListener('scroll', () => {
  if (scrollingDown) {
    header.style.top = '-80px';  // hide
  } else {
    header.style.top = '0px';    // show
  }
});

레이아웃 변경은 transition: top 0.3s ease와 JavaScript에서 header.style.top을 변경하는 조합으로 인해 발생합니다.

이유는 다음과 같습니다: top은 레이아웃 속성입니다. 브라우저가 top에 대한 변경을 처리할 때 전체 렌더링 파이프라인(스타일, 레이아웃, 페인트, 합성)을 실행합니다. 해당 전환 중 모든 애니메이션 프레임에서 브라우저는 레이아웃을 다시 계산합니다. 각 재계산은 새로운 레이아웃 변경 항목을 생성합니다.

스크롤 방향이 바뀔 때마다 사용자가 페이지와 상호작용하는 한 CLS 카운터는 계속 올라갑니다.

제가 감사하던 라이브 사이트에서 이 정확한 동작을 재현하는 데모를 만들고 비디오를 녹화했습니다. 어느 정도 스크롤한 후 총 CLS가 0.1을 넘어섰습니다. 물론 이러한 수준의 스크롤이 실제 환경에서 자주 발생하지는 않지만 메커니즘은 변하지 않습니다. 그리고 저는 이러한 레이아웃 변경이 사이트를 경계 너머로 밀어내어 총 CLS를 실패하게 만드는 것을 보았습니다.

애니메이션 적용 시 레이아웃 변경을 유발할 수 있는 CSS 속성은 무엇입니까?

애니메이션이 CLS를 유발하려면 레이아웃을 트리거해야 합니다. 브라우저의 렌더링 파이프라인에는 스타일 계산 후 레이아웃, 페인트, 합성의 세 가지 주요 단계가 있습니다. 합성 단계에만 영향을 미치는 속성은 결코 레이아웃 변경을 유발하지 않습니다. CSS 전환이 레이아웃 변경을 유발하는 방법에 대한 자세한 내용은 함께 제공되는 문서를 참조하세요.

레이아웃을 트리거하는 속성 (애니메이션 적용 시 CLS를 유발함):

top, left, right, bottom, width, height, margin, padding, border-width, font-size

이 모든 것은 요소의 기하학적 형태나 위치를 변경합니다. 브라우저는 요소(그리고 아마도 다른 요소들도)의 위치를 다시 계산해야 합니다.

레이아웃을 완전히 건너뛰는 속성 (애니메이션에 안전함):

transformopacity는 컴포지터 스레드에서 실행됩니다. 이들은 레이아웃과 페인트를 모두 우회합니다. 브라우저는 이를 GPU에서 처리합니다(이것이 메인 스레드가 바쁠 때도 이러한 애니메이션이 작동하는 이유입니다). 레이아웃 재계산을 결코 트리거하지 않기 때문에 레이아웃 변경 항목을 생성하지 않습니다.

레이아웃 속성을 transform으로 교체

스크롤 시 숨김 헤더에 대한 해결책은 간단합니다. top 애니메이션을 transform: translateY()로 교체합니다.

/* CSS - BEFORE (causes CLS) */
#header {
  position: fixed;
  top: 0;
  transition: top 0.3s ease;
}

/* CSS - AFTER (no CLS) */
#header {
  position: fixed;
  top: 0;
  transition: transform 0.3s ease;
}

/* JavaScript - BEFORE */
header.style.top = '-80px';

/* JavaScript - AFTER */
header.style.transform = 'translateY(-80px)';

이것은 동일한 결과를 제공합니다: 헤더가 위로 슬라이드되어 사라집니다. 하지만 transform: translateY()는 전적으로 컴포지터에서 실행되며 결코 레이아웃 변경을 유발하지 않습니다.

동일한 원칙이 모든 스크롤 트리거 애니메이션에 적용됩니다:

  • 슬라이딩 패널: left 또는 right 대신 transform: translateX() 사용
  • 확장되는 요소: height 대신 transform: scaleY() 사용
  • 페이드 요소: 높이 변경과 결합된 visibility 대신 opacity 사용

CLS를 유발하는 다른 스크롤 트리거 패턴

스크롤 시 숨김 헤더는 꽤 자주 보는 것이지만 더 많은 것들이 있습니다. 스크롤 이벤트에 대한 응답으로 레이아웃 속성을 변경하는 모든 JavaScript는 레이아웃 변경을 생성합니다. 예:

top 또는 margin-top을 사용하는 패럴랙스 효과. transform: translateY()로 교체하거나 순수 CSS 패럴랙스를 위해 CSS perspectivetranslateZ 방식을 사용합니다.

스크롤 시 높이가 변하는 고정(Sticky) 내비게이션. 사용자가 임계값을 지나 스크롤할 때 80px에서 50px로 축소되는 내비게이션 바입니다. 높이 변경이 transition: height로 애니메이션화된 경우 이를 transform: scaleY()로 교체합니다.

width를 사용하는 스크롤 연동 진행률 표시줄. width를 애니메이션화하여 왼쪽에서 오른쪽으로 커지는 읽기 진행률 표시기입니다. 대신 transform-origin: left와 함께 transform: scaleX()를 사용합니다.

최신 대안: CSS Scroll-Driven Animations. animation-timeline: scroll() API를 사용하면 JavaScript 없이 스크롤 진행률에서 애니메이션을 구동할 수 있습니다. 이러한 애니메이션은 기본적으로 transformopacity를 사용하고 컴포지터에서 실행되므로 레이아웃 변경을 결코 유발하지 않습니다. 브라우저 지원은 Chrome 115+ 및 Edge 115+를 포함하며 Safari는 아직 개발 중입니다. 패럴랙스, 진행률 표시줄 및 요소 나타내기 애니메이션과 같은 스크롤 연동 효과의 경우 이것이 가장 깔끔한 해결책입니다. JavaScript 스크롤을 완전히 버리고 싶다면 Scroll-Driven Animations API가 앞으로 나아갈 길입니다.

Lighthouse가 이를 잡아내지 못하는 이유

Lighthouse는 시뮬레이션된 페이지 로드를 실행합니다. 페이지를 스크롤하지 않습니다. 로드 후 페이지와 상호작용하지 않습니다. 이는 스크롤 트리거 레이아웃 변경이 랩 테스트에서 완전히 보이지 않음을 의미합니다.

이러한 변경을 잡는 유일한 방법은 Real User Monitoring을 사용하는 것입니다. 필드 데이터는 사용자가 스크롤하고, 클릭하고, 탐색하는 동안 발생하는 모든 레이아웃 변경을 포함하여 전체 페이지 수명 주기를 캡처합니다. CrUX CLS 점수가 Lighthouse CLS 점수보다 나쁘다면 스크롤 트리거 애니메이션이 가장 먼저 조사해야 할 항목 중 하나입니다. CoreDash는 스크롤로 인해 발생하는 모든 변경을 포함하여 실제 방문자의 CLS를 추적하므로 어떤 요소가 책임이 있는지 정확히 볼 수 있습니다.

스크롤 트리거 레이아웃 변경을 찾는 방법

Chrome DevTools를 엽니다. 성능(Performance) 패널로 이동합니다. 레코더(원형 아이콘)를 시작한 다음 페이지를 위아래로 여러 번 스크롤합니다. 기록을 중지합니다. Layout Shifts 트랙에서 보라색 다이아몬드를 찾습니다. 스크롤 활동과 일치하는 변경 사항의 클러스터가 보이면 스크롤 트리거 CLS 문제가 있는 것입니다. 또한 Core Web Vitals Visualizer Chrome 확장 프로그램을 사용하여 스크롤할 때 실시간으로 CLS를 볼 수도 있습니다.

devtools scroll layout shift

경험 법칙

스크롤 시 이동하는 경우 transform으로 애니메이션화하세요. 스크롤 시 페이드되는 경우 opacity로 애니메이션화하세요. 이러한 속성은 컴포지터 전용입니다.

JavaScript의 경우: 스크롤 이벤트 핸들러, IntersectionObserver 콜백 및 스크롤 위치에 대한 응답으로 요소 스타일을 변경하는 모든 JavaScript를 확인하세요. top, left, margin, width, height 또는 padding을 사용하는 경우 이를 동등한 transform으로 교체하세요.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

Search Console에서 경고 받으셨어요?

50페이지짜리 PDF 말고, 필드 데이터로 뒷받침된 우선순위 수정 목록을 드립니다.

감사 요청
스크롤 트리거 애니메이션이 CLS를 유발하는 방법Core Web Vitals 스크롤 트리거 애니메이션이 CLS를 유발하는 방법