안녕하세요! 웹 API를 깊이 있게 파고들기 위해 MDN 공식 문서를 직접 찾아보시다니 정말 훌륭한 학습 태도입니다. 특히 리액트(React)나 넥스트제이에스(Next.js) 같은 프레임워크로 컴포넌트 기반 개발을 하실 때도, 이런 브라우저 네이티브 API의 동작 원리를 알아두면 아주 복잡한 상태의 애니메이션을 깔끔하게 제어할 수 있어서 큰 무기가 된답니다.
문서가 영어로 되어 있어서 조금 막막하셨을 텐데, 제가 이해하기 쉽도록 딱딱하지 않은 구어체로 꼼꼼하게 번역해 드릴게요. 단순한 번역을 넘어, 실무에서 어떻게 쓰이는지 이해를 돕기 위한 보충 설명과 강사의 팁도 팍팍 넣어두었으니 천천히 읽어보세요!
Web Animations API는 웹 페이지의 시각적인 변화, 즉 DOM(문서 객체 모델) 요소들의 애니메이션을 동기화하고 타이밍을 맞출 수 있게 해주는 기능이에요. 이 API는 '타이밍 모델(Timing Model)'과 '애니메이션 모델(Animation Model)'이라는 두 가지 모델을 결합해서 이런 멋진 작업들을 수행한답니다.
💡 강사의 보충 설명:
기존에는 자바스크립트로 애니메이션을 만들 때setTimeout이나requestAnimationFrame을 사용해서 픽셀 값을 일일이 계산해야 했어요. 아니면 CSS의@keyframes와transition에 전적으로 의존해야 했죠.하지만 Web Animations API(줄여서 WAAPI라고도 불러요)가 등장하면서, CSS 애니메이션의 부드러운 렌더링 성능과 자바스크립트의 유연한 제어 능력(재생, 일시 정지, 역재생, 타임라인 탐색 등)을 모두 가져갈 수 있게 되었습니다.
Web Animations API는 브라우저와 개발자가 DOM 요소의 애니메이션을 설명하고 제어할 수 있는 '공통 언어'를 제공해요. 이 API의 핵심 개념과 구체적인 사용법에 대해 더 자세히 알고 싶다면, Web Animations API 사용하기(Using the Web Animations API) 문서를 꼭 읽어보시는 걸 추천해요.
💡 강사의 팁:
처음 이 API를 접하시면 "CSS로도 충분한데 왜 굳이 자바스크립트 코드를 써야 하지?"라고 생각하실 수 있어요. 하지만 사용자의 스크롤 위치나 클릭 같은 동적인 이벤트에 맞춰 애니메이션을 즉각적으로 조작해야 하는 인터랙티브 웹(예: 애플 사이트 같은 스크롤 애니메이션)을 구현할 때는 이 API가 정말 구세주 같은 역할을 합니다.
다음은 Web Animations API를 구성하는 주요 객체(인터페이스)들입니다.
Animation
: 애니메이션 노드나 소스에 대한 재생 제어 기능(play, pause, reverse 등)과 타임라인을 제공하는 객체예요. KeyframeEffect() 생성자로 만들어진 객체를 인자로 받아서 사용할 수 있습니다.
(강사 보충: 실질적으로 우리가 애니메이션을 조종하는 '리모컨' 역할을 한다고 생각하시면 이해하기 쉬워요!)
KeyframeEffect
: 애니메이션이 가능한 속성들과 그 값들의 집합인 키프레임(keyframes), 그리고 타이밍 옵션(지속 시간, 반복 횟수 등)을 설명하는 객체예요. 이렇게 만들어진 키프레임 효과는 Animation() 생성자를 사용해서 실제로 재생시킬 수 있습니다.
(강사 보충: CSS의 @keyframes 블록을 자바스크립트 객체 형태로 만들어둔 것이라고 보면 됩니다.)
AnimationTimeline
: 애니메이션의 타임라인(시간의 흐름)을 나타냅니다. 이 인터페이스는 타임라인의 기본적인 기능들을 정의하기 위해 존재하며, DocumentTimeline이나 향후 추가될 타임라인 객체들이 이를 상속받아요. 개발자가 직접 이 객체에 접근해서 사용하는 경우는 거의 없습니다.
AnimationEvent
: CSS 애니메이션(CSS Animations) 모듈의 일부로서, 애니메이션의 이름과 진행된 경과 시간을 캡처하여 이벤트를 발생시킵니다.
DocumentTimeline
: 애니메이션 타임라인들을 나타내며, 기본 문서(document) 타임라인을 포함합니다. 이 기본 타임라인은 Document.timeline 속성을 통해 접근할 수 있어요.
Web Animations API는 단순히 새로운 객체만 만든 것이 아니라, 기존에 우리가 잘 알고 있던 document와 element 객체에도 유용한 기능들을 추가해 주었어요.
Document 인터페이스 확장document.timeline
: 기본 문서 타임라인을 나타내는 DocumentTimeline 객체입니다. 문서가 로드된 시점부터 흘러가는 전체 시간을 의미하죠.
document.getAnimations()
: 현재 document 안에 있는 요소들 중에서 실행 중이거나 효과가 적용되어 있는 모든 Animation 객체들을 배열(Array) 형태로 반환합니다.
💡 강사의 팁: 페이지 전체에 재생 중인 애니메이션을 한 번에 모두 멈추고 싶거나(예: 사용자가 '애니메이션 끄기' 옵션을 선택했을 때), 일괄적으로 속도를 조절해야 할 때 이 메서드를 사용하면 정말 편리합니다.
Element 인터페이스 확장Element.animate()
: 특정 요소(Element)에 애니메이션을 생성하고 즉시 재생까지 해주는 단축 메서드예요. 호출하면 생성된 Animation 객체 인스턴스를 반환해 줍니다.
💡 강사의 핵심 팁: 별 다섯 개짜리입니다! 실무에서 Web Animations API를 쓴다고 하면 십중팔구 이 메서드를 사용합니다.
element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 1000 })처럼 키프레임 배열과 옵션 객체만 툭 던져주면 바로 부드러운 애니메이션이 실행되죠. 상태 관리 라이브러리(Zustand 등)의 값 변경에 따라 DOM 요소를 직접 흔들어주거나 페이드 효과를 줄 때 아주 찰떡궁합이에요.
Element.getAnimations()
: 특정 요소에 현재 영향을 주고 있거나, 앞으로 영향을 주도록 예약되어 있는 Animation 객체들의 배열을 반환합니다. 특정 요소의 애니메이션만 콕 집어서 조작하고 싶을 때 사용해요.
| 명세 (Specification) |
|---|
| Web Animations |
관련된 내용들을 더 공부하고 싶다면 아래 링크들을 확인해 보세요!
animation 단축 속성animation-timeline 속성안녕하세요, 미래의 프론트엔드 개발자 여러분! 👋 오늘 우리가 함께 정복할 주제는 바로 Web Animations API(WAAPI)입니다.
Web Animations API를 사용하면 자바스크립트만으로 애니메이션을 구성하고 재생을 세밀하게 제어할 수 있어요. 이번 문서에서는 '이상한 나라의 앨리스(Alice in Wonderland)'를 테마로 한 재미있는 데모와 튜토리얼을 통해 여러분이 실무에서 애니메이션을 다룰 수 있는 올바른 방향을 제시해 드릴게요.
Web Animations API는 브라우저의 강력한 애니메이션 엔진을 개발자에게 개방하여 자바스크립트로 직접 조작할 수 있게 해주는 기능입니다. 이 API는 사실 CSS Animations와 CSS Transitions가 내부적으로 작동하는 기반 원리로 설계되었으며, 향후 등장할 새롭고 화려한 애니메이션 효과들을 위한 확장성까지 염두에 두고 만들어졌죠.
특히 꼼수(hack)를 쓰거나 억지로 렌더링을 유발하지 않고, 심지어 Window.requestAnimationFrame()을 직접 호출하지 않고도 브라우저가 자체적으로 내부 최적화를 수행하게 놔둘 수 있기 때문에 웹에서 애니메이션을 구현하는 가장 성능이 뛰어난 방법 중 하나입니다.
WAAPI를 사용하면 사용자와 상호작용하는(Interactive) 애니메이션을 스타일시트(CSS)에서 자바스크립트로 가져올 수 있습니다. 즉, 프레젠테이션(디자인 시각요소)과 동작(비즈니스 로직)을 완벽하게 분리할 수 있는 것이죠. 재생 방향이나 멈춤 상태를 제어하기 위해 DOM 요소에 클래스를 넣고 빼거나 CSS 속성을 억지로 덮어쓰는 무거운 작업들에 더 이상 의존하지 않아도 됩니다.
게다가 선언적인 순수 CSS와는 다르게, 자바스크립트를 사용하면 애니메이션의 속성 값부터 지속 시간(duration)까지 상황에 맞게 동적(Dynamically)으로 설정할 수도 있어요. 나만의 커스텀 애니메이션 라이브러리를 만들거나 사용자의 마우스/스크롤에 반응하는 애니메이션을 만들 때 WAAPI는 그야말로 완벽한 도구입니다. 자, 그럼 이 녀석이 뭘 할 수 있는지 함께 살펴볼까요?
이 문서에는 '이상한 나라의 앨리스'에서 영감을 받은 WAAPI 활용 예제들이 준비되어 있습니다. 이 예제들은 Rachel Nabors님이 제작하여 공유해주셨어요. 전체 예제 모음은 CodePen에서 직접 확인하실 수 있으며, 여기서는 우리 학습에 꼭 필요한 알짜배기 예제들만 추려서 소개하도록 하겠습니다.
💡 강사의 팁: > 현업에서는 GSAP이나 Framer Motion 같은 무거운 외부 라이브러리를 습관적으로 추가하는 경우가 많아요. 하지만 브라우저 내장 기능인 Web Animations API를 제대로 다룰 줄 안다면, 불필요한 번들 용량을 줄이고 브라우저 렌더링 파이프라인에 최적화된 매우 가볍고 빠른 애니메이션을 구현할 수 있습니다! 면접에서도 바닐라 JS로 애니메이션을 능숙하게 다룬다고 어필하기 아주 좋은 무기랍니다.
WAAPI 학습을 시작하는 가장 친숙하고 쉬운 방법은, 대부분의 웹 개발자라면 이전에 한 번쯤은 장난치듯 다뤄봤을 CSS 애니메이션에서부터 출발하는 것입니다. CSS 애니메이션은 우리에게 익숙한 문법을 가지고 있고, 작동 원리를 단계별로 쪼개서 보여주기에 아주 훌륭한 교보재거든요.
먼저 원더랜드로 이어지는 토끼굴 아래로 빙글빙글 떨어지는 앨리스의 모습을 구현한 CSS 애니메이션을 확인해 볼게요.
<div class="wrapper">
<div id="tunnel"></div>
<div id="alice">
<svg xmlns="[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)" viewBox="0 0 400 400">
<path
d="M110.1 2.7h8.9c3.4.4 6.7.8 10.1 1.3 9.8 1.5 17.8 6.4 24.5 13.7.4.5 1.9.6 2.5.3 6.8-4.4 13.9-8.2 21.9-9.9 1.3-.3 3.4-1.2 3.7 1.5.6 4.9 1.4 9.9 1.7 14.8.3 4.4.1 8.7.1 12.2 2.1 1.5 4.6 2.3 5.5 4 4.2 8.4 3.2 17.6 3.1 26.6 0 1.2-.4 3.3.1 3.6 10.3 4.9 20.7 9.6 31.1 14.4 2.5-4.9-2.3-16-15.8-14.4.6-.5 1.4-1.1 2.2-1.1 2.5.1 4.9.4 7.4.7 6 .8 10.9 3.7 14.6 8.4 1.2 1.5 1.6 4.2 1.1 6.1-.7 3.2-3.7 4-7.1 4.1 4.5 3.5 6.5 8.1 6.8 13.3.6 9.4-1.1 18.6-4.8 27.1-3.9 8.8-5.2 17.5-3.3 26.8.6 3.2 1.2 7 .2 9.9-2 6.2-7.8 8.6-13.4 10.9-3 1.2-7.4 1.2-6.3 6.3.8 3.7-.4 4 .2 4.5 5.8 5.8 11.8 11.5 17.6 17.3 1.7 1.7 3 3.8 4.3 5.5-1.1.4-1.8.7-2.4 1 7.5 5.8 14.9 11.6 22.4 17.4 4.3-4.3 8.6-9 13.3-13.2 8.1-7.3 16.7-14 24.5-21.7 3.3-3.3 4.9-8.2 7.4-12.3.3-.4 1.3-.9 1.6-.7 4.6 2.7 6.8 7.2 7.9 12.1 1.3 5.7 1.6 11.6 2.3 17.1 4.2-.2 8.8-.8 13.4-.4 2 .1 4.6 1.8 5.5 3.5 2.2 4.3 3.8 8.9 5.3 13.5 3.7 11.5 6.9 23.2 10.7 34.7 1.7 5.1 3.4 10.4 8.6 13.4.5.3.5 2.7 0 3.1-3.3 2.5-6.9 4.6-10.5 7 2 5.8 4.3 12.6 6.7 19.6.7-.8 1.4-1.6 2.1-2.3 1.9-1.9 3.5-1.6 4.2 1.2.7 3 1.3 6.2 1.5 9.3.3 7.3.4 14.6.6 21.9 0 .4.2.8.5 1.2 3.6 4.7 7.1 9.3 10.7 14 1.7 2.3 3 5.4 5.3 6.6 5.5 2.7 11.5 4.4 17.3 6.6v.7c-.4.3-.7.8-1.2 1-5.8 2.1-11.6 4.3-17.5 6.2-4.2 1.3-8.4 2-12.4-1.2-1.8-1.5-3.9-2.6-5.8-3.8 0 2.3.1 4.4-.1 6.4-.1.8-.7 2.2-1.2 2.2-2.6.2-5.3.1-7.9.1-1.1 0-2.7.3-3.2-.3-1-1.2-2.1-2.9-2.1-4.4-.1-5.2.1-10.4.3-15.6.1-1.8 1.5-3.9.9-5.4-1.7-4.3-4-8.4-6.1-12.5-2.4-4.6-6.4-9.1-1.2-14.3.3-.3.3-1.3 0-1.7-4.7-6.5-9.5-13.1-14.4-19.5-1.2-1.5-2.9-3.7-4.4-3.7-6.7.1-13.4.8-20.1 1.3-.7.1-1.6.2-1.9.6-7.1 9.1-14 18.3-21.1 27.4-1.3 1.7-2.9 3.4-4.3 5 1.7.6 3.3 1.1 4.8 1.7.6.2 1.3.5 1.6 1 .2.3-.1 1.1-.4 1.5-2 2.6-4.1 5.2-6.1 7.8-4.3 5.3-8.7 10.5-13 15.9-.8 1-1.5 2.4-1.6 3.6-.2 5.4-.1 10.7-.1 16.1 0 1.5-.7 3.6.1 4.5 2.4 3 5.3 5.5 7.9 8.2 1.6 1.7 3 3.5 4.6 5.5-2.6.2-4.5.3-6.4.4h-3.7c-4.8-1.4-9.8-2.5-14.5-4.3-3.5-1.4-7.8-2.5-8-7.7-.1-2.1-.2-4.3-.4-6.7-1 1.1-1.7 2.1-2.6 2.9-.3.3-1 .4-1.3.2-1.9-1.1-3.7-2.2-5.5-3.4-1.7-1.1-4.5-1.6-3.5-4.5 2.3-6.5 6.4-11.6 12.7-14.9.6-.3 1.3-.9 1.6-1.5 3.9-8.2 7.8-16.4 11.8-24.6.7-1.5.4-4.5 3.8-3.8.2.1 1.2-3.1 1.8-4.9-2.8 1.5-5 2.9-7.4 3.9-7.4 3-14.7 6.4-23.1 5.6-8.5-.7-16.2-3.4-23.2-8-9.9-6.7-14.2-17-17.5-27.9-.5-1.7-.5-5.1-3.5-1.6-.1.2-.4.2-.6.3-2.5 1.7-5.4 3-6 6.5-.4 2.3-1 4.6-1.5 7-2.9 13.2-4.2 26.4-2.5 39.9 1.7 13.1 9.2 21.3 21 26.3 2.4 1 4.9 1.9 7.5 2.9-2.1.9-3.9 1.9-5.8 2.3-10.2 2.5-20.5 4.9-30.8 7.1-1.9.4-4.9.7-5.9-.3-6.4-6.5-8.9-14.8-8.3-23.7.7-9.6 2.1-19.2 3.9-28.6 2.2-11.5 6.1-22.5 11.7-32.9.7-1.3 2-3.1 1.6-4.1-1.8-4.6-4.5-8.9-6.2-13.6-2-5.7-4.2-11.6-1.2-17.8.1-.1-.3-.5-.5-.8 7.6.7 12.8 5.3 17.7 10.2-1.3-8.5-2.6-17.2-3.9-25.8 0-.3-.2-.7-.4-.9-6.7-5.5-13.3-11.2-17-19.2-2.6-5.7-4.3-11.8-6.3-17.7-.6-1.6.2-3.4-2.2-4.8-5.9-3.5-10.3-8.6-10.3-16 0-1.8 1.2-5 2.2-5.1 8.3-1.2 16.4-.1 23.8 4.2 2.4 1.4 4.9 2.7 8.1 4.4-.4-8.8-.8-16.2-1.2-23.6-4.2.9-8.6.9-11.5-2-3.3-3.3-5.4-7.8-7.9-11.8-1.1-1.7-2-3.6-3.5-6.4-3.8 10.3-7.4 19.9-10.8 29.1-.3-.6-1.1-1.7-1.5-2.9-3.5-10-2.8-20.2-1.1-30.3 1.2-7.4 4.3-14.6 3.1-22.4-.2-1.1.2-2.3.3-3.4-22.1 17.6-38.8 38.4-42.9 67.4-4 28-2.8 54.8 13.5 79.1-36.3-13.8-53-48.6-58.3-84.1-3 8-15 16.3-22.4 16.6v-.2c2.1-2.9 11.1-10.6 7-30.2-1.3-10.7-4.1-21.2-5.1-31.9-1-10.9-1-21.9-.5-32.9.3-11.6 3.8-22.7 8.6-33.2 5.7-12.5 13.5-23.8 23-33.6 5.6-5.8 11.9-11 18.2-16.1 8.6-6.8 17.7-12.9 28.2-16.5 5.1-1.9 10.4-3 15.7-4.5zm96.4 221.9c-.4.9-1.2 2-1.1 3 .5 7.6 1.2 15.2 2 22.7.2 2.1 0 4.8 3.3 5.5 3.3.7 6.6 1.8 9.9 2.6.3.1.9-.1 1.1-.4 3.8-4.8 7.5-9.6 10.9-14-8.4-6.1-17.1-12.6-26.1-19.4zm-23.1-42.5v6.3c1.9-2 3.6-3.9 5.3-5.7-1.7-.2-3.5-.4-5.3-.6z" />
</svg>
</div>
</div>
자세히 살펴보면 배경이 위로 이동하고, 앨리스는 빙글빙글 돌면서 도는 동작과 시차를 두고 색상도 변합니다. 이 튜토리얼에서는 오직 '앨리스' 한 명에게만 집중할게요! 앨리스의 애니메이션을 제어하는 단순화된 CSS 코드는 아래와 같습니다.
#alice {
animation: alice-tumbling infinite 3s linear;
}
@keyframes alice-tumbling {
0% {
color: black;
transform: rotate(0) translate3d(-50%, -50%, 0);
}
30% {
color: #431236;
}
100% {
color: black;
transform: rotate(360deg) translate3d(-50%, -50%, 0);
}
}
이 CSS는 앨리스의 색상과 3D 회전 변형을 3초 동안 일정한 속도(linear)로 끝없이 반복(infinite) 시킵니다. @keyframes 블록을 보시면 매 루프의 30% 지점(약 0.9초)에서 앨리스의 색상이 검은색에서 깊은 버건디 색상으로 변했다가, 루프가 끝날 때쯤 다시 검은색으로 돌아오는 것을 알 수 있습니다.
이제 이 똑같은 애니메이션을 Web Animations API를 이용해 구현해 보겠습니다. 준비되셨나요?
우리가 가장 먼저 해야 할 일은 CSS의 @keyframes 블록에 대응하는 키프레임 객체(Keyframe Object)를 자바스크립트로 만드는 것입니다.
const aliceTumbling = [
{ transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
{ color: "#431236", offset: 0.3 },
{ transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
];
여기서는 여러 개의 객체를 감싸고 있는 배열(Array)을 사용했습니다. 배열 안의 각 객체는 원본 CSS에서 작성했던 각각의 퍼센트(key) 지점들을 나타냅니다.
그런데 CSS와는 큰 차이점이 하나 있어요! WAAPI는 각 키프레임이 전체 애니메이션의 몇 퍼센트 시점에서 나타나야 하는지 명시적으로 알려줄 필요가 없습니다. 우리가 넘겨준 키 객체의 개수에 따라 브라우저가 애니메이션을 자동으로 균등하게 나누어 주기 때문이죠. 즉, 객체가 3개인 배열을 넘겨준다면 가운데 있는 객체는 별다른 설정이 없는 한 자동으로 애니메이션 루프의 50% 지점에서 실행됩니다.
만약 다른 키프레임들과 간격을 임의로 명시하고 싶다면, 객체 안에 콤마(,)를 찍고 offset 속성을 직접 적어주면 됩니다. 위의 예제에서 앨리스의 색상이 50%가 아닌 30% 지점에서 변하게 하기 위해 offset: 0.3이라고 명시해 준 것을 볼 수 있습니다.
또한 주의할 점이 있습니다! 애니메이션의 시작과 끝 상태를 표현하기 위해 키프레임 리스트 안에는 최소 두 개의 항목이 있어야 합니다. 만약 항목이 딱 한 개뿐이라면 일부 브라우저에서는 Element.animate() 메서드가 NotSupportedError DOMException 에러를 뿜어낼 수 있습니다.
요약하자면, 특별히 offset을 지정해주지 않으면 키프레임들은 기본적으로 똑같은 간격으로 배치된다는 점! 정말 직관적이고 편리하죠?
키프레임을 만들었다면, 이제 앨리스 애니메이션의 지속 시간과 반복 횟수 등을 설정할 타이밍 객체도 만들어야 합니다.
const aliceTiming = {
duration: 3000,
iterations: Infinity,
};
CSS로 표현할 때와 비교하면 약간의 차이점이 눈에 띄실 겁니다.
setTimeout()이나 Window.requestAnimationFrame()처럼 WAAPI는 오직 밀리초만 취급합니다.iteration-count가 아니라 iterations입니다.💡 참고: > CSS 애니메이션과 WAAPI에서 사용하는 용어에는 작고 미묘한 차이들이 있습니다. 예를 들어 WAAPI에서는
"infinite"라는 문자열 대신 자바스크립트의 전역 속성인Infinity를 사용합니다. 그리고 CSS의timing-function대신easing이라는 속성명을 씁니다.
위 코드에서 우리가 별도로easing값을 적지 않은 이유가 궁금하신가요? CSS 애니메이션에서animation-timing-function의 기본값은ease지만, WAAPI의 기본값은linear이기 때문입니다. 지금 우리가 원하는 속도가 바로 일정하게 돌아가는linear속도니까 굳이 적어줄 필요가 없는 것이죠!
이제 준비해 둔 두 가지 조각을 Element.animate() 메서드로 하나로 합쳐서 마법을 부려볼 시간입니다!
document.getElementById("alice").animate(aliceTumbling, aliceTiming);
짜잔! 이렇게 한 줄만 작성해도 애니메이션이 훌륭하게 실행됩니다.
animate() 메서드는 CSS를 통해 애니메이션을 줄 수 있는 모든 DOM 요소에서 호출할 수 있습니다. 위에서는 변수를 선언해서 깔끔하게 넣었지만, 변수를 따로 만들지 않고 직접 값들을 때려 넣는(inline) 방법으로도 작성할 수 있습니다.
document.getElementById("alice").animate(
[
{ transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
{ color: "#431236", offset: 0.3 },
{ transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
],
{
duration: 3000,
iterations: Infinity,
},
);
더 나아가, 만약 반복 횟수(iterations) 같은 세세한 옵션이 필요 없고 단지 애니메이션의 지속 시간만 지정하고 싶다면 (애니메이션은 기본적으로 한 번만 실행되니까요), 옵션 객체 대신 그냥 밀리초 숫자 하나만 딸랑 넘겨줘도 된답니다!
document.getElementById("alice").animate(
[
{ transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
{ color: "#431236", offset: 0.3 },
{ transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
],
3000,
);
💡 강사의 팁: > 리액트나 바닐라 자바스크립트 프로젝트에서 동적으로 스타일 요소가 변하는 애니메이션을 만들어야 할 때,
animate()메서드는 정말 치트키입니다! DOM 트리를 더럽히는 애니메이션 전용 CSS 클래스들을 잔뜩 만들 필요가 없어지죠.
WAAPI로 CSS 애니메이션을 똑같이 재현할 수 있다는 것도 멋지지만, 이 API가 정말 압도적으로 유용한 순간은 바로 애니메이션 재생을 직접 제어할 때입니다.
WAAPI는 재생 제어를 위해 굉장히 유용한 여러 메서드들을 제공합니다. 하얀 토끼를 쫓아가는 예제(Follow the White Rabbit)를 통해 애니메이션을 어떻게 일시정지하고 다시 재생하는지 알아볼게요.
아래 예제에서 하얀 토끼는 토끼굴 아래로 뛰어 들어가는 애니메이션을 가지고 있는데, 이건 오직 사용자가 토끼를 클릭했을 때만 실행되도록 만들어져 있습니다.
<div class="wrapper">
<div class="page">
<div class="background"></div>
<div id="rabbit">Click the rabbit's ears!</div>
<div class="foreground"></div>
<p>
She was just in time to see him pop down a hole between a great tree's
roots.
</p>
</div>
</div>
#rabbit {
background: url("[https://developer.mozilla.org/shared-assets/images/examples/web-animations/park5_rabbit.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/park5_rabbit.png)")
0 0 / 100% 100%;
cursor: pointer;
position: absolute;
top: 15%;
left: 60%;
width: 14.64844%;
padding-top: 31.00586%;
}
body {
background: black;
}
.wrapper {
max-width: 133.33vh;
margin: 0 auto;
}
.page {
background: #431236;
height: 0;
overflow: hidden;
padding-top: 75%;
position: relative;
text-indent: 100%;
white-space: nowrap;
}
.foreground {
height: 100%;
background: url("[https://developer.mozilla.org/shared-assets/images/examples/web-animations/bg_park5_2.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/bg_park5_2.png)")
no-repeat 100% 100% / 100% auto;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
pointer-events: none;
}
.background {
background: url("[https://developer.mozilla.org/shared-assets/images/examples/web-animations/bg_park5_1.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/bg_park5_1.png)")
no-repeat 0 0 / 100% auto;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
평소대로 animate() 메서드를 사용해 토끼에게 애니메이션을 적용해 줍니다.
const whiteRabbit = document.getElementById("rabbit");
const rabbitDownAnimation = whiteRabbit.animate(
[{ transform: "translateY(0%)" }, { transform: "translateY(100%)" }],
{ duration: 3000, fill: "forwards" },
);
다만 여기서 큰 차이가 있습니다. Element.animate() 메서드는 호출되는 즉시 애니메이션을 실행해버립니다. 사용자가 미처 클릭해 보기도 전에 토끼가 굴로 사라져 버리는 것을 막기 위해, 우리는 애니메이션을 정의하자마자 곧바로 Animation.pause() 메서드를 호출해 줘야 합니다.
rabbitDownAnimation.pause();
💡 참고: > 또 다른 방법으로는
animate()메서드 대신Animation()생성자를 직접 사용해서rabbitDownAnimation을 정의할 수도 있습니다. 이렇게 생성자로 만든 애니메이션은play()메서드를 명시적으로 호출하기 전까지는 절대 자동으로 시작되지 않습니다. 상황에 따라 더 편한 방법을 고르시면 됩니다!
이제 우리가 원할 때 언제든지 Animation.play() 메서드를 호출해서 토끼를 뛰게 만들 수 있습니다. 우리는 토끼를 클릭하는 액션과 애니메이션을 연결하고 싶으므로 아래와 같이 코드를 작성할 수 있습니다.
whiteRabbit.addEventListener("click", downHeGoes);
whiteRabbit.addEventListener("touchstart", downHeGoes);
function downHeGoes(event) {
whiteRabbit.removeEventListener("click", downHeGoes);
whiteRabbit.removeEventListener("touchstart", downHeGoes);
rabbitDownAnimation.play();
}
이제 사용자가 토끼를 마우스로 클릭하거나 손가락으로 터치하면, downHeGoes 함수가 실행되고 일시정지 되어 있던 토끼 애니메이션이 멋지게 재생됩니다.
일시정지와 재생 외에도, 아래와 같은 강력한 애니메이션 메서드들을 사용할 수 있습니다!
Animation.finish() - 애니메이션을 즉시 끝 지점으로 건너뛰게 만듭니다.Animation.cancel() - 애니메이션을 완전히 중단시키고, 애니메이션으로 인해 적용되었던 시각 효과들을 제거합니다.Animation.reverse() - 애니메이션의 재생 속도(Animation.playbackRate)를 음수로 설정해서 애니메이션을 반대 방향(뒤로 감기)으로 재생하게 만듭니다.우선 playbackRate에 대해 알아봅시다. 음수 튜닝된 playbackRate는 애니메이션을 거꾸로 돌립니다.
'거울 나라의 앨리스(Through the Looking-Glass)'에서 앨리스는 제자리에 있기 위해 끊임없이 뛰어야 하고, 앞으로 가기 위해서는 무려 두 배나 빨리 뛰어야 하는 기이한 세계에 떨어집니다. '붉은 여왕의 달리기(Red Queen's Race)' 예제에서 앨리스와 여왕이 제자리에 머물기 위해 달리고 있는 코드를 살펴볼까요?
<div class="wrapper">
<div class="sky"></div>
<div class="earth">
<div id="red-queen-and-alice">
<img
id="red-queen-and-alice-sprite"
src="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/sprite_running-alice-queen_small.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/sprite_running-alice-queen_small.png)"
srcset="
[https://developer.mozilla.org/shared-assets/images/examples/web-animations/sprite_running-alice-queen.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/sprite_running-alice-queen.png) 2x
"
alt="Alice and the Red Queen running to stay in place." />
</div>
</div>
<div class="scenery" id="foreground1">
<img
id="palm3"
src="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/palm3_small.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/palm3_small.png)"
srcset="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/palm3.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/palm3.png) 2x"
alt="" />
</div>
<div class="scenery" id="foreground2">
<img
id="bush"
src="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/bush_small.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/bush_small.png)"
srcset="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/bush.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/bush.png) 2x"
alt="" />
<img
id="w_rook_upright"
src="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/w_rook_upright_small.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/w_rook_upright_small.png)"
srcset="
[https://developer.mozilla.org/shared-assets/images/examples/web-animations/w_rook_upright.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/w_rook_upright.png) 2x
"
alt="" />
</div>
<div class="scenery" id="background1">
<img
id="r_pawn_upright"
src="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/r_pawn_upright_small.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/r_pawn_upright_small.png)"
srcset="
[https://developer.mozilla.org/shared-assets/images/examples/web-animations/r_pawn_upright.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/r_pawn_upright.png) 2x
"
alt="" />
<img
id="w_rook"
src="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/w_rook_small.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/w_rook_small.png)"
srcset="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/w_rook.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/w_rook.png) 2x"
alt="" />
<img
id="palm1"
src="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/palm1_small.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/palm1_small.png)"
srcset="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/palm1.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/palm1.png) 2x"
alt="" />
</div>
<div class="scenery" id="background2">
<img
id="r_pawn"
src="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/r_pawn_small.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/r_pawn_small.png)"
srcset="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/r_pawn.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/r_pawn.png) 2x"
alt="" />
<img
id="r_knight"
src="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/r_knight_small.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/r_knight_small.png)"
srcset="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/r_knight.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/r_knight.png) 2x"
alt="" />
<img
id="palm2"
src="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/palm2_small.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/palm2_small.png)"
srcset="[https://developer.mozilla.org/shared-assets/images/examples/web-animations/palm2.png](https://developer.mozilla.org/shared-assets/images/examples/web-animations/palm2.png) 2x"
alt="" />
</div>
</div>
* {
user-select: none;
}
img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.scenery {
width: 100%;
height: 50%;
position: absolute;
bottom: 0;
left: 0;
}
#foreground1,
#foreground2 {
z-index: 1;
}
#foreground2,
#background2 {
transform: translateX(100%);
}
#palm3 {
top: 0;
left: 10%;
}
#w_rook_upright {
top: 30%;
left: 75%;
}
#r_pawn {
top: 10%;
left: 15%;
}
#w_rook {
top: 10%;
left: 80%;
}
#r_pawn_upright {
top: 5%;
left: 30%;
}
#r_knight {
top: 0;
left: 70%;
}
#palm2 {
top: -15%;
left: 90%;
}
#palm1 {
top: -15%;
left: 40%;
}
#bush {
top: 55%;
left: 20%;
}
#red-queen-and-alice {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -50%);
overflow: hidden;
width: 80%;
max-width: 450px;
z-index: 1;
}
#red-queen-and-alice::before {
content: " ";
display: block;
padding-top: 87%;
}
#red-queen-and-alice img {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.sky,
.earth {
position: absolute;
left: 0;
height: 50vh;
width: 100%;
}
.earth {
background: #eb125d
url("[https://developer.mozilla.org/shared-assets/images/examples/web-animations/bg_earth.jpg](https://developer.mozilla.org/shared-assets/images/examples/web-animations/bg_earth.jpg)") repeat-x 0
0 / 100% auto;
bottom: 0;
}
.sky {
background: #246e89
url("[https://developer.mozilla.org/shared-assets/images/examples/web-animations/bg_sky.jpg](https://developer.mozilla.org/shared-assets/images/examples/web-animations/bg_sky.jpg)") repeat-x
100% 100% / auto 100%;
top: 0;
}
html,
body {
width: 100%;
height: 100%;
}
.wrapper {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
}
const background1 = document.getElementById("background1");
const background2 = document.getElementById("background2");
const foreground1 = document.getElementById("foreground1");
const foreground2 = document.getElementById("foreground2");
const redQueenAliceSprite = document.getElementById(
"red-queen-and-alice-sprite",
);
/* Background animations */
const sceneryFrames = [
{ transform: "translateX(100%)" },
{ transform: "translateX(-100%)" },
];
const sceneryTimingBackground = {
duration: 36000,
iterations: Infinity,
};
const sceneryTimingForeground = {
duration: 12000,
iterations: Infinity,
};
const background1Movement = background1.animate(
sceneryFrames,
sceneryTimingBackground,
);
background1Movement.currentTime =
background1Movement.effect.getComputedTiming().duration / 2;
const background2Movement = background2.animate(
sceneryFrames,
sceneryTimingBackground,
);
const foreground1Movement = foreground1.animate(
sceneryFrames,
sceneryTimingForeground,
);
foreground1Movement.currentTime =
foreground1Movement.effect.getComputedTiming().duration / 2;
const foreground2Movement = foreground2.animate(
sceneryFrames,
sceneryTimingForeground,
);
const spriteFrames = [
{ transform: "translateY(0)" },
{ transform: "translateY(-100%)" },
];
const redQueenAlice = redQueenAliceSprite.animate(spriteFrames, {
easing: "steps(7, end)",
direction: "reverse",
duration: 600,
playbackRate: 1,
iterations: Infinity,
});
기계로 만든 체스 말과는 달리 어린아이인 앨리스는 금방 지쳐버리겠죠? 그래서 앨리스의 달리기 속도는 서서히 느려집니다. 우리는 playbackRate에 점점 감소하는 수치를 적용해서 속도를 늦출 수 있습니다. 이때 playbackRate를 직접 수정하는 대신 updatePlaybackRate() 메서드를 사용하면, 속도 변화가 뚝 끊기지 않고 아주 부드럽고 매끄럽게 업데이트됩니다.
setInterval(() => {
// Make sure the playback rate never falls below .4
if (redQueenAlice.playbackRate > 0.4) {
redQueenAlice.updatePlaybackRate(redQueenAlice.playbackRate * 0.9);
}
adjustBackgroundPlayback();
}, 1000);
하지만 안심하세요! 여러분이 화면을 클릭하거나 터치해서 응원해주면, playbackRate 수치가 곱해지면서 앨리스가 다시 빠르게 달리게 할 수 있답니다.
function goFaster() {
// 화면을 클릭하거나 탭해서 속도를 높여주세요!
redQueenAlice.updatePlaybackRate(redQueenAlice.playbackRate * 1.1);
adjustBackgroundPlayback();
}
document.addEventListener("click", goFaster);
document.addEventListener("touchstart", goFaster);
화면 뒤에 있는 배경 요소들 역시 클릭이나 탭의 영향을 받습니다. 왜냐하면 배경의 재생 속도 역시 앨리스의 달리기 속도에 기반하여 계산되도록 만들어져 있거든요! 앨리스와 여왕이 두 배로 빨리 달리게 만들면 무슨 일이 벌어질까요? 반대로 지쳐서 느려지게 놔두면 어떻게 될까요? 코드를 보며 직접 상상해보세요.
/* 앨리스는 너무 빨리 지쳐요!
몇 초마다 그들의 재생 속도를 줄여서 조금씩 느려지게 만듭니다.
*/
const sceneries = [
foreground1Movement,
foreground2Movement,
background1Movement,
background2Movement,
];
function adjustBackgroundPlayback() {
// 앨리스와 여왕이 0.8~1.2 사이의 속도로 달리면 배경은 멈춰있는 것처럼 보입니다.
// 하지만 0.8 미만으로 떨어지면 제자리를 유지하지 못하고 배경이 오히려 뒤로 밀려나죠!
if (redQueenAlice.playbackRate < 0.8) {
sceneries.forEach((anim) => {
anim.updatePlaybackRate(-redQueenAlice.playbackRate / 2);
});
} else if (redQueenAlice.playbackRate > 1.2) {
sceneries.forEach((anim) => {
anim.updatePlaybackRate(redQueenAlice.playbackRate / 2);
});
} else {
sceneries.forEach((anim) => {
anim.updatePlaybackRate(0);
});
}
}
adjustBackgroundPlayback();
요소에 애니메이션을 적용할 때, 아주 흔하게 겪는 상황이 하나 있습니다. 바로 애니메이션이 완전히 끝난 후 도착한 최종 상태의 스타일을 영구적으로 유지하고 싶을 때입니다.
많은 사람들이 이럴 때 애니메이션의 fill mode(채우기 모드)를 forwards로 설정하는 편법을 씁니다. 하지만, 단순히 애니메이션의 최종 효과를 무한정 지속하기 위해 fill mode를 남발하는 것은 다음과 같은 두 가지 이유로 강력히 권장하지 않습니다.
대신에, 이럴 땐 더 완벽한 접근 방식인 Animation.commitStyles() 메서드를 사용하는 것이 좋습니다. 이 메서드는 애니메이션이 렌더링한 최종 계산된 스타일 값을 타겟 요소의 인라인 style 속성에 도장 찍듯 콱 박아버립니다. 이 작업이 끝나고 나면 애니메이션을 제거해도 스타일이 남고, 원할 때 다른 인라인 스타일처럼 정상적으로 디자인을 덮어쓸 수 있죠!
💡 강사의 팁: > "왜 내 화면이 갈수록 버벅거리지?" 프로젝트를 만들 때 꼭 한 번쯤 마주하는 현상이죠. 무한 유지되는
forwards애니메이션들이 DOM에 수백 개 쌓이면 정말 끔찍한 성능 저하가 일어납니다. 영구적으로 모습을 바꿔야 한다면 반드시commitStyles()를 호출하고 애니메이션 객체 자체는 종료시켜서 브라우저가 숨을 돌리게 해 주세요.
하나의 DOM 요소에 엄청나게 많은 수의 애니메이션을 트리거하는 경우도 있을 수 있습니다. 만약 이 모든 애니메이션이 무한정으로 유지되는 (예: forwards로 채워진) 애니메이션이라면, 내부적으로 어마어마하게 긴 애니메이션 목록이 쌓이게 되고 이는 곧 치명적인 메모리 누수(Memory Leak)를 일으킬 겁니다.
이러한 사태를 막기 위해, 최신 브라우저들은 개발자가 이 애니메이션을 굳이 보존하겠다고 명시하지 않는 이상 더 새로운 애니메이션에 의해 덮어씌워진 채우기(filling) 애니메이션을 자동으로 삭제해버립니다. 놀랍도록 똑똑하죠?
애니메이션은 다음 조건들을 '모두' 만족할 때 쓰레기통으로 들어갑니다.
fill 값이 forwards, 역방향 재생일 땐 backwards, 혹은 both로 설정된 경우).fill 설정 때문에 스타일 자체는 여전히 렌더링 되고 있는 상황이어야 합니다).DocumentTimeline에서는 항상 참입니다. 하지만 스크롤 위치에 반응하는 scroll-timeline 같은 녀석은 마우스를 위로 올리면 거꾸로 돌아갈 수도 있죠.)AnimationEffect가 만들어내는 모든 스타일링 효과가 위의 조건들을 완벽하게 만족하는 "다른 새로운 애니메이션"에 의해 덮어씌워졌을 때. (보통 두 애니메이션이 동일한 요소의 동일한 CSS 속성을 건드리면 나중에 덮어씌워진 것이 이전 것을 이기게 됩니다.)위의 첫 4가지 조건들은 "자바스크립트가 개입하지 않는 한, 이 애니메이션의 효과나 상태가 앞으로 절대 변하거나 끝날 일이 없다"라는 것을 증명합니다. 마지막 조건은 "이 애니메이션은 완전히 대체되었으므로 더 이상 요소의 스타일에 아무런 실제적인 영향을 끼치지 못한다"라는 것을 확인시켜 줍니다. 즉, 쓸모가 없어졌다는 판정을 내리는 것이죠.
브라우저에 의해 애니메이션이 이렇게 자동으로 폐기될 때는 애니메이션의 remove 이벤트가 발생합니다.
만약 브라우저가 여러분의 애니메이션을 마음대로 지우는 것을 원치 않는다면 어떻게 해야 할까요? 간단합니다. 애니메이션의 persist() 메서드를 호출해주면 됩니다. "제발 지우지 마!" 라고 브라우저에게 선언하는 셈이죠.
애니메이션의 생존 여부는 replaceState 속성에서 확인할 수 있습니다. 만약 브라우저에 의해 삭제되었다면 removed 값을 가지고, 여러분이 persist()를 호출해서 살아남았다면 persisted 값을, 그 외 평범하게 활성화되어 있는 중이라면 active 값을 가지게 됩니다.
playbackRate가 어떤 유용한 방식으로 활용될 수 있을지 한번 상상해 보세요.
예를 들어, 전정기관(Vestibular)에 장애가 있어 화면이 휙휙 바뀌면 심하게 어지러움을 느끼는 사용자를 위해 사이트 전체의 애니메이션 속도를 극단적으로 낮출 수 있는 기능을 제공한다면 어떨까요? 이걸 순수 CSS로 구현하려면 수백 개의 CSS 룰 안에서 모든 duration 변수를 일일이 재계산하고 교체해줘야 할 겁니다. 생각만 해도 아찔하죠.
하지만 Web Animations API를 활용하면 얘기가 달라집니다. Document.getAnimations() 메서드를 사용해 페이지 내에 돌고 있는 모든 애니메이션을 배열로 싹 쓸어 모은 다음, 반복문을 돌리면서 각 애니메이션의 playbackRate를 반토막 내버리면 그만입니다!
document.getAnimations().forEach((animation) => {
animation.updatePlaybackRate(animation.playbackRate * 0.5);
});
어떤가요? WAAPI를 쓰면 단지 이 작은 속성 하나만 바꾸면 모든 것이 해결됩니다.
또한 CSS 애니메이션만으로는 구현하기 정말 까다로운 것 중 하나가 "다른 애니메이션의 값을 참조해서 의존성을 만드는 것"입니다. 예를 들어 '앨리스 커지기/작아지기 게임'에서 케이크를 먹는 애니메이션의 지속 시간(duration)을 유심히 살펴보셨나요?
document.getElementById("eat-me-sprite").animate([], {
duration: aliceChange.effect.getComputedTiming().duration / 2,
});
무슨 일이 일어나고 있는지 파악하기 위해 앨리스가 커지고 작아지는 애니메이션 본체를 한번 확인해 보겠습니다.
const aliceChange = document
.getElementById("alice")
.animate(
[
{ transform: "translate(-50%, -50%) scale(.5)" },
{ transform: "translate(-50%, -50%) scale(2)" },
],
{
duration: 8000,
easing: "ease-in-out",
fill: "both",
},
);
이 aliceChange 애니메이션은 8초에 걸쳐 앨리스가 원래 사이즈의 절반 크기에서 무려 두 배 크기로 변하게 만듭니다. 우리는 이 애니메이션을 선언하자마자 일단 일시정지 시켜둡니다.
aliceChange.pause();
만약 일시정지만 시켜놓고 그대로 방치한다면, 앨리스는 이미 병 안에 든 약을 몽땅 다 마셔버린 것처럼 아주 쪼그만 사이즈(scale .5)에서 멈춰있게 될 겁니다. 우리는 앨리스의 시작 상태를 평범한 사이즈로 보여주고 싶어요. 즉, 애니메이션의 "재생 헤드(playhead)"를 전체 길이의 딱 중간인 4초 시점에 멈춰두고 싶은 것이죠! 이렇게 하려면 애니메이션의 Animation.currentTime 속성을 4000밀리초로 지정하면 됩니다.
aliceChange.currentTime = 4000;
그런데 코드를 작성하다 보면 앨리스 애니메이션의 전체 지속 시간(duration)을 마음이 바뀌어서 8초에서 10초로, 12초로 자주 바꿀 수도 있잖아요? 그때마다 currentTime 수치도 일일이 계산해서 수정해줘야 할까요? 아닙니다! 앨리스 애니메이션 객체의 속성인 Animation.effect를 직접 참조해서 동적으로 값을 받아오면 됩니다. 이 effect 객체 안에는 앨리스에게 현재 활성화된 모든 애니메이션 세부 정보가 담겨있거든요.
aliceChange.currentTime = aliceChange.effect.getComputedTiming().duration / 2;
effect 객체를 통해 우리는 언제든 키프레임과 타이밍 정보에 접근할 수 있습니다. 위 코드에서 aliceChange.effect.getComputedTiming()을 호출하면 앨리스의 모든 타이밍 정보가 담긴 객체를 반환해주고, 그 안에서 duration 값을 쉽게 꺼내 쓸 수 있습니다. 전체 듀레이션을 반으로 뚝 잘라(divide by 2) 타임라인의 한가운데로 재생 헤드를 위치시키면 앨리스가 완벽하게 원래 크기로 보입니다. 이제 남은 건 이 재생 헤드를 앞이나 뒤로 굴려주기만 하면 앨리스를 크게 만들거나 작게 만들 수 있다는 사실! 정말 멋진 논리죠?
이러한 로직은 앨리스가 마실 약병과 먹을 케이크의 애니메이션 길이를 설정할 때도 똑같이 적용할 수 있습니다.
const drinking = document
.getElementById("liquid")
.animate([{ height: "100%" }, { height: "0" }], {
fill: "forwards",
duration: aliceChange.effect.getComputedTiming().duration / 2,
});
drinking.pause();
이제 앨리스, 약병, 케이크 이 세 개의 애니메이션 지속 시간이 오로지 단 하나의 데이터 원천(앨리스의 duration)에 의존하게 되었습니다. 만약 기획자가 "애니메이션이 너무 느린 것 같아요"라고 하면 이제 앨리스의 숫자 단 하나만 쓱 고치면 됩니다.
이밖에도 Web Animations API를 통해 "현재 애니메이션이 어디까지 진행됐지?"라는 시간 정보를 파악할 수 있습니다. 앨리스 게임은 케이크를 다 먹어 치우거나 약병을 다 마시면 끝이 납니다. 이때 앨리스의 크기가 어떠냐에 따라 게임 오버 화면이 달라집니다. 너무 커져서 작은 문을 못 들어가게 될지, 아니면 문을 열 열쇠가 있는 테이블조차 닿지 못할 만큼 작아져 버렸는지 말이죠.
우리는 앨리스가 거인 상태인지 개미 상태인지를 알기 위해 현재 시간인 currentTime을 전체 동작 시간인 activeDuration으로 나누어서 진행도를 계산할 수 있습니다!
const endGame = () => {
// 앨리스의 타임라인 재생 헤드 위치 가져오기
const alicePlayhead = aliceChange.currentTime;
const aliceTimeline = aliceChange.effect.getComputedTiming().activeDuration;
// 앨리스 및 다른 애니메이션 멈추기
stopPlayingAlice();
// 애니메이션이 3등분 중 어디에 속해 있는지 파악하기
const aliceHeight = alicePlayhead / aliceTimeline;
if (aliceHeight <= 0.333) {
// 앨리스가 작아졌어요!
// …
} else if (aliceHeight >= 0.666) {
// 앨리스가 커졌어요!
// …
} else {
// 앨리스의 크기는 크게 변하지 않았어요.
// …
}
};
기존 CSS Animations나 CSS Transitions가 이벤트를 쏘아 올리듯, Web Animations API에서도 동일하게 이벤트 리스너(Event listeners)를 처리할 수 있습니다.
onfinish - 애니메이션 재생이 완전히 끝났을 때(finish 이벤트) 호출되는 핸들러입니다. 물론 수동으로 finish() 메서드를 직접 쏴서 이벤트를 강제 발생시킬 수도 있죠.oncancel - 애니메이션이 도중에 취소되었을 때(cancel 이벤트) 호출되는 핸들러입니다. 수동으로 cancel()을 호출했을 때 불립니다.앞서 만든 게임에서 케이크, 약병, 앨리스의 애니메이션이 종료되면 endGame 함수가 실행되도록 콜백을 달아볼까요?
// 케이크를 다 먹거나 약병을 다 비우면 실행
nommingCake.onfinish = endGame;
drinking.onfinish = endGame;
// 앨리스의 크기 변화 애니메이션이 끝나면 실행
aliceChange.onfinish = endGame;
더 나아가, WAAPI는 콜백보다 훨씬 세련된 비동기 처리 방식인 Promise 객체도 함께 제공합니다! 바로 finished 속성인데요, 이 프로미스는 애니메이션이 완전히 무사히 종료되면 스스로를 이행(Resolve)하고, 누군가에 의해 강제로 취소되면 거부(Reject) 상태로 빠집니다.
💡 강사의 팁: > 3개의 애니메이션을 연달아 부드럽게 실행해야 할 때를 상상해 보세요.
onfinish콜백만 쓴다면 흔히 말하는 '콜백 지옥(Callback Hell)'에 빠질 확률이 높습니다. 하지만.finished프로미스를 활용하고async/await문법을 쓰면,await animation1.finished; await animation2.finished;단 두 줄만으로 복잡한 연계 애니메이션을 정말 예쁘고 동기적인 코드처럼 작성할 수 있답니다. 정말 매력적이죠?
지금까지 살펴본 내용들이 Web Animations API를 다루기 위해 꼭 알아야 할 가장 핵심적인 기능들입니다. 이 정도라면 이제 여러분도 "토끼굴로 뛰어들어갈" 준비가 되신 것 같네요! 자바스크립트로 브라우저를 지휘하며 여러분만의 놀라운 애니메이션 실험을 맘껏 펼쳐보시길 바랍니다!
안녕하세요! 저번 시간에 이어 이번에는 Web Animations API(이하 WAAPI)의 핵심 '개념'을 다루는 공식 문서를 들고 오셨네요.
이 문서에서는 코드를 직접 짜기 전에, 브라우저가 애니메이션을 어떻게 이해하고 처리하는지 그 원리를 설명해주고 있어요. 원리를 알아야 응용력도 생기는 법이죠! 리액트(React)나 넥스트제이에스(Next.js)처럼 상태 관리가 중요한 프레임워크 환경에서 복잡한 애니메이션을 제어할 때, 이 개념들이 아주 단단한 기초 체력이 되어줄 겁니다.
딱딱한 직역은 피하고, 이해하기 쉽게 살을 붙여가며 번역해 드릴 테니 천천히 따라와 주세요!
Web Animations API(WAAPI)는 자바스크립트 개발자들에게 브라우저에 내장된 애니메이션 엔진에 직접 접근할 수 있는 권한을 제공합니다. 또한 여러 브라우저 간에 애니메이션이 어떻게 구현되어야 하는지에 대한 표준을 설명하고 있죠.
이 문서에서는 WAAPI 이면에 있는 중요한 핵심 개념들을 소개해 드릴 거예요. API가 어떻게 작동하는지 이론적으로 먼저 이해하고 나면, 훨씬 더 효과적으로 써먹을 수 있거든요. 실제 코드로 어떻게 활용하는지 배우고 싶다면 자매 문서인 Web Animations API 사용하기(Using the Web Animations API)를 확인해 보세요.
Web Animations API는 '선언적인(declarative) CSS 애니메이션 및 트랜지션'과 '동적인(dynamic) 자바스크립트 애니메이션' 사이의 빈틈을 완벽하게 메워주는 역할을 합니다.
이게 무슨 뜻이냐면, CSS처럼 미리 정해진 상태(A에서 B로)로 변하는 애니메이션을 쉽게 만들고 조작할 수도 있고, 더 나아가 변수, 반복문, 콜백 함수 등을 사용해서 사용자의 입력이나 변화하는 상황에 즉각적으로 반응하는 상호작용형(interactive) 애니메이션도 만들 수 있다는 뜻이에요.
💡 강사의 핵심 보충 설명:
프론트엔드 개발을 하다 보면, 마우스를 올렸을 때 색이 변하는 단순한 효과는 CSS(transition,@keyframes)로 충분합니다. 하지만 "사용자가 버튼을 클릭하면 상태 값을 계산해서, 그 결과에 따라 요소가 튕기는 속도와 위치를 다르게 적용해라!" 같은 요구사항이 들어오면 CSS만으로는 불가능에 가깝죠.
예전에는 이걸 자바스크립트의requestAnimationFrame등으로 한 땀 한 땀 직접 그렸다면, 이제는 WAAPI를 통해 CSS 애니메이션의 부드러움과 자바스크립트의 강력한 논리를 하나로 합쳐서 사용할 수 있게 된 것입니다!
10여 년 전, SMIL(Synchronized Multimedia Integration Language, '스마일'이라고 발음해요)이라는 기술이 SVG에 처음으로 애니메이션을 도입했습니다. 그 당시 브라우저들이 신경 써야 할 애니메이션 엔진은 오직 이것 하나뿐이었죠. 주요 브라우저 5개 중 4개가 이 SMIL을 지원했지만, 치명적인 단점들이 있었어요. 오직 SVG 요소에만 애니메이션을 줄 수 있었고, CSS에서는 사용할 수도 없었으며, 구조가 너무 복잡해서 브라우저마다 구현 결과가 들쭉날쭉하기 일쑤였습니다.
그로부터 10년 후, 애플의 Safari 브라우저 팀이 CSS 애니메이션(CSS Animations)과 CSS 트랜지션(CSS Transitions) 명세를 세상에 내놓았습니다.
그러자 Internet Explorer 팀에서 모든 브라우저의 애니메이션 기능을 하나로 통합하고 표준화할 수 있는 애니메이션 API를 만들어 달라고 요청했어요. 이를 계기로 Mozilla Firefox와 Google Chrome 개발자들을 중심으로, 모든 것을 통합할 단 하나의 절대 반지 같은 애니메이션 명세를 만들기 위한 본격적인 노력이 시작되었습니다. 그것이 바로 Web Animations API의 탄생입니다.
이제 우리는 앞으로 나올 모든 미래의 애니메이션 명세들이 이 WAAPI 위에서 일관성을 유지하며 잘 어우러질 수 있도록 하는 든든한 기반을 갖게 되었습니다. 또한 브라우저들이 현재 사용 가능한 스펙들을 준수할 수 있도록 훌륭한 기준점 역할도 해주고 있죠.

