성능이 뛰어난 확장 및 축소 애니메이션 빌드

폴 루이스
스티븐 맥그루어
스티븐 맥그루어

요약

클립에 애니메이션을 적용할 때 배율 변환을 사용합니다. 카운터 크기 조정을 통해 애니메이션 중에 하위 요소가 늘어나거나 왜곡되는 것을 방지할 수 있습니다.

이전에 성능 기준에 맞는 시차 효과무한 스크롤러를 만드는 방법에 관한 업데이트를 게시했습니다. 이 게시물에서는 뛰어난 성능의 클립 애니메이션을 원하는 경우 무엇을 해야 하는지 살펴보겠습니다. 데모를 보려면 샘플 UI 요소 GitHub 저장소를 확인하세요.

확장 메뉴를 예로 들어보겠습니다.

이를 빌드하는 일부 옵션은 다른 옵션보다 성능이 뛰어납니다.

좋지 않음: 컨테이너 요소의 너비 및 높이에 애니메이션 적용

약간의 CSS를 사용하여 컨테이너 요소의 너비와 높이에 애니메이션을 적용한다고 상상해 보세요.

.menu {
  overflow: hidden;
  width: 350px;
  height: 600px;
  transition: width 600ms ease-out, height 600ms ease-out;
}

.menu--collapsed {
  width: 200px;
  height: 60px;
}

이 접근 방식의 직접적인 문제는 widthheight를 애니메이션 처리해야 한다는 것입니다. 이러한 속성을 사용하려면 레이아웃을 계산해야 하고 애니메이션의 모든 프레임에서 결과를 페인트해야 합니다. 이로 인해 비용이 많이 들 수 있고 일반적으로 60fps를 놓칠 수 있습니다. 새로운 소식이 있으면 렌더링 성능 가이드를 참고하세요. 여기에서 렌더링 프로세스의 작동 방식에 관한 자세한 내용을 확인할 수 있습니다.

나쁨: CSS 클립 또는 클립 경로 속성 사용

widthheight에 애니메이션을 적용하는 대신 clip 속성 (현재 지원 중단됨)을 사용하여 확장 및 축소 효과를 애니메이션으로 표시할 수 있습니다. 또는 원하는 경우 clip-path를 대신 사용할 수도 있습니다. 그러나 clip-path를 사용하는 것은 clip보다 덜 지원됩니다. 하지만 clip는 지원 중단되었습니다. 실제로 하지만 절망하지 마세요. 원하는 해결책이 아닙니다.

.menu {
  position: absolute;
  clip: rect(0px 112px 175px 0px);
  transition: clip 600ms ease-out;
}

.menu--collapsed {
  clip: rect(0px 70px 34px 0px);
}

메뉴 요소의 widthheight에 애니메이션을 적용하는 것보다 더 나은 방법이지만 이 접근 방식의 단점은 여전히 페인트를 트리거한다는 것입니다. 또한 clip 속성을 사용하려면 작업이 실행되는 요소가 절대적으로 또는 고정된 위치에 있어야 합니다. 이 경우 약간의 랭글링이 필요할 수 있습니다.

좋음: 배율 애니메이션

이 효과에는 무언가가 커지고 작아지므로 배율 변환을 사용할 수 있습니다. 이는 변환을 변경하는 데 레이아웃 또는 페인트가 필요하지 않으며 브라우저가 GPU에 전달할 수 있는 작업이므로 좋은 소식입니다. 즉, 효과가 가속화되고 60fps에 도달할 가능성이 크게 높아집니다.

렌더링 성능의 대부분의 측면과 마찬가지로 이 접근 방식의 단점은 약간의 설정이 필요하다는 점입니다. 그만한 가치가 있습니다.

1단계: 시작 및 종료 상태 계산

크기 조정 애니메이션을 사용하는 접근 방식을 사용하는 경우 첫 번째 단계는 메뉴가 접힐 때와 펼쳐질 때 모두 크기가 필요한지 알려주는 요소를 읽는 것입니다. 어떤 상황에서는 이러한 두 가지 정보를 한 번에 가져올 수 없고, 구성요소의 다양한 상태를 읽을 수 있도록 일부 클래스를 전환해야 할 수도 있습니다. 그러나 이렇게 해야 하는 경우 주의해야 합니다. getBoundingClientRect() (또는 offsetWidthoffsetHeight)는 마지막으로 실행된 이후 스타일이 변경된 경우 브라우저가 스타일과 레이아웃 패스를 실행하도록 강제합니다.

