웹 앱의 애니메이션 강화
TL;DR: 애니메이션 Worklet을 사용하면 기기의 네이티브 프레임 속도로 실행되는 명령형 애니메이션을 작성하여 버터처럼 부드러운™ 끊김 없는 매끄러움을 더하고, 기본 스레드 끊김에 대해 애니메이션의 복원력을 높이며, 시간 대신 스크롤에 연결할 수 있습니다. 애니메이션 워크릿은 Chrome Canary에 있으며('실험용 웹 플랫폼 기능' 플래그 뒤) Chrome 71용 오리진 트라이얼이 계획되어 있습니다. 오늘부터 점진적 개선으로 사용할 수 있습니다.
다른 애니메이션 API
아니요. 기존 기능을 확장한 것입니다. 처음부터 시작해 보겠습니다. 오늘날 웹에서 DOM 요소를 애니메이션으로 만들려면 2가지 반의 선택사항이 있습니다. 간단한 A-B 전환을 위한 CSS 전환, 잠재적으로 순환적이고 더 복잡한 시간 기반 애니메이션을 위한 CSS 애니메이션, 거의 임의로 복잡한 애니메이션을 위한 Web Animations API(WAAPI)입니다. WAAPI 지원 매트릭스는 상당히 어둡지만 상승세에 있습니다. 그때까지는 polyfill이 있습니다.
이러한 모든 메서드의 공통점은 상태가 없고 시간 기반이라는 것입니다. 하지만 개발자가 시도하는 효과 중 일부는 시간 기반도 아니고 상태 비저장도 아닙니다. 예를 들어 악명 높은 시차 스크롤러는 이름에서 알 수 있듯이 스크롤에 의해 작동합니다. 오늘날 웹에서 성능이 우수한 시차 스크롤러를 구현하는 것은 놀라울 정도로 어렵습니다.
스테이트리스(Stateless)는 어떤가요? 예를 들어 Android에서 Chrome의 주소 표시줄을 생각해 보세요. 아래로 스크롤하면 뷰에서 스크롤됩니다. 하지만 페이지 중간에 있더라도 위로 스크롤하면 다시 표시됩니다. 애니메이션은 스크롤 위치뿐만 아니라 이전 스크롤 방향에도 따라 달라집니다. 스테이트풀입니다.
또 다른 문제는 스크롤바 스타일 지정입니다. 스타일 지정이 불가능하거나 최소한 충분히 스타일 지정할 수 없습니다. 스크롤바로 냥이를 사용하고 싶다면 어떻게 해야 하나요? 어떤 기법을 선택하든 맞춤 스크롤바를 빌드하는 것은 성능이 좋지도 않고 쉽지도 않습니다.
이러한 모든 항목은 어색하고 효율적으로 구현하기 어렵거나 불가능합니다. 이러한 애니메이션은 대부분 이벤트 또는 requestAnimationFrame
에 의존하므로 화면이 90fps, 120fps 이상으로 실행될 수 있고 소중한 기본 스레드 프레임 예산의 일부를 사용하더라도 60fps로 유지될 수 있습니다.
애니메이션 Worklet은 웹의 애니메이션 스택 기능을 확장하여 이러한 효과를 더 쉽게 만들 수 있도록 합니다. 시작하기 전에 애니메이션의 기본 사항을 최신 상태로 유지해야 합니다.
애니메이션 및 타임라인 입문
WAAPI와 애니메이션 워크릿은 타임라인을 광범위하게 사용하여 원하는 방식으로 애니메이션과 효과를 오케스트레이션할 수 있도록 합니다. 이 섹션에서는 타임라인과 애니메이션과의 작동 방식을 간략하게 복습하거나 소개합니다.
각 문서에는 document.timeline
가 있습니다. 문서가 생성될 때 0에서 시작하여 문서가 존재하기 시작한 이후의 밀리초를 계산합니다. 문서의 모든 애니메이션은 이 타임라인을 기준으로 작동합니다.
좀 더 구체적으로 설명하기 위해 이 WAAPI 스니펫을 살펴보겠습니다.
const animation = new Animation(
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
{
transform: 'translateY(500px)',
},
],
{
delay: 3000,
duration: 2000,
iterations: 3,
}
),
document.timeline
);
animation.play();
animation.play()
를 호출하면 애니메이션은 타임라인의 currentTime
을 시작 시간으로 사용합니다. 애니메이션의 지연 시간은 3000ms입니다. 즉, 타임라인이 `startTime`에 도달하면 애니메이션이 시작되거나 '활성' 상태가 됩니다.
- 3000
. After that time, the animation engine will animate the given element from the first keyframe (
translateX(0)), through all intermediate keyframes (
translateX(500px)) all the way to the last keyframe (
translateY(500px)) in exactly 2000ms, as prescribed by the
durationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline's
currentTimeis
startTime + 3000 + 1000and the last keyframe at
startTime + 3000 + 2000'입니다. 요점은 타임라인이 애니메이션의 위치를 제어한다는 것입니다.
애니메이션이 마지막 키프레임에 도달하면 첫 번째 키프레임으로 다시 이동하여 애니메이션의 다음 반복을 시작합니다. iterations: 3
을 설정했으므로 이 프로세스는 총 3번 반복됩니다. 애니메이션이 중지되지 않도록 하려면 iterations: Number.POSITIVE_INFINITY
을 작성합니다. 위 코드의 결과는 다음과 같습니다.
WAAPI는 매우 강력하며 이 API에는 이 도움말의 범위를 벗어나는 이징, 시작 오프셋, 키프레임 가중치, 채우기 동작과 같은 기능이 훨씬 더 많이 있습니다. 자세히 알아보려면 CSS Tricks의 CSS 애니메이션에 관한 이 도움말을 참고하세요.
애니메이션 Worklet 작성
이제 타임라인의 개념을 이해했으므로 애니메이션 Worklet과 이를 통해 타임라인을 조작하는 방법을 살펴볼 수 있습니다. Animation Worklet API는 WAAPI를 기반으로 할 뿐만 아니라 확장 가능한 웹이라는 의미에서 WAAPI가 작동하는 방식을 설명하는 하위 수준 기본 요소입니다. 구문 측면에서 매우 유사합니다.
애니메이션 Worklet | WAAPI |
---|---|
new WorkletAnimation( 'passthrough', new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
new Animation( new KeyframeEffect( document.querySelector('#a'), [ { transform: 'translateX(0)' }, { transform: 'translateX(500px)' } ], { duration: 2000, iterations: Number.POSITIVE_INFINITY } ), document.timeline ).play(); |
차이점은 이 애니메이션을 실행하는 워크릿의 이름인 첫 번째 매개변수에 있습니다.
기능 감지
Chrome은 이 기능을 제공하는 첫 번째 브라우저이므로 코드에서 AnimationWorklet
가 있다고 가정해서는 안 됩니다. 따라서 워크릿을 로드하기 전에 간단한 검사를 통해 사용자의 브라우저가 AnimationWorklet
를 지원하는지 감지해야 합니다.
if ('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}
worklet 로드
Worklet은 새로운 API를 더 쉽게 빌드하고 확장할 수 있도록 Houdini 태스크 포스에서 도입한 새로운 개념입니다. 나중에 워크릿의 세부사항을 좀 더 자세히 다루겠지만 지금은 워크릿을 저렴하고 가벼운 스레드 (작업자와 유사)로 생각하면 됩니다.
애니메이션을 선언하기 전에 이름이 'passthrough'인 워크릿을 로드했는지 확인해야 합니다.
// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...
// passthrough-aw.js
registerAnimator(
'passthrough',
class {
animate(currentTime, effect) {
effect.localTime = currentTime;
}
}
);
문제가 무엇인가요? AnimationWorklet의 registerAnimator()
호출을 사용하여 클래스를 애니메이터로 등록하고 이름은 'passthrough'로 지정합니다.
위의 WorkletAnimation()
생성자에서 사용한 이름과 동일합니다. 등록이 완료되면 addModule()
에서 반환된 프로미스가 해결되고 해당 워크릿을 사용하여 애니메이션을 만들 수 있습니다.
인스턴스의 animate()
메서드는 브라우저가 렌더링하려는 모든 프레임에 대해 호출되며, 애니메이션 타임라인의 currentTime
와 현재 처리 중인 효과를 전달합니다. 효과는 KeyframeEffect
하나만 있고 currentTime
를 사용하여 효과의 localTime
를 설정하므로 이 애니메이터를 '패스스루'라고 합니다. 이 워크릿 코드를 사용하면 위의 WAAPI와 AnimationWorklet이 정확히 동일하게 동작합니다. 데모에서 확인할 수 있습니다.
시간
animate()
메서드의 currentTime
매개변수는 WorkletAnimation()
생성자에 전달한 타임라인의 currentTime
입니다. 이전 예에서는 해당 시간을 효과에 전달했습니다. 하지만 JavaScript 코드이므로 시간을 왜곡할 수 있습니다 💫
function remap(minIn, maxIn, minOut, maxOut, v) {
return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
'sin',
class {
animate(currentTime, effect) {
effect.localTime = remap(
-1,
1,
0,
2000,
Math.sin((currentTime * 2 * Math.PI) / 2000)
);
}
}
);
currentTime
의 Math.sin()
를 가져와서 효과가 정의된 시간 범위인 [0; 2000] 범위에 값을 다시 매핑합니다. 이제 키프레임이나 애니메이션 옵션을 변경하지 않았는데도 애니메이션이 매우 다르게 표시됩니다. 워크릿 코드는 임의로 복잡할 수 있으며, 어떤 효과가 어떤 순서로 어느 정도 재생되는지 프로그래매틱 방식으로 정의할 수 있습니다.
옵션 선택
worklet을 재사용하고 숫자를 변경할 수 있습니다. 이러한 이유로 WorkletAnimation 생성자를 사용하면 옵션 객체를 워크릿에 전달할 수 있습니다.
registerAnimator(
'factor',
class {
constructor(options = {}) {
this.factor = options.factor || 1;
}
animate(currentTime, effect) {
effect.localTime = currentTime * this.factor;
}
}
);
new WorkletAnimation(
'factor',
new KeyframeEffect(
document.querySelector('#b'),
[
/* ... same keyframes as before ... */
],
{
duration: 2000,
iterations: Number.POSITIVE_INFINITY,
}
),
document.timeline,
{factor: 0.5}
).play();
이 예에서는 두 애니메이션이 동일한 코드로 실행되지만 옵션은 다릅니다.
지역의 주를 알려 줘.
앞서 언급했듯이 애니메이션 워크릿이 해결하고자 하는 주요 문제 중 하나는 상태 저장 애니메이션입니다. 애니메이션 워크릿은 상태를 보유할 수 있습니다. 하지만 워크릿의 핵심 기능 중 하나는 리소스를 절약하기 위해 다른 스레드로 이전하거나 소멸시킬 수 있다는 점이며, 이 경우 상태도 소멸됩니다. 상태 손실을 방지하기 위해 애니메이션 워크릿은 워크릿이 소멸되기 전에 호출되는 후크를 제공하며, 이 후크를 사용하여 상태 객체를 반환할 수 있습니다. 이 객체는 워크릿이 다시 생성될 때 생성자에 전달됩니다. 처음 생성 시 이 매개변수는 undefined
입니다.
registerAnimator(
'randomspin',
class {
constructor(options = {}, state = {}) {
this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
}
animate(currentTime, effect) {
// Some math to make sure that `localTime` is always > 0.
effect.localTime = 2000 + this.direction * (currentTime % 2000);
}
destroy() {
return {
direction: this.direction,
};
}
}
);
이 데모를 새로고침할 때마다 정사각형이 회전할 방향이 50/50으로 결정됩니다. 브라우저가 워크릿을 해체하고 다른 스레드로 이전하면 생성 시 다른 Math.random()
호출이 발생하여 방향이 갑자기 바뀔 수 있습니다. 이러한 상황을 방지하기 위해 애니메이션의 무작위로 선택된 방향을 상태로 반환하고 제공된 경우 생성자에서 사용합니다.
시공간 연속체에 연결: ScrollTimeline
이전 섹션에서 살펴본 것처럼 AnimationWorklet을 사용하면 타임라인을 진행하는 것이 애니메이션 효과에 어떤 영향을 미치는지 프로그래매틱 방식으로 정의할 수 있습니다. 하지만 지금까지 타임라인은 항상 시간을 추적하는 document.timeline
였습니다.
ScrollTimeline
는 새로운 가능성을 열어주고 시간 대신 스크롤로 애니메이션을 제어할 수 있습니다. 이 데모에서는 첫 번째 '패스스루' 워클릿을 재사용합니다.
new WorkletAnimation(
'passthrough',
new KeyframeEffect(
document.querySelector('#a'),
[
{
transform: 'translateX(0)',
},
{
transform: 'translateX(500px)',
},
],
{
duration: 2000,
fill: 'both',
}
),
new ScrollTimeline({
scrollSource: document.querySelector('main'),
orientation: 'vertical', // "horizontal" or "vertical".
timeRange: 2000,
})
).play();
document.timeline
를 전달하는 대신 새 ScrollTimeline
를 만듭니다.
짐작하셨겠지만 ScrollTimeline
는 시간을 사용하지 않고 scrollSource
의 스크롤 위치를 사용하여 워크릿에서 currentTime
를 설정합니다. 맨 위 (또는 왼쪽)까지 스크롤하면 currentTime = 0
이 되고 맨 아래 (또는 오른쪽)까지 스크롤하면 currentTime
이 timeRange
로 설정됩니다. 이 데모에서 상자를 스크롤하면 빨간색 상자의 위치를 제어할 수 있습니다.
스크롤되지 않는 요소로 ScrollTimeline
를 만들면 타임라인의 currentTime
이 NaN
가 됩니다. 따라서 특히 반응형 디자인을 염두에 두고 항상 NaN
를 currentTime
로 사용할 수 있도록 준비해야 합니다. 기본값을 0으로 설정하는 것이 합리적인 경우가 많습니다.
애니메이션을 스크롤 위치와 연결하는 것은 오랫동안 추구해 왔지만 CSS3D를 사용한 임시 해결 방법을 제외하고는 이 수준의 충실도로 달성한 적이 없습니다. 애니메이션 Worklet을 사용하면 이러한 효과를 매우 성능이 우수하면서도 간단한 방식으로 구현할 수 있습니다. 예를 들어 이 데모와 같은 시차 스크롤 효과를 사용하면 이제 스크롤 기반 애니메이션을 정의하는 데 몇 줄만 있으면 됩니다.
자세히 들여다보기
Worklet
Worklet은 격리된 범위와 매우 작은 API 표면을 갖는 JavaScript 컨텍스트입니다. 작은 API 노출 영역을 통해 브라우저에서 특히 저사양 기기에서 더 적극적으로 최적화할 수 있습니다. 또한 워크릿은 특정 이벤트 루프에 바인딩되지 않지만 필요에 따라 스레드 간에 이동할 수 있습니다. 이는 AnimationWorklet에 특히 중요합니다.
컴포지터 NSync
일부 CSS 속성은 애니메이션이 빠르지만 다른 속성은 그렇지 않다는 것을 알고 있을 것입니다. 일부 속성은 GPU에서 애니메이션을 적용하기만 하면 되지만 다른 속성은 브라우저가 전체 문서를 다시 레이아웃하도록 강제합니다.
Chrome에는 다른 많은 브라우저와 마찬가지로 컴포지터라는 프로세스가 있습니다. 이 프로세스의 역할은 레이어와 텍스처를 정렬한 다음 GPU를 활용하여 화면을 최대한 자주 업데이트하는 것입니다 (여기서는 매우 단순화함). 이상적으로는 화면이 업데이트될 수 있는 속도 (일반적으로 60Hz)만큼 빠르게 업데이트합니다. 애니메이션이 적용되는 CSS 속성에 따라 브라우저에서 컴포지터가 작업을 실행하기만 하면 되는 반면 다른 속성은 레이아웃을 실행해야 할 수도 있습니다. 레이아웃은 기본 스레드에서만 실행할 수 있는 작업입니다. 애니메이션을 적용할 속성에 따라 애니메이션 워크릿이 기본 스레드에 바인딩되거나 컴포지터와 동기화된 별도의 스레드에서 실행됩니다.
가벼운 처벌
GPU는 경합이 심한 리소스이므로 일반적으로 여러 탭에서 공유될 수 있는 컴포지터 프로세스는 하나만 있습니다. 컴포지터가 어떤 이유로 차단되면 전체 브라우저가 멈추고 사용자 입력에 응답하지 않습니다. 이러한 상황은 어떤 경우에도 피해야 합니다. 그렇다면 워크릿이 컴포지터가 프레임을 렌더링하는 데 필요한 데이터를 제때 제공할 수 없는 경우에는 어떻게 될까요?
이 경우 워크렛은 사양에 따라 '슬립'할 수 있습니다. 컴포지터보다 뒤처지며 컴포지터는 프레임 속도를 유지하기 위해 마지막 프레임의 데이터를 재사용할 수 있습니다. 시각적으로는 버벅거림처럼 보이지만 큰 차이점은 브라우저가 사용자 입력에 계속 응답한다는 것입니다.
결론
AnimationWorklet에는 다양한 측면이 있으며 웹에 여러 이점을 제공합니다. 명백한 이점은 애니메이션을 더 세부적으로 제어하고 애니메이션을 유도하는 새로운 방법을 통해 웹에 새로운 수준의 시각적 충실도를 제공할 수 있다는 것입니다. 하지만 API 설계는 동시에 모든 새로운 기능을 이용하면서 앱이 끊김 현상에 더 탄력적으로 대응할 수 있도록 지원합니다.
애니메이션 Worklet은 Canary에 있으며 Chrome 71에서 오리진 트라이얼을 목표로 하고 있습니다. 새로운 웹 환경을 기대하며 개선할 수 있는 부분을 알려주시기 바랍니다. 동일한 API를 제공하지만 성능 격리를 제공하지 않는 polyfill도 있습니다.
CSS 전환과 CSS 애니메이션은 여전히 유효한 옵션이며 기본 애니메이션의 경우 훨씬 간단할 수 있습니다. 하지만 멋진 효과를 내고 싶다면 AnimationWorklet을 사용하세요.