Velog
CSS만으로 스크롤 기반 애니메이션 구현하기
스크롤 기반 애니메이션을 더 이상 JS로 손코딩할 필요가 없다. animation-timeline: scroll()/view() + @keyframes 두 줄로 컴포지터 스레드에서 돌아가는 애니메이션을 만들 수 있다.
핵심 요약
스크롤 기반 애니메이션은 타겟 / @keyframes / 타임라인 세 요소로 구성된다. 새로 들어온 타임라인 종류는 두 가지다.
| 타임라인 | 동력 | 대표 사용처 |
|---|---|---|
scroll() | 스크롤 컨테이너 전체 진행률 | 페이지 진행 바, 가로 갤러리 |
view() | 특정 요소가 뷰포트를 가로지르는 비율 | 페이드인, 패럴랙스, 등장 효과 |
핵심 사용 패턴은 다음과 같다.
@keyframes grow {
from { width: 0; }
to { width: 100%; }
}
footer::after {
animation: grow linear;
animation-timeline: scroll(); /* 또는 view() */
}
브라우저 지원은 Chromium 계열이 먼저, Safari 26 베타가 합류했고 Firefox 는 플래그 단계. 따라서 실무에선 점진적 향상(progressive enhancement) 으로 적용해야 한다.
애니메이션의 "동력"을 시간이 아닌 스크롤로 갈아끼우는 것이 핵심이다. 기존 CSS 애니메이션은 문서 타임라인(시간) 위에서 진행되지만, animation-timeline 속성을 통해 동력을 스크롤 위치로 교체한다. @keyframes 자체는 그대로 — 바뀌는 건 "무엇이 진행률을 결정하는가" 뿐이다. 그래서 IntersectionObserver/스크롤 이벤트를 안 쓰고도 동일한 효과를 컴포지터 스레드에서 돌릴 수 있다.
스크롤 연동 애니메이션은 지금까지 JS 진영의 가장 무거운 영역이었다. 매 프레임 스크롤 위치를 읽어 transform을 갱신하면 메인 스레드가 막히고, IntersectionObserver는 "진입/이탈"만 알지 "진행률"은 모른다. 이번 표준은 다음 세 가지를 한 번에 해결한다.
- 메인 스레드 비의존:
transform/opacity키프레임이면 GPU 합성으로 처리 - 선언형 코드: 이벤트 콜백 없이 CSS 한 블록
- 접근성 친화:
prefers-reduced-motion으로 즉시 끌 수 있음
면접에서 "스크롤 진행 바를 어떻게 구현하시겠습니까" 같은 질문이 오면, JS 답변과 CSS 답변을 둘 다 가지고 트레이드오프를 말할 수 있어야 한다.
학습 포인트
면접 답변으로 연결할 학습 포인트입니다.
`scroll()` vs `view()` 의 의미 차이
둘 다 "스크롤로 진행률을 만든다" 는 점은 같지만 기준점이 다르다.
scroll(): 가장 가까운 스크롤 컨테이너의 0% ~ 100% 를 진행률로 매핑. 페이지 전체 프로그레스 바에 적합.view(): 대상 요소가 뷰포트를 통과하는 정도 를 진행률로 매핑. 카드가 뷰포트에 들어올 때 페이드인 같은 "요소 단위" 효과에 적합.
.progress { animation-timeline: scroll(root block); }
.card { animation-timeline: view(); }
카드 등장 효과에 scroll() 을 쓰는 것. 카드 위치와 무관하게 페이지 전체 스크롤에 끌려가서 모든 카드가 동시에 변한다. 요소 단위 효과는 반드시 view().
JS 구현과의 성능/책임 분리
기존 JS 방식과 비교해 보면 어디가 이득이고 어디가 한계인지가 보인다.
| 방식 | 스레드 | 진행률 해상도 | 한계 |
|---|---|---|---|
scroll 이벤트 | 메인 | 매우 높음 | 입력 지연·jank |
| IntersectionObserver | 메인(콜백) | 이산값(threshold) | 부드러운 보간 불가 |
| Scroll-Driven Animations | 컴포지터 | 연속 0~1 | transform/opacity 외엔 효과 제한 |
즉 단순 진행률·시각 효과는 CSS 가 압승, DOM 조작이나 데이터 로딩 같은 사이드 이펙트는 여전히 JS 쪽이 필요하다.
CSS 만으로 모든 스크롤 효과를 대체할 수 있다고 오해하는 것. 스크롤로 데이터 페칭 이나 상태 변경 은 여전히 JS 책임이다.
접근성과 점진적 향상
스크롤 애니메이션은 모션 민감 사용자에게 직접적인 위험이다. 다음 두 줄을 빼먹으면 회사 디자인 시스템 통과를 못 한다.
@media (prefers-reduced-motion: reduce) {
.card { animation: none; }
}
@supports not (animation-timeline: scroll()) {
/* IntersectionObserver 폴백 또는 정적 표시 */
}
Firefox·구버전 Safari 는 아직 미지원이므로 "애니메이션 없이도 콘텐츠가 정상 보여야 한다" 가 절대 원칙.
초기 상태(opacity: 0)로 두고 애니메이션이 진입할 때 보이게 만드는 패턴. 미지원 브라우저에서는 콘텐츠가 영원히 안 보인다.
읽는 순서
- 1이론
animation-timeline속성과scroll()/view()함수의 정의, 컴포지터 스레드 합성의 의미를 정리한다. "진행률의 동력이 시간 → 스크롤로 바뀐다" 라는 한 문장으로 요약해본다. - 2구현
footer::after진행 바와 카드 등장 페이드인을 각각scroll(),view()로 구현해보고, 같은 효과를 IntersectionObserver 로도 만들어 코드량과 부드러움을 비교한다. - 3실무
프로젝트에서 스크롤 이벤트로 transform 을 갱신하는 곳을 찾아
transform/opacity만 쓰는 효과인지 확인하고, 가능하면 CSS 로 치환 +@supports/prefers-reduced-motion폴백을 함께 추가한다. - 4설명
동료에게 "CSS Scroll-Driven Animation 과 IntersectionObserver 중 무엇을 언제 쓰나" 를 5분 안에 트레이드오프 표 없이 말로 설명해본다. 막히면 브라우저 매트릭스 / 효과 종류 / 사이드 이펙트 세 축으로 정리.
면접 연결 질문
[감점 답변] "CSS 가 새 표준이니까 CSS". [좋은 답변] 시각 효과만 필요하면 CSS — 메인 스레드 비의존이라 jank 가 없고, 코드량도 압도적으로 작다. 다만 Firefox/구 Safari 지원 요구가 강하면 @supports 폴백 + IntersectionObserver/scroll 이벤트 조합. 즉 "브라우저 매트릭스 + 효과 종류(시각만 vs 데이터 변화)" 두 축으로 결정한다고 말한다.
[감점 답변] "둘 다 스크롤에 따라 움직이는 거예요". [좋은 답변] 기준점으로 갈린다. scroll() 은 스크롤 컨테이너의 전체 스크롤 진행률 을 0~1 로 매핑(페이지 진행 바). view() 는 해당 요소가 뷰포트를 통과하는 비율 을 매핑(요소 등장 효과). 한 줄로: "페이지 단위면 scroll, 요소 단위면 view".
[감점 답변] "prefers-reduced-motion 만 쓰면 됨". [좋은 답변] 두 가지를 분리한다. (1) @media (prefers-reduced-motion: reduce) 로 모션 비활성화 분기, (2) 미지원 브라우저에서도 콘텐츠가 보이도록 초기 상태를 정적으로 정상 노출 시키고, 애니메이션은 진행률에 따라 위에 덧붙는 방식으로 작성. 즉 "애니메이션 = 보너스" 원칙.
자기 점검
scroll() 을 쓰면 각 요소별 진행률이 자동으로 잡힌다고 오해하는 것. 실제로는 컨테이너 전체 진행률 이라 모든 카드가 같은 진행률을 공유한다.
"속도가 빠르니 모든 걸 대체할 수 있다" 라는 결론. 실제로는 DOM 조작·네트워크 트리거 등 사이드 이펙트 는 CSS 만으로 불가능.