function calculateCollapsedScale () {
    // The menu title can act as the marker for the collapsed state.
    const collapsed = menuTitle.getBoundingClientRect();

    // Whereas the menu as a whole (title plus items) can act as
    // a proxy for the expanded state.
    const expanded = menu.getBoundingClientRect();
    return {
    x: collapsed.width / expanded.width,
    y: collapsed.height / expanded.height
    };
}

메뉴와 같은 항목의 경우 자연스러운 크기 (1, 1)로 시작할 것이라는 합리적인 가정을 할 수 있습니다. 이 자연스러운 배율은 펼친 상태를 나타냅니다. 즉, 축소된 버전 (위에서 계산됨)에서 자연스러운 크기까지 다시 애니메이션해야 합니다.

하지만 분명히 이렇게 하면 메뉴의 내용도 늘어납니다. 그렇지 않습니까? 글쎄, 아래에서 보시는 것처럼 그렇습니다.

그렇다면 어떻게 해야 할까요? 콘텐츠에 counter- 변환을 적용할 수 있습니다. 예를 들어 컨테이너를 보통 크기의 1/5로 축소하면 콘텐츠가 눌리지 않도록 콘텐츠를 5배 counter-할 수 있습니다. 이 경우 다음 두 가지 사항에 유의해야 합니다.

  1. 카운터 변환은 확장 작업이기도 합니다. 이는 컨테이너의 애니메이션처럼 가속화할 수 있으므로 좋은 방법입니다. 애니메이션 대상 요소에 자체 컴포지터 레이어가 있어야 하며 (GPU가 도움이 되도록 함) 요소에 will-change: transform을 추가하거나 이전 브라우저를 지원해야 하는 경우 backface-visiblity: hidden를 추가하면 됩니다.

  2. 카운터 변환은 프레임별로 계산되어야 합니다. 이 경우 좀 더 까다로울 수 있습니다. 애니메이션이 CSS에 있고 이징 함수를 사용한다고 가정할 때 카운터 변환을 애니메이션 처리할 때 이징 자체를 상쇄해야 하기 때문입니다. 그러나 cubic-bezier(0, 0, 0.3, 1)의 역곡선을 계산하는 것만으로는 명확하게 알 수 없습니다.

JavaScript를 사용하여 효과를 애니메이션으로 만들어 보고 싶을 수 있습니다. 그런 다음 이징 방정식을 사용하여 프레임당 배율 및 카운터 배율 값을 계산할 수 있습니다. 자바스크립트 기반 애니메이션의 단점은 자바스크립트가 실행되는 기본 스레드가 다른 작업으로 인해 사용 중일 때 발생합니다. 즉, 애니메이션이 끊기거나 완전히 멈출 수 있는데, 이는 UX에 적합하지 않습니다.

2단계: 즉석에서 CSS 애니메이션 빌드

처음에는 이상하게 보일 수도 있지만 이 해결 방법은 자체 이징 함수를 사용하여 키프레임이 적용된 애니메이션을 동적으로 만들고 메뉴에서 사용할 수 있도록 페이지에 삽입하는 것입니다. (이 문제를 알려주신 Chrome 엔지니어인 로버트 플랙님께도 감사드립니다.) 이 기능의 주요 이점은 변환을 변경하는 키프레임 형식의 애니메이션을 컴포지터에서 실행할 수 있다는 것입니다. 즉, 기본 스레드의 작업에 영향을 받지 않습니다.

키프레임 애니메이션을 만들려면 0에서 100까지 조정하고 요소와 콘텐츠에 필요한 배율 값을 계산합니다. 그런 다음 문자열로 압축할 수 있으며, 이 문자열은 페이지에 스타일 요소로 삽입할 수 있습니다. 스타일을 삽입하면 페이지에 스타일 재계산 패스가 발생하게 됩니다. 이는 브라우저에서 추가로 수행해야 하는 작업이지만, 구성요소가 부팅될 때 한 번만 실행합니다.

function createKeyframeAnimation () {
    // Figure out the size of the element when collapsed.
    let {x, y} = calculateCollapsedScale();
    let animation = '';
    let inverseAnimation = '';

    for (let step = 0; step <= 100; step++) {
    // Remap the step value to an eased one.
    let easedStep = ease(step / 100);

    // Calculate the scale of the element.
    const xScale = x + (1 - x) * easedStep;
    const yScale = y + (1 - y) * easedStep;

    animation += `${step}% {
        transform: scale(${xScale}, ${yScale});
    }`;

    // And now the inverse for the contents.
    const invXScale = 1 / xScale;
    const invYScale = 1 / yScale;
    inverseAnimation += `${step}% {
        transform: scale(${invXScale}, ${invYScale});
    }`;

    }

    return `
    @keyframes menuAnimation {
    ${animation}
    }

    @keyframes menuContentsAnimation {
    ${inverseAnimation}
    }`;
}

