2025년 웹 애니메이션 성능 티어 리스트로 웹 애니메이션을 빠르게도, 느리게도 만드는 요소와 그 사이의 모든 것을 알아보세요.
MotionDocsExamplesTutorialsAI KitMotion+
Magazine/기획
2025년 웹 애니메이션 성능 티어 리스트를 통해 웹 애니메이션을 빠르게도, 느리게도 만드는 요소와 그 사이의 모든 것을 알아보세요.
Matt Perry2025년 11월 5일
애니메이션 성능은 제가 가장 자주 받는 질문 주제 중 하나입니다. 그럴 만도 합니다. 우리가 애니메이션 UI를 만드는 이유는 전체적으로 더 부드럽고 반응성 있게 느끼도록 하기 위해서이기 때문입니다. 이런 애니메이션이 제대로 성능을 내지 못하면, 전체 경험에 적극적으로 해를 끼치게 됩니다.
하지만 성능은 종종 일종의 비전처럼 느껴질 수 있습니다. 범위가 넓고, 극도로 미묘하며, 트레이드오프로 가득한 주제니까요.
이 글에서는 웹 애니메이션 성능에 대해 제가 알고 있는 모든 것을 공유하려고 합니다. will-change를 언제 써야 하는지(그리고 언제 쓰지 말아야 하는지), 왜 CSS 변수가 사실 성능에 좋지 않은지, 하드웨어 가속이 무엇을 의미하는지까지, 전부 다 다룰 것입니다.
네, 기술적인 세부 사항까지 들어가고, 이런 미묘한 차이와 트레이드오프를 이야기할 것입니다. 동시에 모든 내용을 애니메이션 성능 티어 리스트로 분류해서, 어떤 기법을 우선적으로 써야 하는지, 어떤 것은 주의해서 써야 하는지, 무엇은 완전히 피해야 하는지를 쉽게 기억할 수 있도록 하겠습니다.
순위를 매기기 전에, 브라우저의 렌더 파이프라인에 대해 조금 이해할 필요가 있습니다.
이것은 브라우저가 모든 HTML, CSS, 폰트, 이미지를 받아서, 결국 화면에 보이는 최종 이미지로 바꾸는 과정입니다.
각 브라우저는 세부 구현이 다르지만, 모두 같은 큰 단계들을 따릅니다. 먼저 각 요소에 적용될 스타일 계산을 해야 합니다.
그 다음, 아래 순서대로 실행되는 세 가지 렌더 단계가 있습니다.
**레이아웃:**모든 요소의 기하 정보를 계산합니다. 어디에 있는가? 얼마나 큰가? 이 답은 width, position, display 같은 규칙에 의해 결정됩니다.
**페인트:**어떤 요소를 레이어로 묶을지 결정하고 픽셀을 그립니다. background-color, color 같은 값을 바꾸면 페인트가 발생합니다.
**컴포지트:**이렇게 분리된 이미지들을 다시 합칩니다. transform, filter 같은 값을 사용하면 페인트를 다시 하지 않고도 컴포지트된 레이어를 조작할 수 있습니다.
여기서 중요한 점은 한 단계를 트리거하면 그 이후의 모든 단계도 함께 트리거된다는 것입니다. 다시 말해, 레이아웃을 트리거하면 페인트와 컴포지트도 해야 합니다. 반대로 컴포지트만 트리거하면 다른 단계는 다시 실행할 필요가 없습니다.
각 단계 실행의 정확한 비용(시간 기준)은 전적으로 상황에 따라 다르며, 이를 프로파일링하는 것만으로도 별도의 글이 나올 수 있습니다. 하지만 예를 들어 페인트를 트리거하는 것은 컴포지트를 트리거하는 것보다 항상 더 비쌉니다. 왜냐하면 페인트를 하면 그 뒤에 반드시 컴포지트도 해야 하기 때문입니다.
추가로 알아야 할 점은, 이런 단계 대부분이 동기적으로, 즉 차례대로, "메인 스레드"에서 일어난다는 것입니다. 메인 스레드는 여러분의 JavaScript와 기타 브라우저 작업 대부분이 일어나는 CPU 프로세스입니다.
따라서 메인 스레드가 바쁘면 렌더 파이프라인은 화면을 갱신하지 못하고 막히게 되며, 이것이 버벅거리는 애니메이션으로 나타납니다.
하지만 "컴포지터 스레드"도 있습니다. 스타일 변경(예: transform)이 컴포지트 단계만 트리거한다면, 종종 애니메이션 자체를 컴포지터 스레드에서 실행할 수 있습니다. 이런 경우 메인 스레드가 막혀도 애니메이션은 부드럽게 유지됩니다.
이제 브라우저가 실제로 어떻게 동작하는지 이해했으니, 애니메이션 기법을 평가할 기본 티어를 만들 수 있습니다.
**S-티어:**애니메이션 전체를 컴포지터 스레드에서 실행할 수 있음.
**A-티어:**메인 스레드에서 실행되지만 컴포지트만 트리거함.
**B-티어:**일부 DOM 측정 설정이 필요하지만, 그 이후에는 A 또는 S-티어 애니메이션으로 실행됨.
**C-티어:**페인트를 트리거함.
**D-티어:**레이아웃을 트리거함.
**F-티어:**곧 보시게 됩니다!
물론 위 설명에는 수많은 단서가 있습니다. 그래서 이 글의 나머지 내용이 필요한 것입니다.
앞서 이야기했듯, 우리가 성능 좋은 애니메이션을 원하는 이유는 부드럽고 반응성 있는 UI를 만들기 위해서입니다. 따라서 어떤 애니메이션 기법이 성능 수치상으로는 좋더라도, 어떤 이유로든 반응성이 좋게 느껴지지 않는다면? 그건 등급이 내려갑니다.
이 모든 것과 그 이상을 다룰 예정입니다. 그럼 시작해봅시다!
사이트 감사
어떤 웹사이트든 애니메이션 성능을 점검할 수 있습니다. 리포트는 단순한 등급 체계와 자세한 분석을 제공하며, 수정 방법은 AI 에이전트에 바로 붙여넣을 수 있는 프롬프트로 제공됩니다.
무료 감사 실행 Powered by MotionScore.
S
티어
컴포지트 전용
S-티어 애니메이션은 전부 컴포지터 스레드에서 실행될 수 있습니다. 메인 스레드에서 무거운 작업이 발생해도 S-티어 애니메이션에는 영향을 주지 않으며, 60fps나 120fps를 매끄럽게 유지합니다.
일반적으로 컴포지터를 통해 애니메이션할 수 있는 스타일은 transform, opacity, filter, clip-path입니다.
이 값들을 CSS, Web Animations API (WAAPI), 또는 WAAPI를 지원하는 애니메이션 라이브러리(Motion 같은)를 사용해 애니메이션하면 메인 스레드가 바빠도 애니메이션이 부드럽게 유지됩니다.
animate(".box", { opacity: 1 })
반대로 requestAnimationFrame 기반 JavaScript 라이브러리(GSAP 같은)는 동일한 속성을 업데이트할 수는 있지만, 애니메이션 자체 는 메인 스레드에서 실행됩니다. 즉, 이 애니메이션은 중단될 수 있습니다. 보통은 부드럽지만 메인 스레드가 막힐 때마다 끊김에 취약합니다.
하드웨어 가속 애니메이션의 또 다른 핵심 장점은 요소의 시각적 상태를 업데이트할 때 스타일을 다시 계산하지 않아도 된다는 점입니다.
기억하세요. 이것은 브라우저가 레이아웃, 페인트, 컴포지트 단계를 트리거할지 결정하기 전에 실행되는 메인 스레드 프로세스입니다.
스타일 재계산 자체도 매우 비쌀 수 있습니다. 특히 DOM 구조가 복잡하거나, 느린 CSS 선택자를 사용하는 페이지에서는 더욱 그렇습니다.
이런 컴포지터 값 중 하나를 Scroll Timeline 또는 View Timeline으로 애니메이션하는 경우, CSS든 WAAPI든 Motion의 scroll() 함수든 상관없이, 이런 스크롤 애니메이션도 하드웨어 가속됩니다.
scroll(
animate(element, { opacity: [0, 1] })
)
하지만 이 애니메이션이 훌륭하게 느껴지는 더 큰 이유가 하나 더 있습니다. 스크롤 자체가 컴포지터 스레드에서 실행되기 때문입니다.
스크롤은 UI 반응성을 유지하는 데 있어 아마 가장 중요한 상호작용이기 때문에, 브라우저는 이를 메인 스레드 바깥에서 처리합니다.
즉, scrollTop을 읽어서 스크롤 애니메이션을 구동하면, 애니메이션은 스크롤보다 한 프레임 늦게 업데이트되는 것처럼 보이거나, 심지어 전혀 다른 프레임레이트로 갱신될 수도 있습니다. 이런 종류의 스크롤 애니메이션은 쉽게 D-티어로 떨어집니다.
제 경험상 이 효과는 Safari에서 훨씬 더 두드러지며, 역사적으로 요소 위치를 스크롤에 동기화할 때 transform을 업데이트하기보다 position: sticky 또는 fixed를 쓰라고 권장되어 온 주된 이유이기도 합니다.
하드웨어 가속 애니메이션에는 흥미로운 함정이 하나 있습니다. 이를 지원하려면 브라우저는 사실상 별도의 두 애니메이션 엔진을 유지해야 합니다. 하나는 CPU 기반 메인 스레드용, 다른 하나는 GPU 컴포지터 스레드용입니다.
많은 사람이 모르는 사실이 있습니다. 컴포지터 애니메이션 엔진은 명세를 완전히 구현할 필요가 없습니다. 사용자가 컴포지터 스레드가 지원하지 않는 기능을 요청하면 브라우저는 그냥 메인 스레드에서 실행하면 되고, 그 과정에서 하드웨어 가속은 조용히 사라집니다.
여기서 가장 대표적인 문제가 Safari입니다. Safari는 아직 전용 컴포지터 엔진이 없고 대신 macOS의 Core Animation 프레임워크를 재사용합니다. 그래서 애니메이션이 Core Animation이 지원하지 않는 기능, 예를 들어 1이 아닌 playbackRate를 요구하면 그 애니메이션은 더 이상 하드웨어 가속되지 않습니다.
마찬가지로, 어떤 값들은 컴포지터 엔진에서 지원되지 않을 수 있습니다. 예를 들어 Chrome은 가속 애니메이션을 추가한 뒤 한참이 지나서야 % 기반 translate 값을 지원했습니다(long after).
S-티어 애니메이션의 또 다른, 말 그대로 큰, 성능 관련 주의점은 이것들이 항상 레이어 생성이 필요하다는 점입니다.
레이어는 하나의 요소 또는 여러 요소를 함께 페인트한 것입니다. 본질적으로 컴포지터가 독립적으로 이동, 변형, 페이드할 수 있는 하나의 이미지이며, 최종적으로 모든 것을 하나의 이미지로 묶기 전에(즉 컴포지팅하기 전에) 존재합니다.
이 이미지들은 여러분이 깨닫지 못하는 사이에 엄청나게 커질 수 있습니다. 데스크톱 GPU는 대개 이를 잘 처리하지만, 모바일 기기에서는 GPU 메모리를 쉽게 초과해 웹사이트가 크래시날 수 있습니다.
전형적인 예가 ticker/marquee 애니메이션입니다. 복제된 아이템의 긴 목록이 계속해서 스크롤되는 경우죠. 복제된 각 요소가 거대한 레이어에 기여하고, 이 레이어는 종종 뷰포트 너비의 여러 배에 달합니다. 그래서 Motion+ Ticker는 레이어 크기를 제한하기 위해 reprojection renderer를 사용해 복제 요소를 줄이거나 제거합니다. GPU 메모리를 제어하기 위해 가끔 페인트를 허용하는, 의도적인 트레이드오프입니다.
여기서 혼란을 주는 요소가 blur입니다. 맞습니다. filter: blur()는 하드웨어 가속됩니다. 하지만 공짜는 아닙니다. blur의 비용은 blur 반경이 1픽셀씩 늘어날 때마다, 그리고 레이어가 커질수록 급격히 증가할 수 있습니다. 그리고 blur 자체가 레이어를 더 크게 만들어 메모리를 더 압박합니다. 그래서 저는 오래전에 Framer에 10px를 넘는 blur 값에 경고 플래그를 추가했습니다.

