6.9 KiB
6.9 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| frontend-animation-motion | Animation — Motion / GSAP / View Transitions | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
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 후속)
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 (자동)
<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
<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
<motion.div
animate={{ x: 100 }}
transition={{ type: 'spring', stiffness: 200, damping: 20 }}
/>
GSAP (강력 / 복잡)
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)
@view-transition {
navigation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.3s;
}
::view-transition-group(card) {
animation: cardMorph 0.5s;
}
<img style="view-transition-name: hero" src="...">
→ 페이지 / view 변경이 부드럽게.
// 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 (가벼운)
.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; }
}
<div className="fade-in">...</div>
→ Build 작은 + browser 빠름.
Web Animations API (modern, vanilla)
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
import { useReducedMotion } from 'motion/react';
function Component() {
const reduced = useReducedMotion();
return (
<motion.div
animate={{ x: reduced ? 0 : 100 }}
/>
);
}
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
→ 사용자가 setting 으로 끄면 응답.
Performance — transform / opacity 만
/* ✅ 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)
// 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 친화)
npm install lottie-react
import Lottie from 'lottie-react';
import animationData from './animation.json';
<Lottie animationData={animationData} loop play />
→ After Effects 에서 export. 풍부 애니메이션.
큰 list animation
// AnimatePresence + 큰 list = 느림
// 작게 또는 virtualize + minimal animation
Staggered (timeline-like)
<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 항상.