Framer Motion으로 React 애니메이션 마스터하기
서론
React에서 애니메이션을 만들려고 하니까 CSS 애니메이션도 있고, 라이브러리도 여러 개더라 그 중에 Framer Motion이라는 게 있다고 들었음. 처음엔 "그냥 CSS로 하면 되지 않나?" 싶었는데, 찾아보니 훨씬 강력하더라 Framer Motion을 쓰면 복잡한 애니메이션을 간단하게 만들 수 있다고 함. 그래서 Framer Motion이 뭐고, 어떻게 쓰는지 정리해봄.
본론
Framer Motion이 뭐야?
Framer Motion은 React를 위한 애니메이션 라이브러리 선언적 문법으로 복잡한 애니메이션을 간단하게 만들 수 있음
Framer Motion의 철학:
- 애니메이션을 "선언적으로" 작성 (CSS 프레임처럼)
- JSX 안에서 완전히 제어 가능
- 상태 변화에 따라 자동으로 애니메이션
- 성능 최적화 (GPU 가속)
기본 문법부터 시작
설치:
npm install framer-motion
가장 간단한 애니메이션
import { motion } from 'framer-motion';
export default function SimpleAnimation() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
안녕하세요!
</motion.div>
);
}
뭐가 뭐냐면:
- motion.div: 애니메이션 가능한 div
- initial: 초기 상태 (처음 모양)
- animate: 최종 상태 (어떻게 변할지)
- transition: 애니메이션 속도 (0.5초)
흐름:
1. 컴포넌트 마운트
↓ initial 상태로 시작
opacity: 0 (투명), y: 20 (아래로 20px)
↓ 0.5초에 걸쳐 animate로 변함
↓ 최종 상태
opacity: 1 (보임), y: 0 (원래 위치)
상태 변화에 따른 애니메이션
위 예시는 마운트할 때만 애니메이션됨 그런데 상태가 바뀔 때마다 애니메이션하려면?
import { motion } from 'framer-motion';
import { useState } from 'react';
export default function StateAnimation() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? '닫기' : '열기'}
</button>
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: isOpen ? 300 : 0, opacity: isOpen ? 1 : 0 }}
transition={{ duration: 0.5 }}
style={{ overflow: 'hidden', background: '#f0f0f0' }}
>
<div style={{ padding: '20px' }}>
<p>이것은 토글 가능한 콘텐츠입니다!</p>
</div>
</motion.div>
</div>
);
}
동작:
- "열기" 버튼 클릭
- isOpen 상태 변경
- Framer Motion이 자동으로 감지
- animate 값을 새로 계산
- 0.5초에 걸쳐 애니메이션 재생
Variants로 복잡한 애니메이션 관리
복잡한 애니메이션이 많으면 코드가 복잡해짐 그럴 땐 variants로 정리하는 게 좋음
import { motion } from 'framer-motion';
// 애니메이션 정의를 따로 만듦
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 default function ListAnimation() {
const items = ['항목 1', '항목 2', '항목 3', '항목 4'];
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map((item) => (
<motion.li
key={item}
variants={itemVariants}
style={{ listStyle: 'none', padding: '10px' }}
>
{item}
</motion.li>
))}
</motion.ul>
);
}
뭐가 뭐냐면:
- containerVariants: 부모 애니메이션 정의
- itemVariants: 자식 애니메이션 정의
- staggerChildren: 0.1초씩 차이나면서 순차 애니메이션
결과:
항목 1 → (0.1초 대기) → 항목 2 → (0.1초 대기) → 항목 3 → ...
제스처 애니메이션 (Hover, Tap)
마우스 호버나 클릭에 반응하는 애니메이션
import { motion } from 'framer-motion';
export default function GestureAnimation() {
return (
<motion.button
whileHover={{ scale: 1.1, backgroundColor: '#ff6b6b' }}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.2 }}
style={{
padding: '10px 20px',
fontSize: '16px',
border: 'none',
borderRadius: '4px',
backgroundColor: '#4ecdc4',
color: 'white',
cursor: 'pointer'
}}
>
클릭해보세요!
</motion.button>
);
}
동작:
- whileHover: 마우스 올렸을 때 (크기 1.1배, 색상 변경)
- whileTap: 클릭했을 때 (크기 0.95배로 축소 - 누르는 느낌)
결과:
마우스 올림 → 버튼 커짐 + 색상 변경
클릭 → 살짝 축소 (누르는 느낌)
드래그 애니메이션
마우스로 드래그해서 움직이는 애니메이션
import { motion } from 'framer-motion';
export default function DragAnimation() {
return (
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
dragElastic={0.2}
whileDrag={{ scale: 1.1, cursor: 'grabbing' }}
style={{
width: '100px',
height: '100px',
backgroundColor: '#4ecdc4',
borderRadius: '8px',
cursor: 'grab'
}}
/>
);
}
뭐가 뭐냐면:
- drag: 드래그 가능하게 만듦
- dragConstraints: 드래그 범위 제한
- dragElastic: 탄성 효과 (0.2 = 20% 반동)
- whileDrag: 드래그 중 애니메이션
결과:
마우스로 잡아서 끌기 가능
범위 넘어가면 자동으로 돌아옴 (탄성)
스크롤 기반 애니메이션
스크롤할 때 요소가 애니메이션
import { motion, useViewportScroll, useTransform } from 'framer-motion';
export default function ScrollAnimation() {
const { scrollY } = useViewportScroll();
const opacity = useTransform(scrollY, [0, 300], [1, 0]);
const y = useTransform(scrollY, [0, 300], [0, -100]);
return (
<div style={{ height: '200vh' }}>
<motion.div
style={{ opacity, y }}
style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
fontSize: '24px'
}}
>
스크롤하면 사라집니다!
</motion.div>
<div style={{ padding: '100px', background: '#f5f5f5' }}>
<p>여기서부터 스크롤하면 위의 텍스트가 사라져요!</p>
</div>
</div>
);
}
동작:
- scrollY가 0일 때: opacity 1 (완전히 보임), y 0
- scrollY가 300일 때: opacity 0 (투명), y -100 (위로 100px)
- 그 사이는 자동으로 보간됨
CSS 애니메이션 vs Framer Motion
항목 CSS 애니메이션 Framer Motion
| 문법 | 복잡함 (@keyframes) | 간단함 (JSX) |
| 상태 연동 | 어려움 | 쉬움 |
| 제스처 | 없음 | 풍부함 (hover, tap, drag) |
| 성능 | 좋음 | 좋음 (GPU 가속) |
| 번들 크기 | 0kb | ~40kb |
| 복잡도 | 낮음 | 높을 수 있음 |
CSS 애니메이션:
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.element {
animation: slideIn 0.5s ease-out;
}
Framer Motion:
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: 'easeOut' }}
/>
Framer Motion이 훨씬 간단하고 상태와 연동하기 쉬움!
실제 활용 예시: 모달 애니메이션
import { motion, AnimatePresence } from 'framer-motion';
import { useState } from 'react';
const backdropVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 }
};
const modalVariants = {
hidden: { y: 50, opacity: 0 },
visible: { y: 0, opacity: 1 },
exit: { y: 50, opacity: 0 }
};
export default function ModalAnimation() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>
모달 열기
</button>
<AnimatePresence>
{isOpen && (
<>
{/* 배경 */}
<motion.div
variants={backdropVariants}
initial="hidden"
animate="visible"
exit="hidden"
onClick={() => setIsOpen(false)}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)'
}}
/>
{/* 모달 */}
<motion.div
variants={modalVariants}
initial="hidden"
animate="visible"
exit="exit"
style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'white',
padding: '40px',
borderRadius: '8px',
zIndex: 10
}}
>
<h2>모달 제목</h2>
<p>모달 내용입니다.</p>
<button onClick={() => setIsOpen(false)}>
닫기
</button>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}
핵심:
- AnimatePresence: 컴포넌트가 DOM에서 제거될 때 exit 애니메이션 실행
- 배경과 모달이 동시에 나타났다가 동시에 사라짐
성능 최적화 팁
1. 불필요한 리렌더링 방지
// ❌ 나쁜 예
const containerVariants = {
hidden: { opacity: 0 }
};
export default function Bad() {
const [count, setCount] = useState(0);
// 매번 새로운 variants 객체 생성
return <motion.div variants={containerVariants} />;
}
// ✅ 좋은 예
const containerVariants = {
hidden: { opacity: 0 }
};
export default function Good() {
const [count, setCount] = useState(0);
// variants는 컴포넌트 밖에서 정의
return <motion.div variants={containerVariants} />;
}
2. will-change 활용
<motion.div
animate={{ x: 100 }}
style={{ willChange: 'transform' }}
/>
3. reduce-motion 존중
import { useReducedMotion } from 'framer-motion';
export default function AccessibleAnimation() {
const prefersReducedMotion = useReducedMotion();
return (
<motion.div
animate={{ x: prefersReducedMotion ? 0 : 100 }}
transition={{ duration: prefersReducedMotion ? 0 : 0.5 }}
/>
);
}
결론
Framer Motion은 React에서 애니메이션을 만드는 가장 강력한 라이브러리
CSS 애니메이션보다 문법이 간단하고, 상태와 연동하기 쉬움 제스처 애니메이션, 드래그, 스크롤 기반 애니메이션 등 복잡한 애니메이션도 가능함
처음엔 initial, animate, transition만 해도 충분하지만 나중에 variants, whileHover, drag 등으로 점점 확장할 수 있음
가챠픽이나 로또 시뮬레이션 같은 프로젝트에서도 Framer Motion으로 부드러운 애니메이션을 만들 수 있음!
앞으로 React 프로젝트에서 애니메이션이 필요하면 Framer Motion을 적극 활용해봐야겠음! 🎬
'프론트엔드 > React' 카테고리의 다른 글
| (React) Virtual DOM과 Reconciliation: React의 렌더링 최적화 원리 (0) | 2025.12.08 |
|---|---|
| (React) Custom Hooks 만들기 (0) | 2025.12.02 |
| (React) useMemo vs useCallback 차이 (0) | 2025.11.20 |
| (React) props drilling 문제와 해결법 (0) | 2025.11.18 |
| (React) useRef는 언제쓰는걸까? (0) | 2025.11.17 |