후디니의 애니메이션 Worklet

웹 앱의 애니메이션 강화

요약: 애니메이션 워크렛을 사용하면 기기의 기본 프레임 속도로 실행되는 명령형 애니메이션을 작성하여 버벅거림 없는 부드러운 화면 전환™을 구현하고, 메인 스레드 버벅거림에 대해 애니메이션의 탄력성을 높이며, 시간 대신 스크롤에 연결할 수 있습니다. 애니메이션 워크렛은 Chrome Canary('실험용 웹 플랫폼 기능' 플래그 뒤에 있음)에 있으며 Chrome 71의 오리진 트라이얼을 계획하고 있습니다. 지금부터 점진적 개선으로 사용할 수 있습니다.

다른 Animation API?

사실 그렇지 않습니다. 이미 존재하는 기능을 확장한 것일 뿐입니다. 처음부터 시작해 보겠습니다. 현재 웹에서 DOM 요소에 애니메이션을 적용하려면 두 가지 방법이 있습니다. 간단한 A-B 전환에는 CSS 전환을, 주기적이고 더 복잡한 시간 기반 애니메이션에는 CSS 애니메이션을, 거의 임의로 복잡한 애니메이션에는 Web Animations API(WAAPI)를 사용하세요. WAAPI의 지원 매트릭스는 꽤 암울해 보이지만 점점 개선되고 있습니다. 그때까지는 폴리필이 있습니다.

이러한 모든 메서드의 공통점은 스테이트리스하고 시간 기반이라는 점입니다. 하지만 개발자가 시도하는 일부 효과는 시간 기반이 아니거나 상태가 없습니다. 예를 들어 악명 높은 시차 스크롤러는 이름에서 알 수 있듯이 스크롤을 기반으로 합니다. 현재 웹에서 성능이 우수한 시차 스크롤러를 구현하는 것은 놀라울 정도로 어렵습니다.

스테이트리스(Stateless)는 어떨까요? 예를 들어 Android의 Chrome 주소 표시줄을 생각해 보세요. 아래로 스크롤하면 화면에서 사라집니다. 하지만 위로 스크롤하는 순간, 페이지의 절반을 지나도 다시 표시됩니다. 애니메이션은 스크롤 위치뿐만 아니라 이전 스크롤 방향에 따라 달라집니다. 스테이트풀입니다.

또 다른 문제는 스크롤바 스타일 지정입니다. 스타일을 지정할 수 없거나 적어도 충분히 스타일을 지정할 수 없습니다. 스크롤바로 냐옹캣을 사용하려면 어떻게 해야 하나요? 어떤 기법을 선택하든 맞춤 스크롤바를 빌드하는 것은 성능이 좋지 않고 쉬운 작업이 아닙니다.

요점은 이러한 모든 작업이 어색하고 효율적으로 구현하기 어렵다는 것입니다. 대부분은 이벤트 또는 requestAnimationFrame를 사용합니다. 따라서 화면이 90fps, 120fps 이상으로 실행될 수 있더라도 60fps로 유지되고 소중한 메인 스레드 프레임 예산의 일부만 사용될 수 있습니다.

애니메이션 워크렛은 웹의 애니메이션 스택 기능을 확장하여 이러한 종류의 효과를 더 쉽게 만듭니다. 시작하기 전에 애니메이션의 기본사항을 업데이트해 보겠습니다.

애니메이션 및 타임라인에 관한 입문서

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 thedurationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'scurrentTimeisstartTime + 3000 + 1000and the last keyframe atstartTime + 3000 + 2000`. 중요한 점은 타임라인이 애니메이션의 현재 위치를 제어한다는 것입니다.

애니메이션이 마지막 키프레임에 도달하면 첫 번째 키프레임으로 다시 돌아가고 애니메이션의 다음 반복을 시작합니다. 이 프로세스는 iterations: 3를 설정할 때부터 총 3번 반복됩니다. 애니메이션이 멈추지 않도록 하려면 iterations: Number.POSITIVE_INFINITY를 작성합니다. 다음은 위 코드의 결과입니다.

WAAPI는 매우 강력하며 이 API에는 이의 범위를 벗어나는 이중, 시작 오프셋, 키프레임 가중치, 채우기 동작과 같은 더 많은 기능이 있습니다. 자세한 내용은 CSS Tricks의 CSS 애니메이션에 관한 도움말을 참고하세요.

애니메이션 워크렛 작성

이제 타임라인의 개념을 이해했으므로 애니메이션 워크렛과 타임라인을 조작하는 방법을 살펴보겠습니다. 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!
}

워크렛 로드

워크렛은 많은 새 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)
      );
    }
  }
);

currentTimeMath.sin()를 가져와 효과가 정의된 시간 범위인 [0, 2000] 범위로 재매핑합니다. 이제 키프레임이나 애니메이션 옵션을 변경하지 않고도 애니메이션이 매우 다르게 보입니다. 워크렛 코드는 임의로 복잡할 수 있으며, 어떤 효과가 어떤 순서로 어느 정도 재생되는지 프로그래매틱 방식으로 정의할 수 있습니다.

옵션 위의 옵션

