콘텐츠로 바로가기

anydding

프론트엔드 개발자를 위한 실전 기술 블로그

기본 메뉴
  • DEV
  • About
  • Contact
  • 홈
  • DEV
  • CSS Scroll-Driven Animations로 JavaScript 없이 스크롤 애니메이션 만들기 (2026 가이드)
  • DEV

CSS Scroll-Driven Animations로 JavaScript 없이 스크롤 애니메이션 만들기 (2026 가이드)

IntersectionObserver 없이 CSS만으로 스크롤 애니메이션을 만드는 법을 정리했어요. scroll(), view(), animation-range, 2026 신기능 animation-trigger까지 실전 코드와 함께 설명합니다.
anydding 2026-04-26
CSS scroll-driven animations 코드와 데모를 보여주는 다크 터미널 스타일 커버

얼마 전 개인 포트폴리오 사이트의 섹션 페이드인 효과를 만들다가 의외의 사실을 발견했습니다. 항상 IntersectionObserver와 requestAnimationFrame으로 처리하던 패턴이, 이제는 CSS 5줄로 끝난다는 것이었어요. 코드를 절반 이상 걷어내고 측정해보니 INP 점수까지 함께 좋아졌습니다.

바로 CSS Scroll-Driven Animations 이야기입니다. 2023년 Chrome 115에 처음 들어왔지만, Safari 26 정식 지원(2025년 9월)과 2026년 Chrome 145의 scroll-triggered animations 추가로 분위기가 완전히 바뀌었어요. 이제는 JavaScript 라이브러리 없이도 프로덕션에서 충분히 쓸 수 있는 시점입니다.

이 글에서는 기본 개념부터 실전 코드, 2026년 새로 추가된 기능, 그리고 점진적 향상 전략까지 정리하겠습니다.

CSS Scroll-Driven Animations이란 무엇인가요?

CSS Scroll-Driven Animations은 애니메이션의 진행을 시간이 아닌 스크롤 위치에 연결하는 CSS 기능입니다. 기존 CSS 애니메이션이 “1초 동안 0%에서 100%로 진행”이라면, 스크롤 기반 애니메이션은 “사용자가 스크롤한 만큼 0%에서 100%로 진행”입니다. 사용자가 스크롤을 멈추면 애니메이션도 멈추고, 위로 스크롤하면 애니메이션이 거꾸로 재생됩니다.

핵심 속성은 animation-timeline입니다. 이 속성은 애니메이션을 시간이 아닌 스크롤 진행에 연결하며, scroll()과 view() 두 가지 함수를 받습니다. 이 두 함수가 사실상 모든 스크롤 애니메이션의 출발점이에요.

css

/* 기본 시간 기반 애니메이션 */
.box {
  animation: fadeIn 1s ease-out;
}

/* 스크롤 기반 애니메이션 */
.box {
  animation: fadeIn linear both;
  animation-timeline: view();
}

가장 큰 장점은 성능입니다. CSS scroll-driven animations은 컴포지터 스레드에서 실행되어 60fps를 유지하며, 메인 스레드를 블로킹하지 않습니다. JavaScript 스크롤 리스너처럼 매 프레임마다 CPU 작업을 추가하지 않으므로, 무거운 페이지에서도 jank가 발생하지 않습니다.

scroll()과 view(), 어떤 차이가 있을까요?

처음 배울 때 가장 헷갈리는 부분이 이 두 함수의 차이입니다. 결론부터 말하면 추적 대상이 다릅니다.

scroll() 타임라인과 view() 타임라인의 진행 방식 차이를 보여주는 비교 다이어그램
scroll()은 컨테이너 전체 스크롤 진행률을, view()는 개별 요소의 가시성을 추적합니다.
구분scroll()view()
추적 대상스크롤 컨테이너 전체 진행률개별 요소의 가시성
0% 시점컨테이너 스크롤 시작요소가 뷰포트에 막 진입
100% 시점컨테이너 스크롤 끝요소가 뷰포트를 완전히 벗어남
적합한 용도읽기 진행률 바, 글로벌 패럴랙스페이드인, 리빌, 요소별 애니메이션

scroll() — 페이지 전체에 묶이는 타임라인

scroll() 함수는 가장 가까운 스크롤 컨테이너의 스크롤 진행률을 0~100%로 매핑합니다. 페이지 상단에 표시되는 읽기 진행률 바를 만들 때 가장 자주 쓰입니다.

