본문으로 바로가기
기술

웹 애니메이션 성능 최적화 가이드 — 60fps를 지키는 실전 기법

A
AlwaysCorp 기술팀· 프론트엔드 개발 콘텐츠 전문
||12분 읽기
#애니메이션#CSS#Web Animations API#GSAP#Framer Motion#성능최적화#GPU가속#프론트엔드#60fps

왜 어떤 애니메이션은 부드럽고, 어떤 것은 버벅이는가

같은 요소를 움직이더라도 `left` 속성을 바꾸는 것과 `transform: translateX`를 쓰는 것은 전혀 다른 성능을 보여줍니다. 이 차이를 이해하지 못하면, 아무리 멋진 모션을 구현해도 사용자 경험은 오히려 나빠질 수 있습니다. Google의 연구에 따르면 100ms 이상의 애니메이션 지연은 사용자에게 "느리다"는 인식을 주며, 이는 사이트 이탈률 증가로 이어집니다.

브라우저의 렌더링 파이프라인부터 시작하여, 60fps 애니메이션을 안정적으로 구현하는 구체적인 방법을 다룹니다.

---

브라우저 렌더링 파이프라인 이해하기

브라우저가 화면을 그리는 과정은 크게 다섯 단계로 나뉩니다:

JavaScript → Style → Layout → Paint → Composite

각 단계에서 변경이 발생하면, 그 이후 모든 단계가 다시 실행됩니다. 즉, Layout이 변경되면 Paint와 Composite도 다시 수행되어야 하죠.

Layout을 유발하는 속성: `width`, `height`, `top`, `left`, `margin`, `padding`, `font-size` 등. 요소의 크기나 위치가 바뀌면 주변 요소의 배치도 재계산되므로 비용이 가장 큽니다.

Paint만 유발하는 속성: `color`, `background-color`, `box-shadow`, `border-color` 등. Layout 재계산 없이 픽셀만 다시 그립니다.

Composite만 유발하는 속성: `transform`, `opacity`. GPU에서 처리되므로 메인 스레드를 차단하지 않아 가장 성능이 좋습니다.

```css / ❌ Layout 유발 — 매 프레임마다 레이아웃 재계산 / .animate-bad { animation: slide-bad 0.3s ease; } @keyframes slide-bad { from { left: -100%; } to { left: 0; } }

/ ✅ Composite만 — GPU 가속, 메인 스레드 비차단 / .animate-good { animation: slide-good 0.3s ease; } @keyframes slide-good { from { transform: translateX(-100%); } to { transform: translateX(0); } } ```

브라우저 렌더링 파이프라인과 CSS 속성별 비용 — Layout, Paint, Composite
브라우저 렌더링 파이프라인과 CSS 속성별 비용 — Layout, Paint, Composite

---

CSS Transitions — 가장 간단한 애니메이션

상태 변화(hover, 클래스 토글 등)에 반응하는 단순한 애니메이션에는 CSS Transitions가 최적입니다.

```css .card { transform: translateY(0); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1); }

.card:hover { transform: translateY(-4px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); } ```

이징 함수의 선택

이징(easing)은 애니메이션의 "느낌"을 결정합니다. Material Design 가이드라인에서 권장하는 이징 값을 살펴보면:

```css / 표준 이징 — 대부분의 UI 전환 / transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);

/ 감속 이징 — 화면에 등장할 때 / transition-timing-function: cubic-bezier(0, 0, 0.2, 1);

/ 가속 이징 — 화면에서 사라질 때 / transition-timing-function: cubic-bezier(0.4, 0, 1, 1);

/ 스프링 느낌 — 바운스 효과 / transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); ```

`linear`는 기계적이고 부자연스러운 느낌을 줍니다. 실세계의 물체는 등속으로 움직이지 않기 때문에, 자연스러운 모션을 원한다면 반드시 적절한 이징 함수를 선택해야 합니다.

---

CSS Animations — @keyframes의 활용

여러 단계를 거치는 복잡한 애니메이션에는 `@keyframes`가 적합합니다.

```css / 스켈레톤 로딩 애니메이션 / .skeleton { background: linear-gradient( 90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75% ); background-size: 200% 100%; animation: shimmer 1.5s infinite ease-in-out; }

@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/ 페이드 인 + 슬라이드 업 / .fade-in-up { animation: fadeInUp 0.5s cubic-bezier(0, 0, 0.2, 1) both; }

@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } ```

will-change — GPU 가속 힌트

```css .animated-element { will-change: transform, opacity; }

/ 주의: 남용하면 오히려 성능 저하! / / ❌ 모든 요소에 적용 /

  • { will-change: transform; }

/ ✅ 실제로 애니메이션될 요소에만, 필요한 시점에만 / .card:hover { will-change: transform; } ```