A
티어
메인 스레드 컴포지트
A-티어 애니메이션은 transform이나 opacity 같은 컴포지트 값을 바꾸지만, 구동은 메인 스레드에서 이루어집니다. 이런 값의 변경은 컴포지트만 트리거하므로, 이상적인 상황에서는 애니메이션 성능이 매우 좋지만, 메인 스레드의 다른 작업에 의해 중단될 수 있습니다.
어떤 스타일이 컴포지트만 트리거하려면, 그 스타일이 적용되는 요소는 먼저 레이어로 승격되어 있어야 합니다. 그렇지 않으면 transform, opacity 같은 값을 업데이트해도 여전히 페인트가 트리거됩니다.
궁극적으로 어떤 요소가 레이어가 될지는 브라우저가 결정합니다. 그 이유는 브라우저마다 다르고, 매우 많고도 신비로운데, 예를 들면 다음과 같습니다.
연결된 CSS/WAAPI transform(등) 애니메이션
3D transform
position: fixed 또는 sticky
backdrop-filter
다른 레이어와 겹침
또한 will-change를 사용해 요소가 레이어가 되어야 한다는 힌트를 줄 수도 있습니다.
dialog {
will-change: transform, opacity;
}
MDN 문서는 will-change를 아껴 써야 한다고(커다란 빨간 상자로) 경고합니다. 핵심 우려는, 너무 많은 레이어를 만들거나 너무 큰 레이어를 만들면 GPU 메모리 예산을 초과할 수 있다는 점입니다.
그래서 Motion은 애니메이션되는 모든 요소에 무작정 will-change를 뿌리지 않습니다. 레이어화는 여러분이 알고 있어야 하고, 의식적으로 사용해야 하는 도구입니다.
요소가 레이어가 되면, element.style을 통해 컴포지트 값을 바꾸는 것은 페인트를 건너뛰고 컴포지트만 트리거합니다.
element.style.transform = "translateX(100px)"
이것은 GSAP 같은 전통적인 JS 애니메이션 라이브러리나, 직접 구현한 requestAnimationFrame 방식에도 해당합니다. Motion이 독립적인 transform을 애니메이션하는 방식도 마찬가지입니다.
animate(element, { x: 100 })
물론 JS 애니메이션을 실행할 때는 추가적인 CPU 오버헤드가 있습니다(사실 메인 스레드 CSS/WAAPI 애니메이션도 마찬가지입니다). 하지만 제 경험상, 느린 애니메이션의 원인이 JS 런타임인 경우는 거의 없고, 거의 항상 비용이 큰 렌더링이 원인입니다.
어떤 경우에는, 예를 들어 작은 요소 수천 개를 애니메이션할 때, 벤치마크는 requestAnimationFrame이나 GSAP가 하드웨어 가속 애니메이션보다 더 빠를 수 있음을 보여줍니다.
물론 요소 수천 개를 애니메이션하고 있다면, 아마 shader를 쓰는 편이 더 나을 가능성이 큽니다.
shader는 어떤 픽셀을 어떤 색으로 칠할지 결정하는 작은 WebGL/WebGPU 프로그램입니다. 대규모 병렬로 실행되기 때문에 매우 뛰어난 성능으로 복잡한 효과를 만들어낼 수 있습니다.
하지만 shader 업데이트 역시 requestAnimationFrame을 통해 스케줄되므로, 타이밍 제어는 메인 스레드가 맡습니다. 그래서 shader는 S-티어가 아닙니다. 렌더링은 엄청나게 빠를 수 있지만, 메인 스레드가 막히면 여전히 프레임을 놓칠 수 있습니다.
IntersectionObserverIntersectionObserver는 요소가 뷰포트에 들어오거나 나갈 때를 감지하는 가장 성능 좋은 방법입니다. 이것이 Motion의 inView 함수와 whileInView prop 뒤에 있는 비밀 소스입니다.
observer는 scrollTop 읽기나 다른 DOM 측정 없이, 백그라운드 스레드에서 뷰포트 대비 요소의 가시성을 효율적으로 추적합니다. 따라서 메인 스레드에 거의 부담을 주지 않습니다.
이렇게 가볍기 때문에 스크롤 트리거 애니메이션에 이상적입니다.
inView(element, () => {
animate(element, { x: -100 })
})
<motion.div whileInView={{ opacity: 1 }} />
하지만 IntersectionObserver에는 종종 간과되는 또 하나의 초능력이 있습니다. 바로 화면 밖 애니메이션 비활성화입니다.
페이지가 열려 있는 한 계속 재생되는 장시간 애니메이션이 있다고 상상해봅시다.
.in-view {
animation: spin 2s infinite;
}
IntersectionObserver를 사용하면, 이 애니메이션이 요소 자체가 실제로 화면 안에 있을 때만 재생되도록 만들 수 있습니다.
inView(element, () => {
element.classList.add("in-view")
return () => element.classList.remove("in-view")
})
.in-view {
animation: spin 2s infinite;
}
이것이 Motion+ Ticker가 장시간 실행되는 애니메이션의 성능과 배터리 효율을 유지하는 방식입니다. ticker가 보일 때만 실행되도록 하는 것이죠.
B
티어
A-티어 + DOM 측정
B-티어 애니메이션은 A 또는 S-티어 애니메이션에 초기 비용 하나가 추가된 형태입니다. 바로 DOM 측정입니다.
Motion에는 강력한 레이아웃 애니메이션 엔진이 있어서, 요소의 크기와 위치를 매 프레임 레이아웃을 트리거하지 않고도 애니메이션할 수 있습니다.
<motion.div layout />
이는 transform 스타일만 애니메이션함으로써 가능합니다. 따라서 요소를 width: 500px에서 1000px으로 애니메이션하는 대신, width: 1000px인 요소를 scale(0.5)에서 1로 애니메이션할 수 있습니다.
width/height 대신 scale을 애니메이션할 때의 단점은 요소가 왜곡될 수 있다는 것입니다. 하지만 Motion은 요소와 자식 요소들에 대한 역변환과 border-radius를 매 프레임 계산하여 이를 보정합니다.
이 모든 것을 위해 먼저 FLIP (First, Last, Invert, Play) 기법이라고 알려진 방식을 사용해 몇 가지 초기 측정을 수행해야 합니다.
스케일 보정(그리고 몇 가지 다른 이점)을 가능하게 하는 Motion의 프레임별 계산 때문에, Motion은 이런 종류의 애니메이션을 메인 스레드(A-티어)에서 수행합니다. 하지만 우리가 이것을 직접 만들고, 단 하나의 깊이의 요소만 다룬다면, 충분히 S-티어 하드웨어 가속 애니메이션으로 만들 수 있습니다.
animate(element, { transform: [delta, "none" ]})
이 한 번의 초기 측정은 가장 비싼 종류의 애니메이션을 가장 덜 비싼 종류 중 하나로 바꿔줍니다.
C
티어
페인트 트리거
C-티어 애니메이션은 페인트 단계를 트리거합니다. 즉, 브라우저가 스타일을 다시 계산하고 영향을 받는 레이어를 다시 그리도록 강제합니다.
어떤 스타일이 페인트와 레이아웃을 트리거하는지 알려주는 리스트는 많이 있습니다(그리고 종종 오래되었습니다). 하지만 더 쉬운 경험칙은 상식적으로 생각하는 것입니다. 값이 기하 구조를 바꾸면 width, flex 같은 것은 레이아웃을 트리거합니다. 반면 background-color, color, border-radius처럼 무언가의 외양만 바꾼다면 이는 페인트 단계에서 처리할 수 있습니다.
앞서 보았듯 transform, opacity 같은 값도 레이어가 아닌 요소에서 바뀌면 페인트를 트리거할 수 있습니다.
페인트 애니메이션이 본질적으로 "나쁘다"고 단정할 수는 없습니다. 다만 더 큰 레이어를 페인트하는 것이 더 비싸다는 사실을 염두에 두어야 합니다. 버튼 색상을 바꾸는 것은 아마 괜찮겠지만, 페이지 전체의 색상을 애니메이션하면 큰 화면에서는 비싸질 수 있습니다.
그 외에도, filter: blur에서 보았듯 모든 연산이 동일하지는 않습니다. background-color를 애니메이션하는 것은 mask-image나 background-image gradient를 애니메이션하는 것보다 저렴합니다. 후자의 경우 완전히 새로운 gradient 이미지를 다시 그려야 하기 때문입니다.
CSS 변수는 강력하지만, 성능 면에서는 놀랄 만큼 좋지 않습니다.
어떤 JS 애니메이션 라이브러리로도 애니메이션할 수 있고, 점점 더 많은 브라우저가 @property를 지원하면서 CSS나 WAAPI로도 애니메이션할 수 있게 되고 있습니다.
CSS 변수의 첫 번째 문제이자, 이것이 C-티어에 위치하는 이유는, 하나를 바꾸면 영향받는 요소에서 항상 페인트를 트리거한다는 것입니다.
이는 요소가 opacity 같은 컴포지터 값 안에서만 그 변수를 사용하더라도 마찬가지입니다.
div {
--progress: 0;
opacity: var(--progress);
}
이것은 원래 성능이 좋은 애니메이션을 순식간에 격하시킬 수 있는 빠른 방법입니다.
하지만 CSS 변수 안에 숨어 있는 진짜 F-티어 성능 킬러는 상속입니다.
웹의 많은 "영리한" 데모는 전역 CSS 변수를 애니메이션하고, 그러면 그 값이 트리 내부의 여러 요소에 상속됩니다.
html {
--progress: 0;
}
.box {
transform: translateY(calc(var(--progress) * 100px));
}
문제는 이 값을 바꾸는 것이 트리 전체의 스타일을 무효화할 수 있고, 그 결과 전체를 다시 계산하게 만든다는 점입니다. var(—progress)가 단 몇 군데에서만 쓰이거나, 아예 사용되지 않더라도 그렇습니다.
저는 최근 프레임마다 전역 CSS 변수를 업데이트하는 사이트를 발견했습니다. 그 결과 1300개가 넘는 요소에서 스타일 재계산이 발생했고, 프레임당 무려 8ms가 들었습니다. 120fps 애니메이션의 전체 예산이 바로 이 정도인데, 단지 어떤 요소를 렌더링해야 하는지 결정하는 데만 그 비용이 든 것입니다.
이 CSS 변수를 목표 지향적인 JavaScript 스타일 업데이트로 바꾸자 이 비용은 거의 0이 되었습니다. 8ms에서 나노초 수준으로 줄어든 것입니다.
box.style.transform = translateY(${progress * 100}px)``
이 패턴은 DOM이 작고 CSS 선택자가 단순한 CodeSandbox 데모에서는 잘 동작합니다. 하지만 실제 제품 환경에서는 수천 개의 노드와 복잡한 선택자 때문에, 상속된 변수의 비용은 예측하기 어렵습니다. 현실 세계에서는 이 패턴이 성능을 완전히 폭발시킬 수 있습니다.
그렇다고 CSS 변수를 애니메이션하는 것이 항상 위험한 것은 아닙니다. transform 같은 값에는 절대 권하지 않지만, 어차피 페인트를 트리거하는 mask-image 같은 스타일에서는 안전하게 사용할 수 있습니다.
상속 폭탄을 피하려면 두 가지 선택지가 있습니다.
CSS 변수를 전역으로 설정하는 대신, 실제 사용 지점에 최대한 가깝게 설정하세요. 즉 html에 두는 대신 특정 section에 두는 것입니다.
또는 이상적으로는 아예 상속하지 않게 하세요. @property를 사용하면 그 CSS 변수가 현재 요소에만 영향을 주도록 정의할 수 있습니다.
@property --progress {
syntax: "<number>";
inherits: false;
initial-value: 0;
}
이렇게 하면 변수 변경이 DOM 전체로 연쇄적으로 퍼지는 것을 막아, 스타일 재계산의 들불 번짐 위험을 제거할 수 있습니다.
변수를 등록할 때는 한 번의 스타일 재계산 비용이 들기 때문에, 여러 등록을 한 번에 묶는 것이 더 낫습니다.
때로는 이것이 적용되지 않을 수도 있습니다. 애니메이션이 상속에 의존할 수도 있으니까요(다만 이런 경우라면 아마 목표 지향적인 JS 업데이트가 더 나을 것입니다). 하지만 심지어 사용되지 않는 변수의 변경조차도 큰 문제가 될 수 있다는 점을 감안하면, inherits: false는 안전한 기본값입니다.
path 데이터(d), 원의 위치와 반지름(cx/cy/r) 같은 네이티브 SVG 속성을 애니메이션하면 브라우저는 매 프레임 도형을 다시 페인트해야 합니다.
예를 들어 "그려지는" 스타일의 애니메이션을 만들 때는 이것이 불가피할 수 있습니다. 하지만 SVG 그래픽을 이동하거나 크기를 조절할 때는 가능하면 transform을 사용하세요.
View Transitions API를 사용하면 완전히 다른 두 뷰 사이를 애니메이션할 수 있습니다.
기본적으로 이 API는 나가는 뷰와 들어오는 뷰를 스크린샷으로 찍은 다음 crossfade합니다. 이 crossfade는 opacity를 사용한 S-티어 하드웨어 가속 애니메이션으로 수행됩니다.
추가로, 일치하는 view-transition-name 스타일을 가진 요소의 크기와 위치를 transform(S-티어)과 width/height(D-티어)로 애니메이션할 수 있습니다.
그렇다면 전체 등급이 왜 C-티어일까요? 여기에는 추가로 작용하는 두 요소가 있기 때문입니다. 바로 중단 가능성과 격리입니다.
우리가 애니메이션 성능을 중요하게 여기는 이유는 UI를 반응성 있게 유지하기 위해서라고 했습니다. 여기서 또 하나 중요한 점은, 현재 상태에서 즉시 새로운 애니메이션으로 끊어 들어갈 수 있어야 한다는 것입니다. View Transitions는 그것이 불가능합니다.
선택지는 둘 중 하나뿐입니다. 현재 애니메이션이 끝날 때까지 기다리거나, 다음 애니메이션을 시작하기 전에 현재 애니메이션을 즉시 끝내는 것입니다.
어느 쪽이든 UI는 반응하지 않거나, 시각적으로 깨져 보입니다.
풀스크린 페이지 전환 같은 상황에서는 대체로 괜찮습니다. 하지만 인터랙티브한 요소가 이전 뷰와 새 뷰 모두에 남아 있는 상황에는 전혀 적합하지 않습니다.
Motion의 animateView는 중간에 끼어드는 View Transition을 큐잉하고 배치 처리함으로써 이 상황을 개선하려고 시도합니다. 곧 활성 애니메이션이 끊겼을 때 은근하게 "빨리감기" 하는 기능도 추가될 예정입니다. 하지만 이런 기법들은 해결책이라기보다 임시 보완책에 가깝습니다.
긍정적인 면도 있습니다. View Transitions는 레이아웃 트리거 비용을 제한하는 데 꽤 영리합니다. 이것은 다음에 살펴볼 내용과도 맞닿아 있는데, 핵심은 모든 레이아웃 계산이 같은 것은 아니라는 점입니다.
일치하는 각 view-transition-name 요소 쌍은 하나의 ::view-transition-group 요소로 표현됩니다. 이 요소 안에는 두 개의 요소만 들어 있으며, 각각 이전 요소와 새 요소의 스냅샷입니다.
이것은 매우 단순한 DOM이며, position: absolute를 사용해 주변 요소들과 분리된 레이아웃을 가집니다. 계산 비용이 저렴합니다.
따라서 네, 정의상 이것은 D-티어 애니메이션입니다. 하지만 가능한 한 가장 좋은 형태의 D-티어 애니메이션에 가깝고, 웹 애니메이션에는 절대적인 규칙이 없다는 또 하나의 증거이기도 합니다.
기본적으로 ::view-transition-group는 두 요소의 크기가 완전히 같더라도 항상 width와 height를 애니메이션합니다.
View Transition의 마법사 Bramus는 이 키프레임들을 제거하고 하드웨어 가속되는 위치 애니메이션만 남기는 기법을 찾아냈습니다. 이렇게 하면 전체 애니메이션이 S-티어로 승격됩니다. 제 생각에는 이 기법이 명세 차원에서 보장되어야 하지만, 개념 증명만으로도 적어도 일부 상황에서는 이것이 가능하다는 점을 보여줍니다.
D
티어
레이아웃 트리거
D-티어 애니메이션은 레이아웃 렌더 단계를 트리거하며, 따라서 매 프레임 전체 렌더 파이프라인을 실행하게 됩니다.
이것은 프레임 예산에 엄청난 타격이 될 수 있습니다. 때로는 고성능 기기조차도 여기서 프레임 드롭이 발생합니다.
요소의 레이아웃을 바꾸면 하나 이상의 요소에 대한 기하 계산이 다시 수행됩니다.
레이아웃 변화는 페이지 전체로 파급될 수 있으며, width, margin, border, top, display, justify-content, grid-template-columns 등 아주 다양한 스타일이 레이아웃에 영향을 줄 수 있습니다.
앞서 보았듯, 모든 레이아웃 계산이 같은 것은 아닙니다. 비용은 무효화되는 트리의 크기와 복잡도에 따라 커집니다.
자식이 없는 작고 고립된 컴포넌트에서의 레이아웃 변경은 매우 저렴합니다. 하지만 형제 요소들에 둘러싸여 있고, 수백 개의 자식과 리플로우되는 텍스트를 포함한 최상위 컨테이너의 width를 애니메이션한다면, 그건 꽤 비용이 듭니다.
브라우저는 이미 레이아웃 재계산 범위를 제한하는 데 꽤 똑똑합니다. 예를 들어 position: absolute나 position: fixed 요소의 크기와 위치를 바꾼다고 해서 주변 요소들의 레이아웃까지 다시 계산되지는 않습니다. 그들의 레이아웃은 격리되어 있기 때문입니다.
또한 contain CSS 규칙을 사용해 특정 레이아웃이 독립적이라고 브라우저에 직접 알려줄 수도 있습니다. 이것은 어떤 요소 내부의 레이아웃 변화가 주변 요소의 레이아웃에는 영향을 주지 않는다고 브라우저에 알리는 것입니다.
F
티어
스래싱
F-티어는 웹 애니메이션의 대죄입니다. E-티어는 아예 만들지도 않았습니다. 이것이야말로 피해야 할 단계입니다. 즉시 탈락입니다.
스타일 및 레이아웃 스래싱은 DOM에 쓰기, 측정, 다시 쓰기, 다시 측정을 반복하는 과정입니다.
예를 들어 요소의 크기를 설정하고,
element.style.width = "100px"
그것을 다시 읽은 다음,
const width = element.offsetWidth
다시 설정합니다.
element.style.width = width * 2 + "px"
이런 식으로 계속된다면, 이것이 바로 스래싱입니다.
또는 좀 더 현실적인 사례를 보겠습니다.
const header = useRef()
useLayoutEffect(() => {
const element = header.current
// Read
if (element.scrollWidth > element.clientWidth) {
// Write
header.current.dataset.overflowing = "yes";
} else {
header.current.dataset.overflowing = "";
}
}, [text]);
return <div ref={header}>...</div>
이 컴포넌트 인스턴스가 하나뿐이라면 괜찮을 수 있습니다. 하지만 이 컴포넌트의 여러 버전을 렌더링하기 시작하면, 여러분은 심각한 스래싱 영역에 들어가게 됩니다.
이것은 성능에 엄청난 악영향을 주며, 각기 DOM을 읽고 쓰는 여러 라이브러리를 섞어 사용할 때 사이트에 아주 쉽게 도입될 수 있습니다.
DOM을 읽고 있다는 사실이 항상 분명한 것은 아닙니다. 예를 들어 Motion의 다음 애니메이션을 봅시다.
animate(element, { width: "auto" })
Motion은 auto가 무엇인지 어떻게 알까요? 일반적으로 JS 애니메이션 라이브러리는 이런 키프레임을 즉시 해석하는데, 이 과정에는 종종 읽기/쓰기가 포함됩니다.
하지만 Motion은 대신 WAAPI에서 영감을 받은 지연 키프레임 해석이라는 과정을 사용해, 모든 읽기와 쓰기가 배치 처리되도록 보장합니다. 그리고 바로 이 배치 처리 덕분에 Motion은 알려지지 않은 값에서 애니메이션을 시작할 때 GSAP보다 2.5배 빠르고, 단위 변환에서는 6배 빠릅니다.
Motion은 매 애니메이션 프레임마다 모든 읽기와 쓰기를 배치 처리하며, 이를 frame이라는 저수준 API로도 제공합니다. 이를 통해 다른 라이브러리 작성자나 개발자도 서로의 작업을 방해하거나 무심코 스래싱을 도입하는 일을 피할 수 있습니다.
let width = 0
frame.read(() => {
width = element.offsetWidth
frame.update(() => {
element.style.width = width * 2 + "px"
})
})
성능은 비전이 아닙니다. 하지만 예술이긴 합니다. 렌더 파이프라인이 어떻게 작동하는지 이해하면, 왜 어떤 애니메이션이 버벅이는지, 그리고 대신 무엇을 선택해야 하는지에 대한 감각이 생깁니다.
절대적인 규칙은 없습니다. 메모리, 레이어, 하드웨어 가속 등 모든 선택에는 서로 교차하는 트레이드오프가 있습니다. 제 경험상 성능 문제의 90%는 그저 큰 filter: blur 하나이긴 하지만, 이제 남은 10%도 더 잘 다룰 수 있기를 바랍니다.
거의 4000단어에 달하는 이 글은 예상보다 훨씬 길어졌지만, 여전히 몇 가지를 빠뜨렸거나 어떤 부분은 충분히 깊게 다루지 못했다는 느낌이 듭니다. 아직도 헷갈리는 것이 있나요? 아니면 위 주제 중 하나를 더 자세히 탐구하는 글을 원하시나요? 알려주세요!
추신: 이 글을 교정하고 사실 확인을 도와준 Framer의 Jacob과 Ivan에게 깊이 감사드립니다!
뉴스레터
애니메이션, 성능, 그리고 Motion 구축에 대한 깊이 있는 이야기. 새 이슈는 대략 한 달에 한 번 도착하며, 군더더기는 없습니다.
구독하기
매거진의 더 많은 글
How-To 2026년 1월 26일 ## 방법: 좋아하는 GSAP easing 함수를 하드웨어 가속하기 Web Animations API는 모든 브라우저에 포함되어 있지만 CSS 타이밍 함수만 지원합니다. GSAP의 easing 함수로 이를 더 강력하게 만들어봅시다! Matt Perry
Interview 2026년 1월 8일 ## 인터뷰: React Bits의 제작자 David Haz AI, 빌드 시스템, 생산성 등 다양한 주제로 React Bits의 제작자 David Haz와 이야기를 나눕니다. Matt Perry
Announcement 2025년 11월 25일 ## Motion+ Carousel 소개 React를 위한 Motion+ Carousel을 만나보세요. 무한 루프와 픽셀 단위로 정확한 스와이프를 제공하는, 성능 좋고 접근성 높은 컴포넌트입니다. Matt Perry
Feature 2025년 11월 17일 ## Motion의 독립: 1년 회고 Framer Motion을 Motion으로 독립시킨 지 정확히 1년이 되었습니다. 그동안 어땠는지, 그리고 첫해에 무엇을 배웠는지 이야기합니다. Matt Perry
Announcement 2025년 6월 5일 ## 궁극의 ticker 만들기 Ticker 컴포넌트는 흔한 장식 요소가 되어가고 있지만, 제대로 만드는 일은 쉽지 않습니다. CSS 컴포넌트의 문제와 Motion+ Ticker가 이를 어떻게 해결하려는지 살펴봅시다. Matt Perry
Audit 2025년 5월 7일 ## Motion으로 GTA VI 웹사이트 성능 끌어올리기 Rockstar의 아름다운 GTA VI 웹사이트는 Motion의 지연 키프레임 해석과 네이티브 브라우저 API 활용으로 더 빠르게 로드될 수 있습니다. Matt Perry
Announcement 2025년 3월 19일 ## Vue용 Motion 소개 Motion이 마침내 Vue에 도착했습니다. variants, scroll, layout animations, 그리고 Framer Motion에서 사랑받았던 모든 기능을 완비했습니다. Matt Perry
Announcement 2024년 11월 12일 ## Framer Motion은 이제 독립되었습니다, Motion을 소개합니다 Framer Motion은 이제 독립 프로젝트가 되었습니다. React와 모든 JavaScript 환경을 위한 새로운 애니메이션 라이브러리 Motion을 소개합니다. 이것이 여러분에게 의미하는 바를 설명합니다. Matt PerryMotion은 업계 최고의 지원을 받고 있습니다.
새 기능과 릴리스 업데이트.
구독하기
© 2026 Motion