Web Animations API는 크게 두 가지 모델을 기반으로 작동합니다. 하나는 '시간'을 다루는 타이밍(Timing) 모델이고, 다른 하나는 시간에 따른 '시각적인 변화'를 다루는 애니메이션(Animation) 모델입니다.
타이밍 모델은 정해진 타임라인(시간선)을 따라 우리가 지금 어느 시점에 와 있는지를 추적하고 기록합니다. 그리고 애니메이션 모델은 그 특정 시점에 애니메이션 요소가 정확히 어떤 모습으로 보여야 하는지를 결정하는 역할을 하죠.
💡 강사의 팁: 쉽게 비유하자면, 타이밍 모델은 일정한 속도로 똑딱거리는 '메트로놈(박자기)'이고, 애니메이션 모델은 그 박자에 맞춰 춤을 추는 '댄서'라고 생각하시면 이해가 쏙 되실 겁니다!
타이밍 모델은 WAAPI를 다루는 데 있어 가장 뼈대가 되는 부분입니다. 브라우저에 띄워진 각 문서(document)는 Document.timeline이라는 '마스터 타임라인'을 하나씩 가지고 있어요. 이 타임라인은 페이지가 로딩되는 순간부터 시작해서 무한대까지(혹은 브라우저 창을 닫을 때까지) 쭉 뻗어 나갑니다.
우리가 만든 애니메이션들은 각자의 지속 시간(duration)에 맞춰 이 거대한 타임라인 위에 배치됩니다. 각각의 애니메이션은 startTime이라는 속성을 통해 문서의 타임라인 내 특정 지점에 닻을 내리듯 고정되는데요, 이 지점이 바로 애니메이션의 재생이 시작되는 순간을 의미합니다.
애니메이션의 모든 재생 동작은 이 타임라인에 의존하게 됩니다.
가까운 미래에는 단순히 시간에 기반한 타임라인뿐만 아니라, 사용자의 제스처나 스크롤 위치를 기반으로 하는 타임라인, 혹은 부모-자식 관계의 타임라인까지 도입될 수 있습니다. Web Animations API가 열어줄 가능성은 정말 무궁무진해요!
💡 강사의 경험담: > 언급된 '스크롤 위치 기반 타임라인(
ScrollTimeline)'은 이미 실무에서 엄청난 화두입니다. 기존에는 스크롤 할 때마다 요소가 나타나게 하려면 자바스크립트로 스크롤 이벤트를 매번 감지해야 해서 성능 저하(버벅거림)가 심했어요. 하지만 브라우저가 타임라인 자체를 스크롤로 인식하게 되면, 스크롤 동작과 애니메이션이 프레임 단위로 완벽하게 동기화되어 소위 '애플 감성'의 부드러운 스크롤 애니메이션을 아주 쾌적하게 구현할 수 있답니다.
애니메이션 모델은 시간의 흐름(지속 시간)을 따라 일렬로 늘어선 '스냅샷(사진)들의 배열'이라고 상상하시면 됩니다. 각각의 스냅샷은 특정 시간에 요소가 어떤 모습이어야 하는지를 담고 있죠.

