CSS Scroll-Driven Animations: JavaScript 없이 스크롤 애니메이션 구현

웹 개발

CSSScroll Animations웹 애니메이션성능 최적화인터랙션

이 글은 누구를 위한 것인가

  • IntersectionObserver + JavaScript로 스크롤 애니메이션을 구현 중인 개발자
  • CSS만으로 부드럽고 성능 좋은 스크롤 효과를 만들고 싶은 팀
  • 브라우저 지원 현황을 확인하고 프로덕션 적용 여부를 판단하려는 개발자

들어가며

스크롤 연동 애니메이션은 항상 JavaScript scroll 이벤트 + requestAnimationFrame의 영역이었다. 메인 스레드를 차지하고, 성능이 나빠지면 jank(버벅임)가 생겼다.

CSS Scroll-Driven Animations는 이 작업을 CSS만으로, 그것도 합성 레이어(Compositor)에서 처리한다. 메인 스레드와 무관하게 동작해서 60fps가 안정적이다.

이 글은 bluefoxdev.kr의 CSS 최신 기능 가이드 를 참고하고, Scroll-Driven Animations 실전 구현 관점에서 확장하여 작성했습니다.


1. 핵심 개념

[Scroll-Driven Animations 3가지 개념]

1. animation-timeline: scroll()
   → 스크롤 컨테이너의 진행률에 연동
   → 0% = 최상단, 100% = 최하단
   
2. animation-timeline: view()
   → 뷰포트 내 요소의 가시성에 연동
   → 요소가 화면에 들어올 때 ~ 나갈 때
   
3. @scroll-timeline (명시적 정의)
   → 특정 범위를 타임라인으로 지정

[브라우저 지원 (2026년 기준)]
  Chrome 115+: ✅ 완전 지원
  Edge 115+:   ✅ 완전 지원
  Firefox 110+: ✅ 지원
  Safari 17.4+: ✅ 지원
  → 현재 프로덕션 사용 가능

2. 스크롤 진행 바

/* 페이지 스크롤 진행률 표시 바 */
@keyframes expand-bar {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.scroll-progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg, #0066ff, #00cfff);
  transform-origin: left;
  
  /* 스크롤 타임라인 연동 */
  animation: expand-bar linear;
  animation-timeline: scroll(root block);  /* 루트 요소, 세로 스크롤 */
  animation-fill-mode: both;
}

/* HTML: <div class="scroll-progress-bar"></div> */

3. 요소 페이드인 (view() 타임라인)

/* 스크롤 시 요소가 뷰포트에 들어오면 페이드인 */
@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(40px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.reveal-on-scroll {
  animation: fade-in-up 0.6s ease-out both;
  
  /* view() 타임라인: 요소가 뷰포트에 진입/이탈하는 시점 */
  animation-timeline: view();
  
  /* 진입 시 시작, 완전히 보일 때 완료 */
  animation-range: entry 0% cover 30%;
}

/* 카드 격자에 스태거드 효과 */
.card:nth-child(1) { animation-delay: 0ms; }
.card:nth-child(2) { animation-delay: 100ms; }
.card:nth-child(3) { animation-delay: 200ms; }
.card:nth-child(4) { animation-delay: 300ms; }

.card {
  animation: fade-in-up 0.5s ease-out both;
  animation-timeline: view();
  animation-range: entry 0% cover 25%;
}

4. 패럴랙스 효과

/* CSS만으로 구현하는 패럴랙스 */
@keyframes parallax-slow {
  from { transform: translateY(-20%); }
  to   { transform: translateY(20%); }
}

@keyframes parallax-fast {
  from { transform: translateY(-40%); }
  to   { transform: translateY(40%); }
}

.hero-section {
  position: relative;
  overflow: hidden;
  height: 100vh;
}

.hero-background {
  position: absolute;
  inset: -20%;  /* 패럴랙스 이동 공간 확보 */
  
  animation: parallax-slow linear both;
  animation-timeline: scroll(root);
  animation-range: 0% 50%;  /* 섹션 절반까지만 */
}

.hero-text {
  position: relative;
  z-index: 1;
  
  animation: parallax-fast linear both;
  animation-timeline: scroll(root);
  animation-range: 0% 40%;
}

5. 실용적인 패턴 모음

/* 1. 헤더 축소 효과 (스크롤 다운 시 작아짐) */
@keyframes shrink-header {
  from {
    padding: 20px 40px;
    background: transparent;
  }
  to {
    padding: 12px 40px;
    background: rgba(255, 255, 255, 0.95);
    backdrop-filter: blur(10px);
    box-shadow: 0 2px 20px rgba(0,0,0,0.1);
  }
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  
  animation: shrink-header linear both;
  animation-timeline: scroll(root);
  animation-range: 0px 200px;  /* 0 ~ 200px 스크롤 범위 */
}

/* 2. 텍스트 하이라이트 (읽으면서 강조) */
@keyframes highlight-text {
  from { background-size: 0% 100%; }
  to   { background-size: 100% 100%; }
}

.highlight {
  background-image: linear-gradient(transparent 60%, #ffd700 60%);
  background-repeat: no-repeat;
  background-size: 0% 100%;
  
  animation: highlight-text linear both;
  animation-timeline: view();
  animation-range: entry 20% entry 80%;
}

/* 3. 이미지 reveal (클립 마스크 효과) */
@keyframes image-reveal {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0% 0 0); }
}

.reveal-image {
  animation: image-reveal 0.8s cubic-bezier(0.77, 0, 0.175, 1) both;
  animation-timeline: view();
  animation-range: entry 0% entry 60%;
}

/* 4. 카운터 애니메이션 (숫자 증가) */
@property --num {
  syntax: '<integer>';
  initial-value: 0;
  inherits: false;
}

.stat-number {
  --num: 0;
  counter-reset: num var(--num);
  
  animation: count-up linear both;
  animation-timeline: view();
  animation-range: entry 20% cover 50%;
}

.stat-number::after {
  content: counter(num) "%";
}

@keyframes count-up {
  from { --num: 0; }
  to   { --num: 87; }  /* 목표 숫자 */
}

6. 점진적 향상 전략

/* 미지원 브라우저 대응 */
@supports (animation-timeline: scroll()) {
  .reveal-on-scroll {
    animation: fade-in-up 0.6s ease-out both;
    animation-timeline: view();
    animation-range: entry 0% cover 30%;
  }
}

/* 미지원 시 기본 상태 (모두 보임) */
.reveal-on-scroll {
  opacity: 1;
  transform: none;
}

/* 모션 감소 설정 존중 */
@media (prefers-reduced-motion: reduce) {
  .reveal-on-scroll,
  .scroll-progress-bar,
  .parallax-element {
    animation: none !important;
  }
}

마무리

CSS Scroll-Driven Animations는 2026년 기준으로 모든 주요 브라우저에서 지원된다. JavaScript로 구현하던 스크롤 애니메이션을 CSS로 옮기면 메인 스레드 부담이 줄고, 코드도 간결해진다.

IntersectionObserver로 클래스를 토글하는 패턴은 이제 animation-timeline: view()로 대체할 수 있다. 특히 스크롤 진행 바, 요소 페이드인, 헤더 축소 효과는 바로 적용 가능하다.