React 애니메이션 기초 — CSS Transition부터 Framer Motion까지
모달이 나타날 때는 부드럽게 등장하는데, 닫을 때는 뚝 끊기면서 사라진다면 — 사용자는 어떤 느낌을 받을까요?
애니메이션은 단순한 장식이 아니라 UX의 핵심입니다. 요소의 등장과 퇴장, 상태 전환을 자연스럽게 연결하면 사용자가 앱의 흐름을 직관적으로 이해할 수 있습니다. React에서 애니메이션을 구현하는 세 가지 방법을 단계별로 정리합니다.
CSS Transition — 가장 단순한 방법
JavaScript 라이브러리 없이 CSS만으로 처리하는 방법입니다.
function Toggle() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(!isOpen)}>토글</button>
<div
className="panel"
style={{
maxHeight: isOpen ? '200px' : '0',
opacity: isOpen ? 1 : 0,
overflow: 'hidden',
transition: 'max-height 0.3s ease, opacity 0.3s ease',
}}
>
<p>패널 내용입니다</p>
</div>
</>
);
}
CSS만으로 충분한 경우
- 호버, 포커스 시 색상/크기 변경
- 토글 시 opacity, transform 변화
- 단순한 상태 전환 (펼치기/접기)
CSS의 한계
- **exit 애니메이션 **: 요소가 DOM에서 제거되면 transition이 동작하지 않습니다
- ** 시퀀스 **: 여러 요소를 순차적으로 애니메이션하기 어렵습니다
- ** 물리 기반 **: 스프링, 관성 등 자연스러운 물리 효과가 제한적입니다
react-transition-group — DOM 진입/퇴장
DOM에 요소가 추가/제거될 때의 전환을 관리합니다.
npm install react-transition-group
CSSTransition
import { CSSTransition } from 'react-transition-group';
function Alert({ show, message, onClose }) {
const nodeRef = useRef(null);
return (
<CSSTransition
in={show}
timeout={300}
classNames="alert"
unmountOnExit
nodeRef={nodeRef}
>
<div ref={nodeRef} className="alert">
{message}
<button onClick={onClose}>닫기</button>
</div>
</CSSTransition>
);
}
/* 진입 */
.alert-enter {
opacity: 0;
transform: translateY(-20px);
}
.alert-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s, transform 0.3s;
}
/* 퇴장 */
.alert-exit {
opacity: 1;
transform: translateY(0);
}
.alert-exit-active {
opacity: 0;
transform: translateY(-20px);
transition: opacity 0.3s, transform 0.3s;
}
TransitionGroup — 리스트 애니메이션
import { TransitionGroup, CSSTransition } from 'react-transition-group';
function TodoList({ todos, onRemove }) {
return (
<TransitionGroup component="ul">
{todos.map((todo) => (
<CSSTransition
key={todo.id}
timeout={300}
classNames="todo"
>
<li>
{todo.text}
<button onClick={() => onRemove(todo.id)}>삭제</button>
</li>
</CSSTransition>
))}
</TransitionGroup>
);
}
Framer Motion — 선언적 애니메이션
Framer Motion은 React에 특화된 애니메이션 라이브러리로, 선언적 API와 물리 기반 애니메이션을 제공합니다.
npm install framer-motion
기본 사용
import { motion } from 'framer-motion';
function Card() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }} // 초기 상태
animate={{ opacity: 1, y: 0 }} // 최종 상태
transition={{ duration: 0.5 }} // 전환 설정
>
<h2>카드 제목</h2>
<p>카드 내용</p>
</motion.div>
);
}
인터랙션 애니메이션
function InteractiveButton() {
return (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: 'spring', stiffness: 300 }}
>
클릭하세요
</motion.button>
);
}
AnimatePresence — exit 애니메이션
Framer Motion의 가장 강력한 기능입니다. 컴포넌트가 DOM에서 제거될 때 애니메이션을 실행합니다.
import { motion, AnimatePresence } from 'framer-motion';
function Notification({ notifications, onDismiss }) {
return (
<AnimatePresence>
{notifications.map((notif) => (
<motion.div
key={notif.id}
initial={{ opacity: 0, x: 100 }} // 등장: 오른쪽에서
animate={{ opacity: 1, x: 0 }} // 제자리
exit={{ opacity: 0, x: -100 }} // 퇴장: 왼쪽으로
transition={{ duration: 0.3 }}
className="notification"
>
{notif.message}
<button onClick={() => onDismiss(notif.id)}>닫기</button>
</motion.div>
))}
</AnimatePresence>
);
}
AnimatePresence가 자식의 key를 추적하여, 자식이 제거될 때 exit 애니메이션이 완료된 후에만 DOM에서 제거합니다.
Variants — 오케스트레이션
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // 자식 요소를 0.1초 간격으로 순차 실행
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
function StaggeredList({ items }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map((item) => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>
);
}
부모에 staggerChildren: 0.1을 설정하면, 자식 요소들이 0.1초 간격으로 순차적으로 등장합니다. 리스트 아이템이 하나씩 나타나는 효과를 아주 간단하게 구현할 수 있습니다.
레이아웃 애니메이션
function ExpandableCard({ isExpanded, onClick }) {
return (
<motion.div
layout // 레이아웃 변경 시 자동 애니메이션
onClick={onClick}
style={{
width: isExpanded ? '300px' : '150px',
height: isExpanded ? '400px' : '200px',
}}
transition={{ type: 'spring', stiffness: 200, damping: 20 }}
>
<motion.h2 layout="position">제목</motion.h2>
{isExpanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
상세 내용
</motion.p>
)}
</motion.div>
);
}
layout prop을 추가하면 CSS 속성 변경으로 인한 레이아웃 변화를 자동으로 애니메이션합니다.
어떤 도구를 선택할까
| 요구사항 | 추천 도구 |
|---|---|
| 호버/포커스 효과 | CSS transition |
| 토글 애니메이션 | CSS transition |
| 진입/퇴장 전환 | react-transition-group 또는 Framer Motion |
| 리스트 아이템 등장 | Framer Motion (staggerChildren) |
| exit 애니메이션 | Framer Motion (AnimatePresence) |
| 물리 기반 애니메이션 | Framer Motion (spring) |
| 레이아웃 애니메이션 | Framer Motion (layout) |
| 복잡한 시퀀스 | Framer Motion (variants) |
판단 기준
- CSS로 충분한가? — 가능하면 CSS를 우선 사용합니다 (성능, 번들 크기)
- exit 애니메이션이 필요한가? — 필요하면 react-transition-group 또는 Framer Motion
- ** 복잡한 오케스트레이션이 필요한가?** — Framer Motion
성능 고려사항
// transform과 opacity만 애니메이션하면 GPU 가속을 받는다 (레이아웃 재계산 없음)
<motion.div animate={{ x: 100, opacity: 0.5 }} />
// width, height는 레이아웃 재계산을 유발한다 (느림)
<motion.div animate={{ width: '200px', height: '300px' }} />
transform(x, y, scale, rotate)과opacity는 GPU에서 처리되어 빠릅니다width,height,margin등은 레이아웃 재계산이 필요하여 느립니다will-change: transform을 미리 힌트로 제공하면 GPU가 준비합니다
정리
React 애니메이션은 도구 선택이 핵심입니다.
- CSS transition: 가장 단순하고 성능이 좋지만 exit 애니메이션이 불가능합니다
- react-transition-group: DOM 진입/퇴장 전환에 CSS 클래스를 적용합니다
- Framer Motion: 선언적 API, exit 애니메이션, variants, 물리 기반 spring까지 제공하는 종합 도구입니다
- AnimatePresence 가 Framer Motion의 가장 강력한 기능입니다 — 컴포넌트 퇴장 시 애니메이션을 가능하게 합니다
- 성능을 위해 transform과 opacity 위주로 애니메이션합니다
주의할 점
CSS transition으로 exit 애니메이션을 구현할 수 없음
React에서 컴포넌트가 언마운트되면 DOM에서 즉시 제거됩니다. CSS transition만으로는 퇴장 애니메이션이 불가능하며, AnimatePresence(Framer Motion)나 CSSTransition(react-transition-group)이 필요합니다.
transform과 opacity 이외의 속성 애니메이션은 성능에 불리
width, height, margin 등을 애니메이션하면 레이아웃 재계산(reflow)이 발생하여 프레임 드롭이 생깁니다. GPU 가속이 가능한 transform과 opacity를 우선 사용해야 합니다.
간단한 효과에는 CSS를, 복잡한 인터랙션에는 Framer Motion을 사용하는 것이 실용적입니다.