웹 애니메이션은 타임라인 객체(Timeline Objects), 애니메이션 객체(Animation Objects), 그리고 애니메이션 효과 객체(Animation Effect Objects)가 톱니바퀴처럼 함께 맞물려 돌아가며 구성됩니다. 개발자는 이 각기 다른 객체들을 레고 블록처럼 조립해서 나만의 멋진 애니메이션을 만들어낼 수 있어요.
타임라인 객체는 아주 유용한 속성인 currentTime을 제공합니다. 이 속성을 확인해보면 사용자가 웹 페이지를 연 지 얼마나 지났는지를 알 수 있어요. 즉, 페이지가 열린 시점부터 시작된 해당 문서 타임라인의 '현재 시간'을 알려주는 것이죠.
이 문서가 작성되고 있는 현재 시점을 기준으로는 타임라인 객체의 종류가 단 하나뿐입니다. 바로 활성화된 문서의 timeline에 기반한 것이죠. 하지만 미래에는 페이지의 전체 길이와 연동되는 ScrollTimeline 등 완전히 다른 형태의 타임라인 객체들을 만나볼 수 있을 것입니다.
Animation 객체들은 흔히 'DVD 플레이어'에 비유할 수 있습니다. 미디어 재생을 제어하기 위해 쓰이지만, 정작 재생할 미디어(DVD 알맹이)가 없으면 아무 일도 하지 못하는 기계장치인 셈이죠.
애니메이션 객체는 미디어로서 '애니메이션 효과(Animation Effects)'를 입력받는데, 더 구체적으로는 '키프레임 효과(Keyframe Effects)'를 받습니다(이건 바로 다음 섹션에서 설명할게요). 우리는 DVD 플레이어의 리모컨 버튼을 누르듯이, 애니메이션 객체에 내장된 메서드들을 사용해서 애니메이션을 재생(play)하거나 일시 정지(pause)하고, 원하는 시간대로 탐색(currentTime)할 수 있으며, 재생 방향을 바꾸거나(reverse) 재생 속도(playbackRate)까지 자유자재로 제어할 수 있습니다.

