320 lines
6.9 KiB
Markdown
320 lines
6.9 KiB
Markdown
---
|
|
id: frontend-animation-motion
|
|
title: Animation — Motion / GSAP / View Transitions
|
|
category: Coding
|
|
status: draft
|
|
source_trust_level: B
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
tags: [frontend, animation, motion, gsap, vibe-coding]
|
|
tech_stack: { language: "TS / React", applicable_to: ["Frontend"] }
|
|
applied_in: []
|
|
aliases: [Framer Motion, Motion, GSAP, View Transitions API, CSS animation, Web Animations API]
|
|
---
|
|
|
|
# Frontend Animation
|
|
|
|
> Modern stack: **Motion (Framer Motion 후속) / GSAP / View Transitions API**. CSS / Web Animations API / RequestAnimationFrame. 60fps + a11y `prefers-reduced-motion`.
|
|
|
|
## 📖 핵심 개념
|
|
- Declarative (Motion / Framer): React 친화.
|
|
- Imperative (GSAP / WAAPI): 강력 / 정밀.
|
|
- View Transitions: 페이지 전환 자동 (browser native).
|
|
- Hardware-accelerated: transform / opacity 만.
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### Motion (Framer Motion 후속)
|
|
```tsx
|
|
import { motion } from 'motion/react';
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.3, ease: 'easeOut' }}
|
|
>
|
|
Content
|
|
</motion.div>
|
|
|
|
// Variants
|
|
const variants = {
|
|
hidden: { opacity: 0 },
|
|
visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
|
|
};
|
|
|
|
<motion.ul variants={variants} initial="hidden" animate="visible">
|
|
{items.map(i => (
|
|
<motion.li key={i.id} variants={{ hidden: { opacity: 0, x: -20 }, visible: { opacity: 1, x: 0 } }}>
|
|
{i.text}
|
|
</motion.li>
|
|
))}
|
|
</motion.ul>
|
|
```
|
|
|
|
### Layout animation (자동)
|
|
```tsx
|
|
<motion.div layout>
|
|
{/* size / position 변경 자동 animate */}
|
|
</motion.div>
|
|
|
|
<AnimatePresence mode="popLayout">
|
|
{items.map(i => (
|
|
<motion.div key={i.id} layout exit={{ opacity: 0 }}>
|
|
{i.text}
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
```
|
|
|
|
### Gesture
|
|
```tsx
|
|
<motion.div
|
|
drag dragConstraints={{ left: 0, right: 200 }}
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
whileDrag={{ scale: 1.1 }}
|
|
>
|
|
Drag me
|
|
</motion.div>
|
|
```
|
|
|
|
### Spring physics
|
|
```tsx
|
|
<motion.div
|
|
animate={{ x: 100 }}
|
|
transition={{ type: 'spring', stiffness: 200, damping: 20 }}
|
|
/>
|
|
```
|
|
|
|
### GSAP (강력 / 복잡)
|
|
```ts
|
|
import gsap from 'gsap';
|
|
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
|
|
gsap.registerPlugin(ScrollTrigger);
|
|
|
|
// Timeline
|
|
const tl = gsap.timeline();
|
|
tl.to('.box', { x: 100, duration: 1 })
|
|
.to('.box', { y: 100, duration: 0.5 })
|
|
.to('.box', { opacity: 0, duration: 0.3 });
|
|
|
|
// Scroll trigger
|
|
gsap.to('.parallax', {
|
|
y: -200,
|
|
scrollTrigger: {
|
|
trigger: '.section',
|
|
start: 'top top',
|
|
end: 'bottom top',
|
|
scrub: 1,
|
|
},
|
|
});
|
|
|
|
// React useGSAP
|
|
import { useGSAP } from '@gsap/react';
|
|
|
|
function Component() {
|
|
const ref = useRef(null);
|
|
useGSAP(() => {
|
|
gsap.from('.item', { y: 50, opacity: 0, stagger: 0.1 });
|
|
}, { scope: ref });
|
|
|
|
return <div ref={ref}>...</div>;
|
|
}
|
|
```
|
|
|
|
### View Transitions API (browser native)
|
|
```css
|
|
@view-transition {
|
|
navigation: auto;
|
|
}
|
|
|
|
::view-transition-old(root),
|
|
::view-transition-new(root) {
|
|
animation-duration: 0.3s;
|
|
}
|
|
|
|
::view-transition-group(card) {
|
|
animation: cardMorph 0.5s;
|
|
}
|
|
```
|
|
|
|
```html
|
|
<img style="view-transition-name: hero" src="...">
|
|
```
|
|
|
|
→ 페이지 / view 변경이 부드럽게.
|
|
|
|
```ts
|
|
// SPA 사용
|
|
async function navigate(url: string) {
|
|
if (!document.startViewTransition) return goto(url);
|
|
|
|
document.startViewTransition(async () => {
|
|
await goto(url);
|
|
});
|
|
}
|
|
```
|
|
|
|
→ Chrome / Edge / Safari 17.4+. Polyfill X.
|
|
|
|
### CSS animations (가벼운)
|
|
```css
|
|
.fade-in {
|
|
animation: fadeIn 0.3s ease-out forwards;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
/* Reduced motion */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.fade-in { animation: none; }
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
<div className="fade-in">...</div>
|
|
```
|
|
|
|
→ Build 작은 + browser 빠름.
|
|
|
|
### Web Animations API (modern, vanilla)
|
|
```ts
|
|
const el = document.querySelector('.box')!;
|
|
const anim = el.animate(
|
|
[
|
|
{ opacity: 0, transform: 'translateY(20px)' },
|
|
{ opacity: 1, transform: 'translateY(0)' },
|
|
],
|
|
{ duration: 300, easing: 'ease-out', fill: 'forwards' }
|
|
);
|
|
|
|
await anim.finished;
|
|
```
|
|
|
|
### a11y — prefers-reduced-motion
|
|
```tsx
|
|
import { useReducedMotion } from 'motion/react';
|
|
|
|
function Component() {
|
|
const reduced = useReducedMotion();
|
|
return (
|
|
<motion.div
|
|
animate={{ x: reduced ? 0 : 100 }}
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
```ts
|
|
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
```
|
|
|
|
→ 사용자가 setting 으로 끄면 응답.
|
|
|
|
### Performance — transform / opacity 만
|
|
```css
|
|
/* ✅ 60fps (GPU) */
|
|
.move { transform: translateX(100px); }
|
|
.fade { opacity: 0.5; }
|
|
|
|
/* ❌ Reflow (CPU) */
|
|
.bad { left: 100px; }
|
|
.bad { width: 200px; }
|
|
.bad { margin-top: 50px; }
|
|
```
|
|
|
|
→ `will-change: transform` (cautious — 메모리).
|
|
|
|
### FLIP technique (자체 구현 layout animation)
|
|
```ts
|
|
// First — 시작 위치
|
|
const start = el.getBoundingClientRect();
|
|
// Last — 변경 후 위치
|
|
performLayoutChange();
|
|
const end = el.getBoundingClientRect();
|
|
// Invert — 차이 적용
|
|
const dx = start.left - end.left;
|
|
el.style.transform = `translateX(${dx}px)`;
|
|
// Play — 0 으로
|
|
requestAnimationFrame(() => {
|
|
el.style.transition = 'transform 0.3s';
|
|
el.style.transform = '';
|
|
});
|
|
```
|
|
|
|
→ Motion 의 `layout` 이 FLIP 자동.
|
|
|
|
### Lottie (designer 친화)
|
|
```bash
|
|
npm install lottie-react
|
|
```
|
|
|
|
```tsx
|
|
import Lottie from 'lottie-react';
|
|
import animationData from './animation.json';
|
|
|
|
<Lottie animationData={animationData} loop play />
|
|
```
|
|
|
|
→ After Effects 에서 export. 풍부 애니메이션.
|
|
|
|
### 큰 list animation
|
|
```tsx
|
|
// AnimatePresence + 큰 list = 느림
|
|
// 작게 또는 virtualize + minimal animation
|
|
```
|
|
|
|
### Staggered (timeline-like)
|
|
```tsx
|
|
<motion.div
|
|
variants={{
|
|
visible: { transition: { staggerChildren: 0.05, delayChildren: 0.2 } },
|
|
}}
|
|
initial="hidden"
|
|
animate="visible"
|
|
>
|
|
{items.map(i => (
|
|
<motion.div key={i.id} variants={itemVariants}>
|
|
{i.text}
|
|
</motion.div>
|
|
))}
|
|
</motion.div>
|
|
```
|
|
|
|
## 🤔 의사결정 기준
|
|
| 상황 | 추천 |
|
|
|---|---|
|
|
| React 일반 | Motion (Framer 후속) |
|
|
| 매우 복잡 / 강력 | GSAP |
|
|
| 간단 (전환 / fade) | CSS animation |
|
|
| Vanilla / 가벼움 | WAAPI |
|
|
| 페이지 전환 | View Transitions API |
|
|
| Designer animation | Lottie |
|
|
| Skia / canvas | Reanimated (RN) / Pixi (web) |
|
|
|
|
## ❌ 안티패턴
|
|
- **width / left animation**: reflow — jank.
|
|
- **prefers-reduced-motion 무시**: a11y / 멀미.
|
|
- **너무 길음 (1초+ 일반)**: 사용자 지루.
|
|
- **모든 component animate**: noise.
|
|
- **Lottie 거대 JSON (500KB+)**: bundle 큼.
|
|
- **GSAP transition 매번 cleanup 안 함**: leak.
|
|
- **CSS animation + JS animation 같은 element**: 충돌.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
- Motion = React 표준 (variants, layout, AnimatePresence).
|
|
- GSAP = 강력 timeline / scroll.
|
|
- View Transitions = page transition.
|
|
- prefers-reduced-motion 항상.
|
|
|
|
## 🔗 관련 문서
|
|
- [[React_Animation_Performance]]
|
|
- [[Frontend_A11y_Testing]]
|
|
- [[Web_Performance_Core_Vitals]]
|