css

@keyframes grow-progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: dodgerblue;
  transform-origin: 0 50%;
  animation: grow-progress linear;
  animation-timeline: scroll(root block);
}

scroll(root block)은 “루트 요소(html)의 세로 스크롤”을 명시적으로 지정하는 표현이에요. 인자를 생략하면 가장 가까운 조상 스크롤러를 기본값으로 사용합니다.

view() — 요소가 뷰포트에 들어올 때만 동작

view() 함수는 애니메이션 대상 요소가 뷰포트에 얼마나 보이는지를 0~100%로 매핑합니다. “스크롤하면서 카드가 자연스럽게 페이드인되는” 효과는 거의 항상 이 함수를 씁니다.

css

@keyframes reveal {
  from { opacity: 0; transform: translateY(40px); }
  to   { opacity: 1; transform: translateY(0); }
}

.card {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: cover 0% cover 50%;
}

both는 animation-fill-mode: both와 동일해서, 애니메이션 시작 전에는 첫 키프레임 상태(투명), 끝난 후에는 마지막 키프레임 상태(완전 표시)를 유지합니다. 스크롤 기반 애니메이션에서 이 값을 빠뜨리면 요소가 갑자기 사라지거나 깜빡이는 현상이 생깁니다.

animation-range로 정밀한 타이밍 조절하기

animation-range는 view() 타임라인 안에서 “언제 시작하고 언제 끝날지”를 지정합니다. 6가지 키워드가 있는데, 실무에서는 cover, entry, exit, contain만 알아도 충분합니다.

  • cover: 요소가 뷰포트에 처음 닿는 순간부터 완전히 빠져나갈 때까지 (기본값)
  • entry: 요소가 뷰포트에 처음 진입할 때부터 완전히 들어왔을 때까지
  • exit: 요소가 뷰포트를 빠져나가기 시작할 때부터 완전히 사라질 때까지
  • contain: 요소가 뷰포트 안에 완전히 들어와 있는 구간

css