애니메이션 객체가 'DVD 플레이어'라면, 애니메이션 효과(Animation Effects)나 키프레임 효과(Keyframe Effects)는 바로 그 안에 집어넣는 'DVD 타이틀'이라고 생각하시면 됩니다.
키프레임 효과는 필수적으로 포함되어야 하는 정보들의 꾸러미입니다. 요소가 어떻게 변할지를 지정하는 '키프레임 세트(keys)'와 애니메이션이 진행될 '지속 시간(duration)'은 최소한으로 포함되어야 하죠. 그러면 DVD 플레이어인 애니메이션 객체가 이 정보 꾸러미를 받아들이고, 타임라인 객체를 활용해서 우리가 실제로 화면에서 보고 제어할 수 있는 재생 가능한 애니메이션으로 조립해 내는 것입니다.
현재 우리가 사용할 수 있는 애니메이션 효과의 종류는 KeyframeEffect 단 하나뿐입니다. 하지만 향후에는 예전 어도비 플래시(Flash) 시절에나 볼 수 있었던 기능들처럼, 여러 애니메이션을 하나로 묶어주는 그룹 효과(Group Effects)나 순서대로 재생시켜 주는 시퀀스 효과(Sequence Effects) 등 다양한 유형의 애니메이션 효과들이 등장할 가능성이 열려 있습니다. 실제로 현재 개발이 진행 중인 WAAPI 레벨 2 스펙 문서에는 이 내용들이 이미 스케치되어 있답니다.
💡 강사의 실무 팁:
DVD 비유가 정말 찰떡인 이유가 있습니다! 실무에서는 DVD 플레이어(Animation객체) 하나만 만들어 두고, 상황에 따라 DVD(KeyframeEffect객체)만 바꿔 끼우는 테크닉을 종종 씁니다. 똑같은 재생 제어 로직을 여러 애니메이션에 재사용할 수 있어서 코드가 아주 깔끔해지죠.
이렇게 흩어져 있는 부품들을 하나로 모아 작동하는 애니메이션을 만드는 방법은 두 가지입니다.
Animation() 생성자 함수를 사용해서 정석대로 하나하나 조립할 수도 있고, 더 쉽고 빠른 지름길인 Element.animate() 단축 함수를 사용할 수도 있습니다. (Element.animate() 사용법에 대한 자세한 내용은 Web Animations API 사용하기를 참고해 주세요.)
이 API를 사용하면 CSS가 만들어내는 것처럼 간단하고 선언적인 애니메이션뿐만 아니라, 상황에 따라 즉흥적으로 업데이트할 수 있는 다이내믹한 애니메이션까지 자유롭게 창조할 수 있습니다.
단순히 화면을 꾸미는 것을 넘어서 다음과 같은 강력한 활용처가 있습니다.
어떤 경우에는, 마치 jQuery 없이도 순수 자바스크립트(Vanilla JS)만으로 충분히 화면을 제어할 수 있는 것처럼 무겁고 복잡한 외부 애니메이션 라이브러리를 굳이 설치할 필요를 아예 없애주기도 합니다.
관련된 내용들을 더 깊이 있게 공부하고 싶으시다면 아래 링크들을 활용해 보세요!
안녕하세요! 지난번 애니메이션의 핵심 개념에 이어서, 오늘은 코드를 직접 짤 때 가장 중요한 '키프레임 포맷(Keyframe Formats)' 공식 문서를 가져오셨네요.
포트폴리오용 웹 프로필 사이트나 독후감 사이트를 만드실 때, 요소들이 부드럽게 나타나고 사라지는 디테일이 전체적인 완성도를 확 높여주곤 하죠. 특히 리액트(React)나 넥스트제이에스(Next.js) 환경에서 Zustand 같은 상태 관리 도구와 엮어 동적인 UI를 제어하거나, 스토리북(Storybook)으로 컴포넌트 단위의 애니메이션을 테스트하실 때 오늘 배우는 이 키프레임 객체 문법들이 정말 강력한 무기가 될 거예요.
이번에도 딱딱한 직역은 피하고, 이해하기 쉬운 구어체와 실무 팁을 팍팍 섞어서 번역해 드리겠습니다!
Element.animate(), KeyframeEffect(), 그리고 KeyframeEffect.setKeyframes() 메서드는 모두 키프레임 세트를 나타내는 형태로 구성된 객체(object)를 인자로 받습니다. 이 키프레임을 작성하는 형식에는 몇 가지 옵션이 있는데요, 아래에서 자세히 설명해 드릴게요.
키프레임을 작성하는 방법은 크게 두 가지가 있습니다.
array of objects)가장 첫 번째 방식은 변경할 속성과 값들로 이루어진 객체(키프레임)들을 배열로 나열하는 것입니다. 이는 getKeyframes() 메서드를 호출했을 때 브라우저가 기본적으로 반환해 주는 정석적인 포맷이기도 해요.
💡 강사의 팁: 배열 안에 시간 순서대로 객체를 툭툭 던져 넣는 방식이라 가장 직관적이고 이해하기 쉽습니다. 실무에서도 가장 많이 쓰이는 형태예요!
element.animate(
[
{
// from (시작 상태)
opacity: 0,
color: "white",
},
{
// to (끝 상태)
opacity: 1,
color: "black",
},
],
2000,
);
각 키프레임이 전체 애니메이션 시간 중 어느 시점에 실행될지는 offset 값을 제공하여 명시적으로 지정할 수 있습니다.
element.animate(
[{ opacity: 1 }, { opacity: 0.1, offset: 0.7 }, { opacity: 0 }],
2000,
);
참고:
offset값을 입력할 때는 반드시 0.0에서 1.0 사이의 값(포함)이어야 하며, 시간이 흐르는 순서에 맞게 오름차순으로 배치해야 합니다.
모든 키프레임에 일일이 offset을 지정할 필요는 없어요. offset이 지정되지 않은 키프레임들은 알아서 양옆에 있는 인접한 키프레임들 사이의 간격에 맞춰 균등하게 분배됩니다.
각 키프레임 사이를 부드럽게 이어주는 타이밍 함수는 아래 코드처럼 easing 값을 제공하여 지정할 수 있습니다.
element.animate(
[
{ opacity: 1, easing: "ease-out" },
{ opacity: 0.1, easing: "ease-in" },
{ opacity: 0 },
],
2000,
);
이 예제에서 지정된 easing 값은 해당 키프레임에서 시작하여 바로 다음 키프레임으로 넘어갈 때까지만 적용됩니다. 반면에, 애니메이션의 전체 설정(options 인자)에 easing 값을 지정하면 애니메이션의 전체 지속 시간(한 번의 반복 주기 전체) 동안 동일한 타이밍 함수가 쭈욱 적용된다는 차이점이 있으니 꼭 기억해 두세요!
object containing key-value pairs)두 번째 방식은 애니메이션을 적용할 '속성'을 키(key)로 하고, 변화할 '값들의 배열'을 값(value)으로 갖는 단일 객체 형식입니다.
💡 강사의 팁: 리액트 네이티브(React Native)나 Framer Motion 같은 애니메이션 라이브러리를 써보셨다면 이 형태가 아주 익숙하실 겁니다. 속성별로 어떻게 변하는지 한눈에 볼 수 있어서 코드가 더 간결해지는 장점이 있죠.
element.animate(
{
opacity: [0, 1], // [ from, to ]
color: ["white", "black"], // [ from, to ]
},
2000,
);
이 형식을 사용할 때는 각 배열에 들어있는 값의 개수가 꼭 똑같을 필요는 없어요. 입력된 값들은 배열 길이에 맞춰 각자 독립적으로 균등하게 간격이 벌어집니다.
element.animate(
{
opacity: [0, 1], // 시간대(offset): 0, 1
backgroundColor: ["red", "yellow", "green"], // 시간대(offset): 0, 0.5, 1
},
2000,
);
물론 여기서도 특별한 키워드인 offset, easing, 그리고 composite(아래에서 설명)을 속성 값들과 함께 배열 형태로 지정할 수 있습니다.
element.animate(
{
opacity: [0, 0.9, 1],
offset: [0, 0.8], // [ 0, 0.8, 1 ] 의 축약형으로 작동합니다.
easing: ["ease-in", "ease-out"],
},
2000,
);
브라우저는 전달받은 속성값 리스트들을 바탕으로 적절한 키프레임 세트를 생성한 뒤, 입력된 offset 배열의 값들을 각 키프레임에 순서대로 매칭시킵니다. 만약 offset 배열의 개수가 부족하거나 중간에 null 값이 섞여 있다면, 위에서 배열 방식으로 설명했던 것과 똑같이 지정되지 않은 빈자리는 균등하게 자동 배치됩니다.
만약 easing이나 composite 값의 개수가 부족하다면, 필요한 만큼 해당 리스트의 값들이 반복해서 적용됩니다.
브라우저는 현재 요소의 상태를 활용해서 애니메이션의 시작 상태나 종료 상태를 스스로 유추해 낼 수 있을 만큼 똑똑합니다.
기본적으로 딱 하나의 키프레임만 전달하면, 브라우저는 그걸 '종료 상태(to)'로 간주합니다. 그리고 '시작 상태(from)'는 현재 요소에 계산되어 적용되어 있는 CSS 스타일(computed style)을 가져와서 알아서 채워 넣죠. 하지만 offset 값을 직접 명시해주면, 내가 제공한 이 유일한 키프레임이 전체 타임라인의 어느 위치(예: 시작, 중간, 끝)에 놓여야 할지 정확히 지정해 줄 수도 있습니다. 더 자세한 내용은 Element.animate() 문서에서 확인하실 수 있어요.
💡 강사의 팁: 이 기능은 화면 바깥에 있던 모달창을 현재 화면 중앙으로
translateX시키면서 나타나게 할 때처럼, "지금 상태에서 특정 지점까지 이동해라!"라고 명령할 때 아주 깔끔하게 코드를 작성할 수 있게 해줍니다.
// 현재 상태에서 translateX(300px) 위치로 애니메이션 (도착지 지정)
logo.animate({ transform: "translateX(300px)" }, 1000);
// translateX(300px) 위치에서 현재 상태로 애니메이션 (출발지 지정)
logo.animate({ transform: "translateX(300px)", offset: 0 }, 1000);
// 현재 상태에서 시작해 중간에 translateX(300px)를 찍고 다시 현재 상태로 돌아오는 애니메이션
logo.animate({ transform: "translateX(300px)", offset: 0.5 }, 1000);
키프레임은 애니메이션을 적용할 CSS 속성들의 속성-값 쌍(property-value pairs)을 지정합니다.
이때 속성 이름은 반드시 카멜 케이스(camel case)로 작성해야 합니다. 예를 들어 CSS의 background-color는 backgroundColor로, background-position-x는 backgroundPositionX로 적어야 하죠. margin 같은 단축(shorthand) 속성도 당연히 사용할 수 있습니다.
다만, 두 가지 예외적인 CSS 속성이 있습니다.
float: 자바스크립트에서 "float"은 이미 예약어(소수점 숫자를 의미)로 쓰이고 있기 때문에 반드시 cssFloat이라고 작성해야 합니다. (참고로 float 자체는 애니메이션이 불가능한 속성이기 때문에 실제 애니메이션에 영향을 주지는 않습니다.)offset: 아래에서 설명할 키프레임의 타이밍 속성인 offset과 겹치기 때문에, 위치를 나타내는 CSS 속성을 쓸 때는 cssOffset이라고 작성해야 합니다.추가로, 다음과 같은 특별한 애니메이션 제어 속성들도 함께 사용할 수 있습니다.
offset해당 키프레임의 위치(타이밍)를 0.0에서 1.0 사이의 숫자 또는 null로 지정합니다. 이는 CSS 스타일시트에서 @keyframes를 사용할 때 0%, 100% 등의 백분율로 상태를 지정하는 것과 완전히 똑같은 역할을 합니다. 만약 이 값이 null이거나 아예 생략되었다면, 해당 키프레임은 인접한 키프레임들 사이에 균등한 간격으로 자동 배치됩니다.
현재 키프레임에서 다음 키프레임으로 진행될 때 사용할 타이밍 함수(easing function)를 지정합니다.
composite현재 키프레임에 지정된 값과 요소가 원래 가지고 있던 기존 값을 어떻게 섞을(결합할) 것인지 결정하는 KeyframeEffect.composite 연산입니다. 이 효과에 이미 지정된 합성 연산 설정이 있다면 기본값은 auto가 됩니다.
안녕하세요, 예비 프론트엔드 개발자 여러분! 오늘 강사와 함께 살펴볼 내용은 MDN의 'Web Animations API 팁과 요령' 공식 문서입니다. 문서가 영어로 되어 있어서 조금 막막하셨죠? 제가 여러분이 이해하기 쉽도록 친절한 구어체로, 빠지는 내용 없이 전부 번역해 드릴게요. 중간중간 제 실무 경험을 담은 꿀팁과 부연 설명도 팍팍 넣어드릴 테니 잘 따라와 주세요!
CSS 애니메이션은 여러분의 문서와 앱을 구성하는 요소들로 정말 믿기 힘들 만큼 놀라운 일들을 할 수 있게 해줍니다. 하지만 막상 작업을 하다 보면 어떻게 구현해야 할지 바로 감이 오지 않는 기능들이 있고, 당장 떠오르지 않는 아주 기발하고 똑똑한 방법들도 존재하기 마련이죠.
이 문서는 여러분의 작업 수고를 덜어드리기 위해 저희(MDN)가 찾아낸 다양한 팁과 요령들을 모아놓은 컬렉션입니다. 완료된 애니메이션을 다시 실행하는 방법 같은 유용한 내용들이 포함되어 있답니다.
🧑🏫 강사의 팁: 현업에서 애니메이션을 다루다 보면 "애니메이션 끝난 후에 클릭하면 다시 처음부터 실행되게 해주세요" 같은 기획자의 요청을 정말 많이 받습니다. CSS만으로는 은근히 까다로운 이런 작업들을 Web Animations API(JS)를 활용하면 얼마나 우아하게 풀 수 있는지 이번 기회에 확실히 알아가시길 바랍니다!
CSS Animations (CSS 애니메이션) 명세(specification)에서는 애니메이션을 '다시 실행'하는 방법을 기본적으로 제공하지 않습니다. 애니메이션이 한 번 끝난 후에 요소의 animation-play-state 속성을 다시 "running"으로 설정한다고 해서 애니메이션이 다시 재생되지는 않아요. 대신에, 완료된 애니메이션을 다시 재생시키려면 JavaScript(자바스크립트)를 사용해야만 합니다.
지금부터 보여드릴 방법은 이 문제를 해결하는 아주 안정적이고 신뢰할 수 있는 방법입니다.
먼저, 우리가 애니메이션을 적용할 <div> 요소와 애니메이션을 재생(또는 다시 재생)시킬 버튼의 HTML을 정의해 보겠습니다.
<div class="box"></div>
<button class="runButton">Run the animation</button>
다음으로, CSS를 사용해 박스(box)에 스타일을 입혀보겠습니다.
.box {
width: 100px;
height: 100px;
border: 1px solid black;
margin-bottom: 1rem;
}
이제 실제로 동작을 수행하는 자바스크립트 코드를 살펴볼 차례입니다. playAnimation() 함수는 사용자가 실행(Run) 버튼을 클릭했을 때 호출될 함수입니다. 우리는 CSS의 @keyframes 규칙을 사용하는 대신, 자바스크립트 내에서 직접 키프레임을 정의할 것입니다.
const box = document.querySelector(".box");
const button = document.querySelector(".runButton");
/*
아래의 CSS @keyframes 규칙과 동일한 역할을 합니다.
@keyframes colorChange {
0% {
background-color: grey;
}
100% {
background-color: lime;
}
}
*/
const colorChangeFrames = { backgroundColor: ["grey", "lime"] };
function playAnimation() {
box.animate(colorChangeFrames, 4000);
}
button.addEventListener("click", playAnimation);
이 코드에서 playAnimation 메서드는 박스 요소의 Element.animate() 메서드를 호출하여 애니메이션을 재생합니다. animate() 메서드는 키프레임 객체(또는 키프레임 객체들의 배열)와 애니메이션 지속 시간 등의 옵션을 인자(arguments)로 받습니다. 이 예제에서는 colorChangeFrames라는 키프레임 객체와 4000ms(4초)라는 애니메이션 지속 시간을 메서드에 전달하고 있죠.
그리고 버튼이 실제로 무언가 작동하도록 만들기 위해 실행 버튼에 이벤트 핸들러(event handler)도 추가했습니다.
🧑🏫 강사의 부연 설명: CSS의 클래스를 넣었다 뺐다(
classList.add,classList.remove) 하면서 애니메이션을 재시작하는 꼼수를 써보신 적 있으신가요? 그 방법은 DOM을 강제로 리페인트(Repaint) 시켜야 해서 코드가 지저분해지고 버그가 발생하기 쉽습니다. 반면Element.animate()를 사용하면 브라우저의 애니메이션 엔진을 직접 타격하기 때문에 클릭할 때마다 아주 깔끔하고 독립적으로 애니메이션이 처음부터 다시 실행됩니다!
이전 예제에서는, 만약 애니메이션이 채 끝나기도 전에 사용자가 실행 버튼을 다시 클릭하면 현재 진행 중이던 애니메이션이 갑자기 뚝 끊기고 0% (또는 from) 시작 키프레임부터 애니메이션이 재시작됩니다.
만약 새로운 애니메이션을 시작하기 전에 현재 진행 중인 애니메이션 사이클이 끝까지 완료되기를 원한다면 어떻게 해야 할까요? 애니메이션이 실행되는 동안에는 run 버튼을 비활성화(disable)시키고, finish 이벤트가 발생했을 때 다시 버튼을 활성화(reenable)시키는 방법이 있습니다.
혹은 이와는 다르게 애니메이션이 여러 번 중첩되어 반복되도록 만들고 싶다면, 요소에서 애니메이션이 실행 중인지 확인한 다음, 애니메이션이 실행되는 동안 버튼을 클릭할 때마다 animation-iteration(반복 횟수) 카운트를 증가시키는 방법도 쓸 수 있죠.
이번 예제에서는, 버튼이 클릭되면 버튼을 비활성화시키고 애니메이션이 끝나는 finish 이벤트를 감지하여 다시 버튼을 활성화하도록 playAnimation() 함수를 수정해 보겠습니다.
<div class="box"></div>
<button class="runButton">Run the animation</button>
.box {
width: 100px;
height: 100px;
border: 1px solid black;
margin-bottom: 1rem;
}
const box = document.querySelector(".box");
const button = document.querySelector(".runButton");
const colorChangeFrames = { backgroundColor: ["grey", "lime"] };
button.addEventListener("click", playAnimation);
function playAnimation() {
button.setAttribute("disabled", true);
const anim = box.animate(colorChangeFrames, 4000);
anim.addEventListener("finish", (event) => {
button.removeAttribute("disabled");
});
}
이 코드는 버튼을 비활성화한 뒤 애니메이션을 시작합니다. 그리고 애니메이션이 무사히 완료되면 버튼이 다시 활성화됩니다.
🧑🏫 강사의 팁: 버튼을 비활성화(
disabled) 처리하는 것은 UI/UX 관점에서도 매우 훌륭한 접근법입니다. 사용자가 무의미하게 버튼을 여러 번 '따닥따닥' 누르는 것을 방지할 수 있기 때문이죠.anim.addEventListener('finish', ...)패턴은 비동기 작업 후 UI 상태를 원상 복구시키는 패턴으로 실무에서 굉장히 자주 쓰입니다. 기억해두세요! Promise를 좋아하신다면await anim.finished;를 사용할 수도 있답니다.
CSS 애니메이션이 진행되는 동안 애니메이션이 적용되는 속성(properties)들은 마치 will-change 속성에 선언된 것처럼 동일하게 동작합니다. 만약 어떤 속성이 will-change로 지정되었을 때 쌓임 문맥(Stacking context)을 생성하는 속성이라면, 해당 요소는 애니메이션이 진행되는 동안 새로운 쌓임 문맥을 부여받게 됩니다.
animation-fill-mode: forwards (그리고 both의 경우도 포함)를 사용하는 경우, 애니메이션된 속성들은 애니메이션이 완전히 끝난 후에도 마지막 키프레임 상태에 그대로 머물러 있게 됩니다. 이때 속성들은 will-change 상태도 함께 유지하게 됩니다. 즉, 애니메이션 도중에 새로운 쌓임 문맥이 생성되었고 애니메이션이 끝난 후에도 그 상태가 지속된다면, 타겟 요소는 애니메이션이 종료된 이후에도 계속해서 그 새로운 쌓임 문맥을 유지하게 된다는 뜻입니다.
🧑🏫 강사의 부연 설명: 이 부분은 프론트엔드 개발자들이 정말 자주 마주치는 z-index 버그의 핵심 원인입니다!
"강사님, 분명히z-index를 9999로 줬는데 다른 요소 밑으로 숨어버려요!"라고 질문하는 경우가 많은데요. 애니메이션(특히transform이나opacity)이 걸려있는 요소는 독립적인 '쌓임 문맥(레이어 층)'을 형성해버립니다. 그래서 애니메이션이 끝난 후에도forwards로 인해 그 상태가 유지되면, 부모나 다른 요소들과의 층위 계산이 꼬이는 현상이 발생할 수 있습니다.
애니메이션이 끝난 요소의 z-index가 이상하게 동작한다면 이 'Stacking context' 개념을 꼭 떠올려 보세요!
더 깊이 있는 학습을 원하신다면 아래 문서들을 참고해 보세요.
수고하셨습니다! Web Animations API를 통해 여러분의 웹 페이지에 더 생동감 넘치고 부드러운 상호작용을 더해보시길 바랍니다! 궁금한 점이 있으면 언제든 질문해 주세요.