Framer Motion은 React를 위한 가장 강력한 애니메이션 라이브러리입니다. 선언적 API로 복잡한 애니메이션도 쉽게 구현할 수 있습니다.
Framer Motion vs 다른 라이브러리
| 항목 | Framer Motion | React Spring | CSS/GSAP |
|---|---|---|---|
| 러닝 커브 | 낮음 | 중간 | 다양 |
| 번들 크기 | ~50KB | ~25KB | CSS: 0 / GSAP: ~60KB |
| 레이아웃 애니메이션 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐ |
| 제스처 지원 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
| Exit 애니메이션 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| 물리 기반 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
설치 및 기본 사용
설치
npm install framer-motion
기본 애니메이션
import { motion } from 'framer-motion'
export function BasicAnimation() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
안녕하세요!
</motion.div>
)
}
motion 컴포넌트
// 모든 HTML 요소에 motion 프리픽스
<motion.div />
<motion.span />
<motion.button />
<motion.ul />
<motion.svg />
<motion.path />
// 커스텀 컴포넌트
const MotionButton = motion(Button)
const MotionCard = motion.create(Card)
애니메이션 속성
initial, animate, exit
<motion.div
initial={{ opacity: 0, scale: 0.8 }} // 시작 상태
animate={{ opacity: 1, scale: 1 }} // 최종 상태
exit={{ opacity: 0, scale: 0.8 }} // 언마운트 시
transition={{ duration: 0.3 }}
/>
variants (상태 정의)
const cardVariants = {
hidden: {
opacity: 0,
y: 50,
},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
ease: 'easeOut',
},
},
hover: {
scale: 1.05,
boxShadow: '0 10px 30px rgba(0,0,0,0.2)',
},
}
export function Card() {
return (
<motion.div
variants={cardVariants}
initial="hidden"
animate="visible"
whileHover="hover"
>
카드 내용
</motion.div>
)
}
transition 옵션
<motion.div
animate={{ x: 100 }}
transition={{
// 기본
duration: 0.5,
delay: 0.2,
// 이징
ease: 'easeInOut',
// 또는 커스텀: [0.6, 0.01, -0.05, 0.95]
// 스프링 (물리 기반)
type: 'spring',
stiffness: 300,
damping: 20,
mass: 1,
// 또는 간단하게
type: 'spring',
bounce: 0.25,
// 반복
repeat: Infinity,
repeatType: 'reverse',
repeatDelay: 1,
}}
/>
제스처 애니메이션
hover, tap, drag
export function InteractiveButton() {
return (
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
클릭하세요
</motion.button>
)
}
드래그
export function DraggableCard() {
return (
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
dragElastic={0.2}
dragMomentum={false}
whileDrag={{ scale: 1.1, cursor: 'grabbing' }}
>
드래그하세요
</motion.div>
)
}
// 드래그 영역 제한 (부모 기준)
export function ConstrainedDrag() {
const constraintsRef = useRef(null)
return (
<motion.div ref={constraintsRef} className="w-80 h-80 bg-gray-100">
<motion.div
drag
dragConstraints={constraintsRef}
className="w-20 h-20 bg-blue-500"
/>
</motion.div>
)
}
AnimatePresence (Exit 애니메이션)
import { motion, AnimatePresence } from 'framer-motion'
export function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<>
{/* 오버레이 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-black/50"
/>
{/* 모달 */}
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
className="fixed inset-0 flex items-center justify-center"
>
<div className="bg-white rounded-lg p-6">
{children}
</div>
</motion.div>
</>
)}
</AnimatePresence>
)
}
리스트 애니메이션
export function AnimatedList({ items }) {
return (
<AnimatePresence mode="popLayout">
{items.map((item) => (
<motion.li
key={item.id}
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 50 }}
layout
>
{item.text}
</motion.li>
))}
</AnimatePresence>
)
}
레이아웃 애니메이션
기본 레이아웃 애니메이션
export function ExpandableCard() {
const [isExpanded, setIsExpanded] = useState(false)
return (
<motion.div
layout
onClick={() => setIsExpanded(!isExpanded)}
className={isExpanded ? 'w-full h-80' : 'w-40 h-40'}
style={{ borderRadius: 12 }}
>
<motion.h2 layout="position">제목</motion.h2>
{isExpanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
상세 내용이 여기에 표시됩니다.
</motion.p>
)}
</motion.div>
)
}
공유 레이아웃 (layoutId)
export function SharedLayoutExample() {
const [selectedId, setSelectedId] = useState(null)
return (
<>
{/* 그리드 */}
<div className="grid grid-cols-3 gap-4">
{items.map((item) => (
<motion.div
key={item.id}
layoutId={item.id}
onClick={() => setSelectedId(item.id)}
>
<motion.h3 layoutId={`title-${item.id}`}>
{item.title}
</motion.h3>
</motion.div>
))}
</div>
{/* 확장된 뷰 */}
<AnimatePresence>
{selectedId && (
<motion.div
layoutId={selectedId}
className="fixed inset-0 flex items-center justify-center"
>
<motion.h3 layoutId={`title-${selectedId}`}>
{items.find(i => i.id === selectedId)?.title}
</motion.h3>
<motion.button onClick={() => setSelectedId(null)}>
닫기
</motion.button>
</motion.div>
)}
</AnimatePresence>
</>
)
}
스크롤 애니메이션
useScroll
import { motion, useScroll, useTransform } from 'framer-motion'
export function ScrollProgress() {
const { scrollYProgress } = useScroll()
return (
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-blue-500 origin-left"
style={{ scaleX: scrollYProgress }}
/>
)
}
스크롤 기반 애니메이션
export function ParallaxSection() {
const ref = useRef(null)
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'end start'],
})
const y = useTransform(scrollYProgress, [0, 1], [100, -100])
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0])
return (
<section ref={ref} className="h-screen">
<motion.div style={{ y, opacity }}>
패럴랙스 효과
</motion.div>
</section>
)
}
useInView
import { motion, useInView } from 'framer-motion'
export function FadeInWhenVisible({ children }) {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: '-100px' })
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
>
{children}
</motion.div>
)
}
실전 컴포넌트 예제
탭 인디케이터
export function Tabs({ tabs, activeTab, setActiveTab }) {
return (
<div className="flex relative">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className="px-4 py-2 relative"
>
{tab.label}
{activeTab === tab.id && (
<motion.div
layoutId="activeTab"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500"
/>
)}
</button>
))}
</div>
)
}
알림 토스트
const toastVariants = {
initial: { opacity: 0, y: 50, scale: 0.3 },
animate: { opacity: 1, y: 0, scale: 1 },
exit: { opacity: 0, scale: 0.5, transition: { duration: 0.2 } },
}
export function Toast({ message, onClose }) {
return (
<motion.div
variants={toastVariants}
initial="initial"
animate="animate"
exit="exit"
drag="x"
dragConstraints={{ left: 0, right: 0 }}
onDragEnd={(_, info) => {
if (Math.abs(info.offset.x) > 100) {
onClose()
}
}}
className="bg-gray-800 text-white px-4 py-3 rounded-lg shadow-lg"
>
{message}
</motion.div>
)
}
스텝 인디케이터
export function StepIndicator({ currentStep, totalSteps }) {
return (
<div className="flex gap-2">
{Array.from({ length: totalSteps }).map((_, index) => (
<motion.div
key={index}
className="h-2 rounded-full bg-gray-200"
animate={{
width: index === currentStep ? 24 : 8,
backgroundColor: index <= currentStep ? '#3B82F6' : '#E5E7EB',
}}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
/>
))}
</div>
)
}
성능 최적화
1. transform만 애니메이션
// ❌ 레이아웃 트리거
<motion.div animate={{ width: 200, height: 200 }} />
// ✅ transform만 (GPU 가속)
<motion.div animate={{ scale: 1.5 }} />
2. layout 최소화
// ❌ 모든 자식에 layout
<motion.div layout>
<motion.div layout>
<motion.div layout />
</motion.div>
</motion.div>
// ✅ 필요한 곳만
<motion.div layout>
<div>
<div />
</div>
</motion.div>
3. LazyMotion (번들 최적화)
import { LazyMotion, domAnimation, m } from 'framer-motion'
// 필요한 기능만 로드
export function App() {
return (
<LazyMotion features={domAnimation}>
<m.div animate={{ opacity: 1 }} />
</LazyMotion>
)
}
4. useReducedMotion
import { useReducedMotion } from 'framer-motion'
export function AccessibleAnimation() {
const shouldReduceMotion = useReducedMotion()
return (
<motion.div
animate={{ x: 100 }}
transition={{
duration: shouldReduceMotion ? 0 : 0.5,
}}
/>
)
}
자주 쓰는 패턴
staggerChildren (자식 순차 애니메이션)
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
}
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}
export function StaggeredList({ items }) {
return (
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
{items.map((item) => (
<motion.li key={item.id} variants={itemVariants}>
{item.text}
</motion.li>
))}
</motion.ul>
)
}
Page Transitions (Next.js)
// components/PageTransition.tsx
export function PageTransition({ children }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
)
}
마치며
Framer Motion 핵심 포인트:
- 선언적: 상태만 정의하면 알아서 보간
- 레이아웃: 복잡한 레이아웃 변화도 자동 처리
- 제스처: hover, tap, drag 쉽게 구현
- AnimatePresence: exit 애니메이션 지원
프로덕션 앱에 폴리시된 애니메이션을 추가하고 싶다면 Framer Motion이 최고의 선택입니다.