끝없는 호기심으로 for 루프 내부의 ease() 함수에 관해 궁금할 수 있습니다. 이와 같은 값을 사용하여 0에서 1까지의 값을 이징된 값에 매핑할 수 있습니다.

function ease (v, pow=4) {
  return 1 - Math.pow(1 - v, pow);
}

Google 검색을 사용하여 그 모양을 표시할 수도 있습니다. 편해요! 다른 이징 등식이 필요한 경우 전체 힙이 포함되어 있는 Soledad Penadés의 Tween.js를 확인하세요.

3단계: CSS 애니메이션 사용 설정

이러한 애니메이션을 만들고 JavaScript로 페이지에 베이킹한 후 마지막 단계는 애니메이션을 사용 설정하는 클래스를 전환하는 것입니다.

.menu--expanded {
  animation-name: menuAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

.menu__contents--expanded {
  animation-name: menuContentsAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

이렇게 하면 이전 단계에서 만든 애니메이션이 실행됩니다. 베이킹된 애니메이션은 이미 이징되었기 때문에 타이밍 함수를 linear로 설정해야 합니다. 그러지 않으면 각 키프레임 사이가 아주 이상하게 보일 것입니다.

요소를 다시 축소하는 경우 두 가지 옵션이 있습니다. CSS 애니메이션을 업데이트하면 앞으로가 아니라 역방향으로 실행할 수 있습니다. 이렇게 하면 잘 작동하지만 애니메이션의 '느낌'이 반전됩니다. 따라서 ease-out 곡선을 사용하면 역방향이 in 완만해져서 느긋한 느낌을 줄 수 있습니다. 더 적절한 해결책은 요소를 접을 수 있는 두 번째 애니메이션 쌍을 만드는 것입니다. 확장 키프레임 애니메이션과 정확히 동일한 방식으로 만들 수 있지만 시작 값과 종료 값이 전환되어 있습니다.

const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;

고급 버전: 원형 표시

이 기법을 사용하여 원형 펼치기 및 접기 애니메이션을 만들 수도 있습니다.

이 원칙은 요소를 확장하고 직계 하위 요소를 역으로 조정하는 이전 버전과 대체로 동일합니다. 이 경우 확장 중인 요소의 border-radius은 50%로 원형이 되고 overflow: hidden가 있는 다른 요소에 의해 래핑됩니다. 즉, 원이 요소 경계 밖으로 확장되는 것을 볼 수 없습니다.

이 특정 변형에 대한 경고: Chrome에서는 텍스트의 배율과 카운터 배율로 인한 반올림 오류로 인해 애니메이션이 진행되는 동안 DPI가 낮은 화면에서 텍스트가 흐릿해집니다. 이와 관련된 자세한 내용이 궁금하다면 별표표시하여 팔로우할 수 있는 버그를 제출하세요.

원형 확장 효과의 코드는 GitHub 저장소에서 확인할 수 있습니다.

결론

지금까지 배율 변환을 사용하여 성능 기준에 맞는 클립 애니메이션을 수행하는 방법을 알아봤습니다. 완벽한 환경에서는 클립 애니메이션이 가속화되는 것이 좋지만 (제이크 아치볼드가 만든 Chromium 버그가 있음) 그때까지는 clip 또는 clip-path를 애니메이션 처리할 때 주의하고 width 또는 height 애니메이션을 사용하지 않도록 주의해야 합니다.

이러한 효과에는 웹 애니메이션을 사용하는 것이 편리합니다. 웹 애니메이션에는 JavaScript API가 있지만 transformopacity에만 애니메이션을 적용하는 경우 컴포지터 스레드에서 실행될 수 있기 때문입니다. 안타깝게도 웹 애니메이션은 제대로 지원되지 않습니다. 하지만 점진적 개선을 통해 제공되는 경우 사용할 수 있습니다.

if ('animate' in HTMLElement.prototype) {
    // Animate with Web Animations.
} else {
    // Fall back to generated CSS Animations or JS.
}

이 변경사항이 적용될 때까지는 자바스크립트 기반 라이브러리를 사용하여 애니메이션을 만들 수 있지만 CSS 애니메이션을 대신 사용하여 더 안정적인 성능을 얻을 수 있습니다. 마찬가지로 앱이 이미 애니메이션에 JavaScript를 사용하고 있는 경우 최소한 기존 코드베이스와 일관성을 유지하는 것이 더 나을 수 있습니다.

이 효과의 코드를 살펴보려면 UI 요소 샘플 GitHub 저장소를 살펴보고 늘 그랬듯 아래 댓글에 어떻게 작업을 진행했는지 알려주세요.