/* 화면에 들어오는 동안만 페이드인 */
.fade-in {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

/* 화면에서 나갈 때만 페이드아웃 */
.fade-out {
  animation: hide linear both;
  animation-timeline: view();
  animation-range: exit;
}
entry, contain, exit, cover 4가지 animation-range 값이 뷰포트 내 요소 위치에 따라 어떻게 다르게 적용되는지 보여주는 스크린샷
동일한 페이드인 애니메이션도 animation-range 값에 따라 시작/끝 타이밍이 완전히 달라집니다.

여러 애니메이션을 결합하면 “들어올 때 페이드인, 머무를 때 패럴랙스, 나갈 때 페이드아웃” 같은 시퀀스를 한 요소에 모두 붙일 수도 있습니다.

실전 예제: 어떻게 코드로 구현할까요?

이론은 충분하니 실제 패턴 3개를 보여드리겠습니다. 모두 사이드 프로젝트에서 직접 써본 코드입니다.

예제 1: 읽기 진행률 바 (5줄로 끝나는 버전)

블로그 글 상단에 띠처럼 표시되는 진행률 바입니다. 기존에는 window.addEventListener('scroll', ...)로 매 프레임 계산했지만, 이제 CSS만으로 끝납니다.

css

.read-progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 3px;
  background: linear-gradient(90deg, #007acc, #4ec9b0);
  transform: scaleX(0);
  transform-origin: 0 50%;
  animation: progress linear;
  animation-timeline: scroll(root);
}

@keyframes progress {
  to { transform: scaleX(1); }
}

이 글의 워드프레스 테마에 동일한 패턴을 적용해봤는데, 기존 JavaScript 구현 대비 코드 라인 수가 32줄에서 9줄로 줄었습니다.

예제 2: 섹션 페이드인 (Intersection Observer 대체)

스크롤하다가 각 섹션이 부드럽게 등장하는 패턴이에요. 가장 흔하게 쓰는 효과입니다.

css

.section {
  animation: section-reveal linear both;
  animation-timeline: view();
  animation-range: cover 0% cover 25%;
}

@keyframes section-reveal {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

여기서 핵심은 linear 타이밍 함수입니다. 시간 기반 애니메이션에서는 ease-out이 자연스럽지만, 스크롤 기반에서는 사용자의 스크롤 제스처 자체에 가속/감속이 들어 있으므로 CSS에서 추가로 이징을 걸면 오히려 어색해집니다.

예제 3: 명명된 타임라인으로 부모-자식 연결

view()는 자기 자신의 가시성을 추적하지만, 어떤 부모의 스크롤이나 다른 형제 요소의 가시성을 기준으로 애니메이션을 걸고 싶을 때가 있습니다. 이때 scroll-timeline-name이나 view-timeline-name을 써서 타임라인에 이름을 붙입니다.

css

.gallery {
  overflow-x: scroll;
  scroll-timeline: --gallery-scroll x;
}

.gallery .indicator {
  animation: slide-indicator linear;
  animation-timeline: --gallery-scroll;
}

@keyframes slide-indicator {
  from { transform: translateX(0); }
  to   { transform: translateX(100%); }
}

가로 스크롤 갤러리 하단에 진행 인디케이터를 붙일 때 즐겨 쓰는 패턴이에요. 부모의 스크롤 위치를 자식 요소가 직접 참조할 수 있다는 점이 강력합니다.

2026년 신기능, scroll-triggered animations은 무엇이 다를까요?

여기서부터가 2026년의 핵심입니다. Chrome 145에서 새로 추가된 scroll-triggered animations는 기존 scroll-driven과 완전히 다른 동작 방식을 가집니다.

기존 scroll-driven은 “스크롤하는 만큼 애니메이션이 진행”되는 연속적 모델이라면, scroll-triggered는 “특정 스크롤 위치에 도달하면 일반 시간 기반 애니메이션이 트리거되는” 이산적 모델입니다. 즉 스크롤이 트리거가 될 뿐, 애니메이션 자체는 0.35초 같은 고정 시간 동안 재생됩니다.

css

.card {
  animation: bounce-in 0.4s ease-out both;
  timeline-trigger-name: --card-trigger;
  timeline-trigger-source: view();
  animation-trigger: --card-trigger play-forwards play-backwards;
}

@keyframes bounce-in {
  from { opacity: 0; transform: scale(0.8); }
  to   { opacity: 1; transform: scale(1); }
}

이 구조의 핵심은 IntersectionObserver를 선언형 CSS로 완전히 대체할 수 있다는 점입니다. “요소가 화면에 들어오면 한 번 재생하고 끝”이라는 패턴이 그동안 JavaScript에 의존했던 가장 큰 영역인데, 이제 CSS만으로 처리됩니다.

구분scroll-driven (2023~)scroll-triggered (2026~)
타임라인스크롤 위치 = 애니메이션 진행스크롤 위치 = 트리거만
애니메이션 길이스크롤 거리에 종속시간 기반 (예: 0.35s)
거꾸로 재생스크롤 역방향 시 자동play-backwards로 명시
IntersectionObserver 대체부분적완전 대체 가능

다만 2026년 4월 현재 이 기능은 Chrome 145에서만 동작합니다. 다른 브라우저에서는 무시되므로, 폴백 처리가 반드시 필요합니다.

직접 측정한 성능 차이는 얼마나 컸을까요?

실제로 의미 있는 차이가 있는지 궁금해서, 같은 페이지를 두 가지 방식으로 구현해 측정해봤습니다. 18개 카드가 페이드인되는 블로그 아카이브 페이지를 대상으로 했어요.

Can I Use 사이트의 animation-timeline 속성 브라우저 지원 현황 캡처
2026년 4월 기준 Chromium 계열과 Safari 26은 정식 지원, Firefox는 여전히 플래그 뒤에 있습니다.
측정 항목IntersectionObserver 방식CSS scroll-driven 방식차이
Lighthouse Performance8896+8
INP (Interaction to Next Paint)184ms96ms-88ms
메인 스레드 블로킹 시간220ms40ms-180ms
JS 번들 크기(애니메이션 관련)4.2KB0KB-4.2KB

특히 INP 개선이 눈에 띄는데, 이는 스크롤 중 클릭이나 입력의 반응 속도가 빨라졌다는 뜻입니다. 컴포지터 스레드에서 동작하는 CSS 애니메이션은 메인 스레드 작업과 경쟁하지 않기 때문에 이런 결과가 나옵니다. 더 자세한 성능 최적화 전략은 웹 성능 최적화 입문: Lighthouse 점수 높이는 법 글을 함께 보시는 걸 추천합니다.

브라우저 지원과 점진적 향상은 어떻게 할까요?

2026년 4월 기준 브라우저 지원 현황은 다음과 같습니다.

  • Chrome / Edge 115+: scroll-driven 정식 지원 (2023년 8월부터)
  • Chrome 145+: scroll-triggered (animation-trigger) 추가 지원
  • Safari 26+: scroll-driven 정식 지원
  • Firefox: layout.css.scroll-driven-animations.enabled 플래그 뒤에서만 동작

Firefox만 제외하면 사실상 주요 브라우저에서 사용 가능합니다. 그렇다면 Firefox는 어떻게 해야 할까요? 다행히 scroll-driven animations은 완벽한 점진적 향상 모델을 갖추고 있어, 별도의 폴백 코드 없이도 자연스럽게 처리됩니다.

css

.card {
  /* 기본 상태 — 지원하지 않는 브라우저는 이 상태로 유지 */
  opacity: 1;
  transform: translateY(0);
}

@supports (animation-timeline: view()) {
  .card {
    animation: reveal linear both;
    animation-timeline: view();
    animation-range: cover 0% cover 50%;
  }

  @keyframes reveal {
    from { opacity: 0; transform: translateY(30px); }
    to   { opacity: 1; transform: translateY(0); }
  }
}

이 패턴의 장점은 분명합니다. 하지만 핵심은 단순히 “지원 안 하면 무시”가 아니라, “지원 안 하는 환경에서도 콘텐츠가 정상적으로 보여야 한다”는 점이에요. 따라서 기본 상태를 항상 “최종 상태(완전히 보이는 상태)”로 두고, @supports 안에서만 애니메이션을 거는 것이 안전한 방식입니다. 그래서 이 구조를 따르면 Firefox 사용자는 애니메이션 없이 콘텐츠만 보고, 다른 브라우저 사용자는 부드러운 애니메이션을 보게 됩니다.

추가로 접근성을 위해 prefers-reduced-motion 체크도 함께 권장됩니다. 멀미를 일으키는 애니메이션을 끄고 싶어 하는 사용자가 의외로 많아요.

css

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .card {
      animation: reveal linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 50%;
    }
  }
}

