사용자는 300ms만 느려도 체감 속도가 떨어집니다. 컴포넌트가 마운트/언마운트되거나 데이터가 로딩되는 순간에 적절한 애니메이션을 적용하면 지각 성능을 크게 개선할 수 있습니다. 이 글에서는 CSS 전환, React Transition Group, Framer Motion, Suspense 스켈레톤, 라우트 전환까지 실무에서 바로 쓰는 패턴을 정리합니다.
1. CSS만으로 가벼운 페이드 인 트랜지션
외부 라이브러리 없이 마운트 시 부드럽게 나타나는 패턴입니다. 마운트 직후 클래스 토글로 트랜지션을 시작합니다. 접근성을 위해 prefers-reduced-motion도 고려합니다.
import React, { useEffect, useState } from 'react';
const styles = `
.fade { opacity: 0; transform: translateY(4px); transition: opacity .24s ease, transform .24s ease; }
.fade.is-visible { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
.fade, .fade.is-visible { transition: none; transform: none; }
}
`;
function useInjectStyle(cssText) {
useEffect(() => {
const id = 'fade-style';
if (document.getElementById(id)) return;
const el = document.createElement('style');
el.id = id;
el.appendChild(document.createTextNode(cssText));
document.head.appendChild(el);
return () => { document.head.removeChild(el); };
}, [cssText]);
}
export function FadeIn({ children }) {
useInjectStyle(styles);
const [visible, setVisible] = useState(false);
useEffect(() => {
const r = requestAnimationFrame(() => setVisible(true));
return () => cancelAnimationFrame(r);
}, []);
return (
<div className={'fade' + (visible ? ' is-visible' : '')}>{children}</div>
);
}
// 사용 예시
// <FadeIn><Card /></FadeIn>
장점: 0 의존성, 매우 가벼움. 한계: 언마운트 애니메이션은 직접 제어가 필요합니다.
2. React Transition Group으로 마운트/언마운트 제어
상태 변화에 따른 입장/퇴장 클래스가 자동으로 바인딩되어 언마운트 시에도 자연스럽게 사라지게 만들 수 있습니다.
import React, { useState } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
const css = `
.fade-item-enter { opacity: 0; transform: translateY(6px); }
.fade-item-enter-active { opacity: 1; transform: translateY(0); transition: opacity .22s ease, transform .22s ease; }
.fade-item-exit { opacity: 1; }
.fade-item-exit-active { opacity: 0; transform: translateY(-6px); transition: opacity .18s ease, transform .18s ease; }
@media (prefers-reduced-motion: reduce) {
.fade-item-enter-active, .fade-item-exit-active { transition: none; }
}
`;
function Style() {
return <style dangerouslySetInnerHTML={{ __html: css }} />;
}
export function TodoList() {
const [items, setItems] = useState(['a', 'b', 'c']);
return (
<div>
<Style />
<button onClick={() => setItems(prev => ['x', ...prev])}>추가</button>
<TransitionGroup component='ul'>
{items.map(i => (
<CSSTransition key={i} classNames='fade-item' timeout={{ enter: 220, exit: 180 }}>
<li>{i}</li>
</CSSTransition>
))}
</TransitionGroup>
</div>
);
}
목록, 모달, 토스트 등 입장/퇴장이 명확한 컴포넌트에 적합합니다.
3. Framer Motion으로 생산성 높이기
프리셋 이징, 레이아웃 애니메이션, Gesture 등 강력한 기능을 제공합니다. 기본 페이드/슬라이드 인 패턴입니다.
import { motion, AnimatePresence, useReducedMotion } from 'framer-motion';
export function MotionCard({ open }) {
const reduce = useReducedMotion();
return (
<AnimatePresence>
{open ? (
<motion.div
key='card'
initial={{ opacity: 0, y: reduce ? 0 : 6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: reduce ? 0 : -6 }}
transition={{ duration: 0.24, ease: [0.22, 1, 0.36, 1] }}
style={{ willChange: 'transform, opacity' }}
>
내용
</motion.div>
) : null}
</AnimatePresence>
);
}
useReducedMotion으로 모션 민감 사용자에게 애니메이션을 줄이는 것이 중요합니다.
4. Suspense + 스켈레톤 로더로 심리적 대기 최소화
코드 스플리팅된 컴포넌트나 데이터 의존 컴포넌트에 스켈레톤을 보여주면 로딩 스트레스가 줄어듭니다.
import React, { Suspense } from 'react';
const HeavyCard = React.lazy(() => import('./HeavyCard'));
const css = `
.skeleton { position: relative; overflow: hidden; background: #f2f3f5; border-radius: 8px; height: 120px; }
.skeleton::after { content: ''; position: absolute; inset: 0; transform: translateX(-100%);
background: linear-gradient(90deg, transparent, rgba(255,255,255,.6), transparent);
animation: shimmer 1.2s infinite; }
@keyframes shimmer { 100% { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) { .skeleton::after { animation: none; } }
`;
function Style() { return <style dangerouslySetInnerHTML={{ __html: css }} />; }
export function CardSection() {
return (
<div>
<Style />
<Suspense fallback={<div className='skeleton' />}>
<HeavyCard />
</Suspense>
</div>
);
}
스켈레톤은 실제 레이아웃과 최대한 유사하게 구성하고, 점프(레이아웃 시프트)가 없도록 크기를 고정합니다.
5. 라우트 전환 애니메이션 (React Router v6 + Framer Motion)
페이지 전환 시 페이드/슬라이드로 맥락을 유지합니다. location을 키로 전달해 AnimatePresence가 경로 변경을 인식하도록 합니다.
import { Routes, Route, useLocation } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
function Page({ title }) {
return (
<motion.main
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.28 }}
style={{ padding: 24 }}
>
<h1>{title}</h1>
</motion.main>
);
}
export default function AppRoutes() {
const location = useLocation();
return (
<AnimatePresence mode='wait'>
<Routes location={location} key={location.pathname}>
<Route path='/' element={<Page title='Home' />} />
<Route path='/about' element={<Page title='About' />} />
</Routes>
</AnimatePresence>
);
}
공통 레이아웃(Header/Footer)은 밖에 두고, 페이지 컨텐츠 영역만 애니메이션하는 것이 안정적입니다.
6. React 18 useTransition으로 체감 성능 개선
무거운 상태 업데이트를 transition으로 표시하면 입력 반응성은 유지하고, 로딩 상태에만 애니메이션/스켈레톤을 적용할 수 있습니다.
import React, { useState, useTransition } from 'react';
import { motion } from 'framer-motion';
export function Search() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
function onChange(e) {
const v = e.target.value;
startTransition(() => setQuery(v));
}
return (
<div>
<input placeholder='검색' onChange={onChange} />
{isPending ? (
<div style={{ height: 80 }}>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: .2 }}>
로딩 중...
</motion.div>
</div>
) : (
<Results query={query} />
)}
</div>
);
}
function Results({ query }) {
// 무거운 목록 렌더링 가정
return <div>결과: {query}</div>;
}
isPending 동안은 단순한 페이드만 사용해 메인 콘텐츠 점프를 피합니다.
7. 성능/접근성 체크리스트
1) transform/opacity 위주로 전환해 레이아웃 리플로우를 피합니다. 2) will-change는 남발하지 말고 애니메이션 직전에만 적용합니다. 3) Transition 시간은 160~280ms 구간이 무난합니다. 4) 모션 민감 사용자를 위해 prefers-reduced-motion, useReducedMotion을 반드시 처리합니다. 5) 이미지 onLoad 시 페이드 인을 적용하면 콘텐츠 깜빡임을 줄일 수 있습니다.
import React, { useState } from 'react';
export function SmartImage({ src, alt, ...rest }) {
const [loaded, setLoaded] = useState(false);
return (
<img
{...rest}
src={src}
alt={alt}
onLoad={() => setLoaded(true)}
style={{
opacity: loaded ? 1 : 0,
transform: loaded ? 'none' : 'translateY(4px)',
transition: 'opacity .22s ease, transform .22s ease'
}}
/>
);
}
결론: 상황에 따라 가장 단순한 방법부터 적용하고, 언마운트 제어가 필요하면 React Transition Group, 페이지/상호작용 수준에서는 Framer Motion을 권장합니다. 로딩 구간에는 스켈레톤과 짧은 페이드를 조합해 사용자의 맥락 유지와 체감 성능을 개선합니다.
'React' 카테고리의 다른 글
| React 앱에서 클라우드 스토리지(AWS S3, GCP Storage) 연동하기 (0) | 2026.06.12 |
|---|---|
| React에서 HTML5 Canvas를 활용한 인터랙티브 그래픽 그리기 (0) | 2026.06.12 |
| React 앱에서 QR 코드 생성 및 스캐닝 기능 추가하기 (0) | 2026.06.11 |
| React에서 스크롤 위치 저장 및 복원 기능 구현하기 (1) | 2026.06.10 |
| React 앱에 서버리스 함수(Function) 연동하기 (0) | 2026.06.05 |