워크렛을 재사용하고 숫자를 변경할 수 있습니다. 따라서 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%의 확률로 결정됩니다. 브라우저가 워크렛을 해체하고 다른 스레드로 이전하면 생성 시 또 다른 Math.random() 호출이 발생하여 갑작스러운 방향 변경이 발생할 수 있습니다. 이를 방지하기 위해 애니메이션에서 무작위로 선택한 방향을 state로 반환하고 생성자에서 사용합니다(제공된 경우).

시공간 연속체에 연결: 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를 의미하고 맨 아래 (또는 오른쪽)까지 스크롤하면 currentTimetimeRange로 설정됩니다. 이 데모에서 상자를 스크롤하면 빨간색 상자의 위치를 제어할 수 있습니다.

스크롤되지 않는 요소로 ScrollTimeline를 만들면 타임라인의 currentTimeNaN가 됩니다. 따라서 특히 반응형 디자인을 염두에 두고 항상 currentTimeNaN를 사용할 준비를 해야 합니다. 기본값을 0으로 설정하는 것이 좋습니다.

애니메이션을 스크롤 위치와 연결하는 것은 오래 전부터 시도되었지만 CSS3D의 해킹 해결 방법을 제외하고는 이 정도의 정확성 수준을 달성한 적이 없었습니다. 애니메이션 워크렛을 사용하면 이러한 효과를 간단한 방식으로 구현하면서도 성능을 높일 수 있습니다. 예를 들어 이 데모와 같은 시차 스크롤 효과는 이제 스크롤 기반 애니메이션을 정의하는 데 몇 줄만 있으면 된다는 것을 보여줍니다.

자세히 들여다보기

Worklets

워크렛은 격리된 범위와 매우 작은 API 노출 영역이 있는 JavaScript 컨텍스트입니다. 작은 API 노출 영역을 사용하면 특히 저사양 기기에서 브라우저의 더 공격적인 최적화가 가능합니다. 또한 워크렛은 특정 이벤트 루프에 바인딩되지 않지만 필요에 따라 스레드 간에 이동할 수 있습니다. 이는 AnimationWorklet에 특히 중요합니다.

Compositor NSync

특정 CSS 속성은 애니메이션이 빠르게 적용되는 반면 다른 속성은 그렇지 않다는 것을 알고 있을 수 있습니다. 일부 속성은 GPU에서 약간의 작업만 하면 애니메이션이 적용되지만, 다른 속성은 브라우저가 전체 문서를 다시 레이아웃하도록 강제합니다.

Chrome에는 다른 많은 브라우저와 마찬가지로 컴포저라는 프로세스가 있습니다. 컴포저의 역할은 레이어와 텍스처를 정렬한 다음 GPU를 활용하여 화면을 최대한 정기적으로 업데이트하는 것입니다(가급적 화면이 업데이트되는 속도만큼 빠르게, 일반적으로 60Hz). 애니메이션이 적용되는 CSS 속성에 따라 브라우저에서 컴포저가 작업을 실행하기만 하면 되는 경우도 있고, 다른 속성의 경우 기본 스레드에서만 실행할 수 있는 작업인 레이아웃을 실행해야 하는 경우도 있습니다. 애니메이션을 적용할 속성에 따라 애니메이션 워크렛이 기본 스레드에 바인딩되거나 컴포저와 동기화되어 별도의 스레드에서 실행됩니다.

경고

GPU는 경쟁이 치열한 리소스이므로 일반적으로 여러 탭에서 공유될 수 있는 컴포저 프로세스는 하나뿐입니다. 컴포저이터가 어떻게든 차단되면 전체 브라우저가 중단되고 사용자 입력에 응답하지 않게 됩니다. 이러한 상황은 어떤 경우에도 피해야 합니다. 그러면 프레임이 렌더링될 때 워크렛이 컴포저이터에 필요한 데이터를 제때 전송할 수 없는 경우 어떻게 되나요?

이 경우 사양에 따라 워크렛이 '누락'될 수 있습니다. 컴포저이터보다 뒤처지며 컴포저이터는 프레임 속도를 유지하기 위해 마지막 프레임의 데이터를 재사용할 수 있습니다. 시각적으로는 버벅거림처럼 보이지만 큰 차이점은 브라우저가 여전히 사용자 입력에 반응한다는 점입니다.

결론

AnimationWorklet과 웹에 제공하는 이점에는 여러 측면이 있습니다. 애니메이션을 더 세부적으로 제어하고 애니메이션을 구동하는 새로운 방법을 통해 웹에 새로운 수준의 시각적 충실도를 제공할 수 있다는 것이 분명한 이점입니다. 또한 API 설계를 통해 모든 새로운 기능에 액세스하는 동시에 앱이 버벅거림에 더 탄력적으로 대응할 수 있습니다.

애니메이션 워크렛은 Canary에 있으며 Chrome 71에서 오리진 체험판을 목표로 하고 있습니다. Google은 새로운 웹 환경을 사용해 보고 개선이 필요한 부분을 알려주시기를 기다리고 있습니다. 동일한 API를 제공하지만 성능 격리를 제공하지 않는 폴리필도 있습니다.

CSS 전환 및 CSS 애니메이션은 여전히 유효한 옵션이며 기본 애니메이션의 경우 훨씬 더 간단할 수 있습니다. 하지만 멋진 애니메이션을 사용해야 하는 경우 AnimationWorklet을 사용하세요.