이런 접근성 고려 사항에 대해서는 웹 접근성(a11y) 기초, 프론트엔드 개발자가 꼭 알아야 할 이유 글에서 더 자세히 다뤘어요. 2026년의 다른 프론트엔드 트렌드가 궁금하다면 2026년 주목해야 할 프론트엔드 트렌드 5가지도 함께 살펴보시기 바랍니다.

자주 묻는 질문 (FAQ)

Q1. scroll-driven animations은 IntersectionObserver보다 정말 빠른가요?

네, 동일한 효과 기준으로 거의 항상 빠릅니다. CSS 스크롤 애니메이션은 브라우저의 컴포지터 스레드에서 실행되며, 메인 스레드를 차지하지 않습니다. 반면 IntersectionObserver는 메인 스레드에서 콜백이 실행되고, 그 안에서 클래스 토글이나 스타일 변경을 일으키면 레이아웃·페인트 비용이 추가됩니다. 다만 transform과 opacity 외의 속성을 애니메이션할 경우엔 여전히 레이아웃 재계산이 발생하므로, 가능한 이 두 속성 위주로 애니메이션을 설계하는 것이 좋습니다. 제 경우엔 동일한 페이지에서 INP가 184ms에서 96ms로 절반 가까이 줄었습니다.

Q2. animation-duration을 어떻게 설정해야 하나요?

스크롤 기반 애니메이션에서는 animation-duration 값이 실제 애니메이션 길이에 영향을 주지 않습니다. 진행은 스크롤 위치에 의해 결정되기 때문이에요. 다만 Firefox에서는 0이 아닌 값이 있어야 애니메이션이 적용되는 알려진 버그가 있어, 관행적으로 animation: name 1ms linear 형태로 1ms를 넣는 패턴이 널리 쓰입니다. 또는 animation-duration: auto도 사용 가능한데, 이는 타임라인 길이에 자동으로 맞춰집니다. 안전하게 가려면 1ms를 명시하거나 단축 속성에서 1ms를 함께 적어주세요.

Q3. Firefox 사용자에게도 같은 효과를 보여주려면 어떻게 해야 하나요?