`will-change`는 브라우저에게 "이 요소가 곧 변할 것"이라고 알려주어, 별도의 합성 레이어를 미리 생성하게 합니다. 하지만 모든 요소에 적용하면 메모리를 과도하게 사용하므로, 정말 필요한 요소에만 선택적으로 사용해야 합니다.

---

Web Animations API — JavaScript로 정밀 제어

CSS로는 어려운 동적 애니메이션(사용자 입력에 반응, 조건부 애니메이션 등)에는 Web Animations API(WAAPI)가 적합합니다.

```typescript // 기본 사용법 const element = document.querySelector('.box'); const animation = element.animate( [ { transform: 'scale(1)', opacity: 1 }, { transform: 'scale(1.2)', opacity: 0.8, offset: 0.5 }, { transform: 'scale(1)', opacity: 1 }, ], { duration: 600, easing: 'cubic-bezier(0.4, 0, 0.2, 1)', iterations: 1, fill: 'forwards', } );

// 제어 animation.pause(); animation.play(); animation.reverse(); animation.cancel();

// 완료 대기 await animation.finished; console.log('애니메이션 완료'); ```

Scroll-driven Animations

CSS Scroll-driven Animations는 스크롤 위치에 따라 애니메이션을 진행시킵니다. JavaScript 스크롤 이벤트 리스너 없이도 스크롤 연동 애니메이션을 구현할 수 있어 성능이 훨씬 뛰어나죠.

```css / 스크롤에 따라 프로그레스 바 채우기 / .progress-bar { position: fixed; top: 0; left: 0; height: 3px; background: #2563eb; transform-origin: left; animation: grow-progress auto linear; animation-timeline: scroll(); }

@keyframes grow-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/ 요소가 뷰포트에 들어올 때 애니메이션 / .reveal { animation: fade-in auto linear; animation-timeline: view(); animation-range: entry 0% entry 100%; }

@keyframes fade-in { from { opacity: 0; transform: translateY(40px); } to { opacity: 1; transform: translateY(0); } } ```

---

React에서의 애니메이션 — Framer Motion

React 생태계에서 가장 인기 있는 애니메이션 라이브러리는 Framer Motion입니다. 선언적 API가 React의 철학과 잘 맞기 때문이죠.

```typescript import { motion, AnimatePresence } from 'framer-motion';

function CardList({ items }: { items: Item[] }) { return ( <AnimatePresence mode="popLayout"> {items.map((item) => ( <motion.div key={item.id} layout // 레이아웃 변화 시 자동 애니메이션 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, scale: 0.9 }} transition={{ type: 'spring', stiffness: 300, damping: 25, }} className="card" > {item.title} </motion.div> ))} </AnimatePresence> ); } ```

Framer Motion의 `layout` prop은 DOM 요소의 위치나 크기가 변경될 때 자동으로 FLIP(First, Last, Invert, Play) 애니메이션을 적용합니다. 리스트 재정렬, 필터링 등에서 별도 코드 없이 부드러운 전환이 만들어지죠.

웹 애니메이션 기술 선택 가이드 — CSS vs WAAPI vs 라이브러리
웹 애니메이션 기술 선택 가이드 — CSS vs WAAPI vs 라이브러리

---

성능 디버깅 도구

Chrome DevTools의 Performance 패널에서 "Enable paint flashing"을 활성화하면, 리페인트가 발생하는 영역이 녹색으로 강조됩니다. "Rendering" 탭의 "Layer borders"를 켜면 합성 레이어 경계를 확인할 수 있고요.

애니메이션이 60fps를 유지하지 못하는 경우, 대부분 다음 중 하나가 원인입니다. Layout을 유발하는 속성(`width`, `height`, `top` 등)을 애니메이션하고 있거나, JavaScript에서 레이아웃 스래싱(Layout Thrashing)이 발생하거나, 합성 레이어가 너무 많아 메모리를 과도하게 사용하는 경우입니다.

좋은 웹 애니메이션은 "화려한 것"이 아니라 "자연스러운 것"입니다. `transform`과 `opacity`를 중심으로, 적절한 이징과 지속 시간(200~500ms)을 사용하면, 사용자가 애니메이션을 의식하지 않으면서도 인터페이스가 "살아있다"고 느끼게 됩니다. 성능을 해치는 애니메이션은 차라리 없는 것보다 못합니다.
A

AlwaysCorp 기술팀

프론트엔드 개발 콘텐츠 전문

얼웨이즈 블로그에서 유용한 정보와 인사이트를 공유합니다.