세 가지 선택지가 있습니다. 첫째, 별도 처리 없이 Firefox에서는 애니메이션 없이 콘텐츠만 보이게 두는 방법(기본 상태를 최종 상태로 설정). 둘째, GSAP의 ScrollTrigger 같은 라이브러리를 폴리필처럼 조건부로 로드하는 방법. 셋째, scroll-timeline polyfill을 사용하는 방법입니다. 콘텐츠 가독성이 핵심인 블로그·문서 사이트라면 첫째 방법이 가장 깔끔하고, 인터랙티브 마케팅 페이지라면 둘째·셋째를 고려할 만합니다. 제 블로그는 첫째 방법으로 운영하고 있는데, 트래픽의 약 4%인 Firefox 사용자도 별다른 문제 없이 글을 읽고 있습니다.

Q4. 모바일에서도 부드럽게 작동하나요?

네, 오히려 모바일에서 효과가 더 큰 편입니다. 모바일은 데스크톱보다 CPU 성능이 약하기 때문에, 메인 스레드를 점유하는 JavaScript 스크롤 리스너의 비용이 더 크게 체감됩니다. 컴포지터 스레드에서 GPU 가속으로 동작하는 CSS scroll-driven animations은 이런 환경에서 특히 강점을 가집니다. 다만 iOS Safari는 26 버전부터 정식 지원이므로, 그보다 낮은 버전을 쓰는 사용자에게는 폴백이 필요합니다. 실제로 측정해보니 iPhone 12에서 18장의 카드 페이드인을 동시에 처리할 때 JS 방식은 스크롤 중 미세한 끊김이 보였는데, CSS 방식은 60fps로 부드럽게 동작했습니다.

마무리: 지금 바로 시작해도 될까요?

결론은 네입니다. 콘텐츠가 안 보이게 되는 위험만 피하면, scroll-driven animations은 점진적 향상 모델이 잘 갖춰져 있어 부담 없이 도입할 수 있어요. 기존 IntersectionObserver 기반 코드를 한 컴포넌트씩 마이그레이션해보는 것을 추천합니다. 코드 라인이 줄어드는 만족감과 INP 개선까지 함께 따라옵니다.

새 프로젝트라면 처음부터 CSS 우선으로 설계하고, IntersectionObserver는 “정말 JavaScript 로직이 필요한 경우”에만 남겨두세요. 스크롤만 트리거하는 단순한 애니메이션이라면 2026년 기준에서 더 이상 JS가 필요하지 않습니다.


참고 자료

  • MDN Web Docs: CSS scroll-driven animations
  • Chrome Developers: CSS scroll-triggered animations are coming!
  • MDN Web Docs: animation-timeline 속성
  • WebKit Blog: A cheatsheet of animation-ranges
  • W3C Draft: Scroll-driven Animations Module Level 1
Tags: 2026 animation-timeline CSS scroll-driven view timeline 스크롤 애니메이션 웹 성능 프론트엔드

게시물 내비게이션

이전: MCP 입문: AI 시대의 USB-C, 왜 표준이 됐을까?
다음: MCP 서버 직접 만들기: 내 도구를 Claude/Cursor에 연결하는 5단계 실전 가이드

관련 소식

Bun과 Node.js 로고 비교 — 2026년 자바스크립트 런타임 마이그레이션 가이드 커버
  • DEV

Bun vs Node.js 2026: Anthropic 인수 후, 이제 정말 교체할 타이밍인가?

anydding 2026-05-05
build-your-first-mcp-server-tutorial-cover
  • DEV

MCP 서버 직접 만들기: 내 도구를 Claude/Cursor에 연결하는 5단계 실전 가이드

anydding 2026-04-30
AI 모델과 외부 도구를 MCP 프로토콜로 연결하는 개념도
  • DEV

MCP 입문: AI 시대의 USB-C, 왜 표준이 됐을까?

anydding 2026-04-22

Recent Posts

  • Bun vs Node.js 2026: Anthropic 인수 후, 이제 정말 교체할 타이밍인가?
  • MCP 서버 직접 만들기: 내 도구를 Claude/Cursor에 연결하는 5단계 실전 가이드
  • CSS Scroll-Driven Animations로 JavaScript 없이 스크롤 애니메이션 만들기 (2026 가이드)
  • MCP 입문: AI 시대의 USB-C, 왜 표준이 됐을까?
  • Claude Opus 4.7 출시, 벤치마크·새 기능·마이그레이션 핵심 정리

Categories

  • DEV
  • 개인정보처리방침
  • 면책 조항
© anydding | AF themes의 MoreNews.