Suspense와 Lazy Loading
4. Suspense와 Lazy Loading
React의 Suspense는 컴포넌트가 렌더링할 준비가 완료될 때까지 대기할 수 있게 해주는 기능입니다. 주로 코드 분할(code splitting)과 데이터 로딩에 사용됩니다.
React.lazy를 이용한 코드 분할
기본 사용법
import { lazy, Suspense, useState } from 'react';
// lazy로 컴포넌트를 동적 로딩
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
const [showLazy, setShowLazy] = useState(false);
return (
<div>
<h1>Suspense 예제</h1>
<button onClick={() => setShowLazy(!showLazy)}>
{showLazy ? '컴포넌트 숨기기' : '컴포넌트 보기'}
</button>
{showLazy && (
<Suspense fallback={<div>로딩 중...</div>}>
<LazyComponent />
</Suspense>
)}
</div>
);
}
별도 파일로 분리된 컴포넌트
// LazyComponent.js
import { useState } from 'react';
function LazyComponent() {
const [count, setCount] = useState(0);
return (
<div>
<h2>지연 로딩된 컴포넌트</h2>
<p>이 컴포넌트는 필요할 때만 로드됩니다.</p>
<button onClick={() => setCount(count + 1)}>
카운트: {count}
</button>
</div>
);
}
export default LazyComponent;
다중 컴포넌트 lazy loading
여러 컴포넌트를 조건부로 로딩
import { lazy, Suspense, useState } from 'react';
// 여러 컴포넌트를 lazy loading
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const Settings = lazy(() => import('./Settings'));
const Reports = lazy(() => import('./Reports'));
function MultiPageApp() {
const [currentPage, setCurrentPage] = useState('home');
const renderPage = () => {
switch (currentPage) {
case 'dashboard':
return <Dashboard />;
case 'profile':
return <Profile />;
case 'settings':
return <Settings />;
case 'reports':
return <Reports />;
default:
return (
<div>
<h2>홈페이지</h2>
<p>원하는 페이지를 선택하세요.</p>
</div>
);
}
};
return (
<div>
<nav>
<button
onClick={() => setCurrentPage('home')}
>
홈
</button>
<button
onClick={() => setCurrentPage('dashboard')}
>
대시보드
</button>
<button
onClick={() => setCurrentPage('profile')}
>
프로필
</button>
<button
onClick={() => setCurrentPage('settings')}
>
설정
</button>
<button onClick={() => setCurrentPage('reports')}>
리포트
</button>
</nav>
<main>
<Suspense fallback={<div>페이지를 로딩하는 중...</div>}>
{renderPage()}
</Suspense>
</main>
</div>
);
}
각 페이지 컴포넌트 예제
// Dashboard.js
function Dashboard() {
return (
<div>
<h2>대시보드</h2>
<div>
<div>
<h3>매출 현황</h3>
<p>이번 달 매출: 1,500,000원</p>
</div>
<div>
<h3>사용자 통계</h3>
<p>활성 사용자: 1,234명</p>
</div>
</div>
</div>
);
}
export default Dashboard;
// Profile.js
import { useState } from 'react';
function Profile() {
const [user, setUser] = useState({
name: '김개발',
email: 'kim@example.com',
bio: 'React 개발자입니다.'
});
const [isEditing, setIsEditing] = useState(false);
return (
<div>
<h2>프로필</h2>
{isEditing ? (
<div>
<input
value={user.name}
onChange={(e) => setUser({ ...user, name: e.target.value })}
placeholder="이름"
/>
<input
value={user.email}
onChange={(e) => setUser({ ...user, email: e.target.value })}
placeholder="이메일"
/>
<textarea
value={user.bio}
onChange={(e) => setUser({ ...user, bio: e.target.value })}
placeholder="자기소개"
/>
<div>
<button onClick={() => setIsEditing(false)}>저장</button>
<button onClick={() => setIsEditing(false)}>취소</button>
</div>
</div>
) : (
<div>
<p>이름: {user.name}</p>
<p>이메일: {user.email}</p>
<p>소개: {user.bio}</p>
<button onClick={() => setIsEditing(true)}>편집</button>
</div>
)}
</div>
);
}
export default Profile;
중첩된 Suspense
계층적 로딩 구조
import { lazy, Suspense, useState } from 'react';
const UserList = lazy(() => import('./UserList'));
const UserDetail = lazy(() => import('./UserDetail'));
// UserList.js의 내용
const LazyUserList = lazy(() =>
new Promise(resolve => {
setTimeout(() => {
resolve({
default: function UserList({ onSelectUser }) {
const users = [
{ id: 1, name: '김개발', email: 'kim@example.com' },
{ id: 2, name: '이디자인', email: 'lee@example.com' },
{ id: 3, name: '박기획', email: 'park@example.com' }
];
return (
<div>
<h3>사용자 목록</h3>
{users.map(user => (
<div key={user.id}>
<p>{user.name} ({user.email})</p>
<button onClick={() => onSelectUser(user)}>
상세보기
</button>
</div>
))}
</div>
);
}
});
}, 1000); // 1초 지연
})
);
const LazyUserDetail = lazy(() =>
new Promise(resolve => {
setTimeout(() => {
resolve({
default: function UserDetail({ user, onBack }) {
if (!user) return null;
return (
<div>
<button onClick={onBack}>← 뒤로가기</button>
<h3>{user.name}의 상세 정보</h3>
<p>이메일: {user.email}</p>
<p>가입일: 2024-01-15</p>
<p>마지막 로그인: 2024-12-20</p>
<Suspense fallback={<div>추가 정보 로딩 중...</div>}>
<LazyUserStats userId={user.id} />
</Suspense>
</div>
);
}
});
}, 800); // 0.8초 지연
})
);
const LazyUserStats = lazy(() =>
new Promise(resolve => {
setTimeout(() => {
resolve({
default: function UserStats({ userId }) {
const stats = {
posts: 42,
comments: 156,
likes: 89
};
return (
<div>
<h4>활동 통계</h4>
<p>작성한 게시글: {stats.posts}개</p>
<p>댓글: {stats.comments}개</p>
<p>받은 좋아요: {stats.likes}개</p>
</div>
);
}
});
}, 500); // 0.5초 지연
})
);
function NestedSuspenseExample() {
const [selectedUser, setSelectedUser] = useState(null);
const [showUsers, setShowUsers] = useState(false);
if (selectedUser) {
return (
<div>
<h2>사용자 관리 시스템</h2>
<Suspense fallback={<div>사용자 상세 정보 로딩 중...</div>}>
<LazyUserDetail
user={selectedUser}
onBack={() => setSelectedUser(null)}
/>
</Suspense>
</div>
);
}
return (
<div>
<h2>사용자 관리 시스템</h2>
<button onClick={() => setShowUsers(!showUsers)}>
{showUsers ? '사용자 목록 숨기기' : '사용자 목록 보기'}
</button>
{showUsers && (
<Suspense fallback={<div>사용자 목록 로딩 중...</div>}>
<LazyUserList onSelectUser={setSelectedUser} />
</Suspense>
)}
</div>
);
}
에러 경계와 함께 사용하기
Suspense와 ErrorBoundary 조합
import { lazy, Suspense, Component } from 'react';
class SuspenseErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Suspense 로딩 중 오류 발생:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h3>컴포넌트를 불러오는 중 오류가 발생했습니다</h3>
<button onClick={() => this.setState({ hasError: false })}>
다시 시도
</button>
</div>
);
}
return this.props.children;
}
}
// 로딩에 실패할 수 있는 컴포넌트
const ProblematicComponent = lazy(() =>
new Promise((resolve, reject) => {
setTimeout(() => {
// 50% 확률로 로딩 실패
if (Math.random() > 0.5) {
resolve({
default: function ProblematicComponent() {
return <div>성공적으로 로드된 컴포넌트!</div>;
}
});
} else {
reject(new Error('컴포넌트 로딩 실패'));
}
}, 2000);
})
);
function ErrorHandlingExample() {
const [showComponent, setShowComponent] = useState(false);
return (
<div>
<h2>에러 처리가 포함된 Suspense</h2>
<button onClick={() => setShowComponent(!showComponent)}>
{showComponent ? '컴포넌트 숨기기' : '컴포넌트 로드하기'}
</button>
{showComponent && (
<SuspenseErrorBoundary>
<Suspense fallback={<div>컴포넌트 로딩 중... (실패할 수 있음)</div>}>
<ProblematicComponent />
</Suspense>
</SuspenseErrorBoundary>
)}
</div>
);
}
실습: 이미지 갤러리 애플리케이션
이미지를 lazy loading으로 로드하는 갤러리
import { lazy, Suspense, useState } from 'react';
// 이미지 컴포넌트를 동적으로 생성
const createLazyImageComponent = (imageSrc, delay = 1000) => {
return lazy(() =>
new Promise(resolve => {
const img = new Image();
img.onload = () => {
setTimeout(() => {
resolve({
default: function LazyImage() {
return (
<div>
<img
src={imageSrc}
alt="갤러리 이미지"
/>
</div>
);
}
});
}, delay);
};
img.onerror = () => {
resolve({
default: function ErrorImage() {
return (
<div>
<p>이미지 로드 실패</p>
</div>
);
}
});
};
img.src = imageSrc;
})
);
};
function ImageGallery() {
const [visibleImages, setVisibleImages] = useState(new Set());
// 샘플 이미지 URL들 (실제로는 API에서 받아올 수 있음)
const imageUrls = [
'https://picsum.photos/400/300?random=1',
'https://picsum.photos/400/300?random=2',
'https://picsum.photos/400/300?random=3',
'https://picsum.photos/400/300?random=4',
'https://picsum.photos/400/300?random=5',
'https://picsum.photos/400/300?random=6'
];
const toggleImage = (index) => {
setVisibleImages(prev => {
const newSet = new Set(prev);
if (newSet.has(index)) {
newSet.delete(index);
} else {
newSet.add(index);
}
return newSet;
});
};
const showAllImages = () => {
setVisibleImages(new Set(imageUrls.map((_, index) => index)));
};
const hideAllImages = () => {
setVisibleImages(new Set());
};
return (
<div>
<h2>이미지 갤러리</h2>
<div>
<button onClick={showAllImages}>
모든 이미지 보기
</button>
<button onClick={hideAllImages}>
모든 이미지 숨기기
</button>
</div>
<div>
{imageUrls.map((url, index) => (
<div key={index}>
<button
onClick={() => toggleImage(index)}
>
이미지 {index + 1} {visibleImages.has(index) ? '숨기기' : '보기'}
</button>
{visibleImages.has(index) && (
<Suspense fallback={
<div>
<p>이미지 로딩 중...</p>
</div>
}>
{(() => {
const LazyImage = createLazyImageComponent(url, Math.random() * 2000 + 500);
return <LazyImage />;
})()}
</Suspense>
)}
</div>
))}
</div>
</div>
);
}
라우터와 함께 사용하기
페이지별 코드 분할
import { lazy, Suspense } from 'react';
// 각 페이지를 별도 파일로 분리하여 lazy loading
const HomePage = lazy(() => import('./pages/HomePage'));
const AboutPage = lazy(() => import('./pages/AboutPage'));
const ContactPage = lazy(() => import('./pages/ContactPage'));
const ProductsPage = lazy(() => import('./pages/ProductsPage'));
function App() {
const [currentRoute, setCurrentRoute] = useState('home');
const renderRoute = () => {
switch (currentRoute) {
case 'home':
return <HomePage />;
case 'about':
return <AboutPage />;
case 'contact':
return <ContactPage />;
case 'products':
return <ProductsPage />;
default:
return <HomePage />;
}
};
const routes = [
{ key: 'home', label: '홈' },
{ key: 'about', label: '소개' },
{ key: 'contact', label: '연락처' },
{ key: 'products', label: '제품' }
];
return (
<div>
<nav>
<h1>My Website</h1>
<div>
{routes.map(route => (
<button
key={route.key}
onClick={() => setCurrentRoute(route.key)}
>
{route.label}
</button>
))}
</div>
</nav>
<main>
<Suspense fallback={
<div>
<div>페이지를 불러오는 중...</div>
</div>
}>
{renderRoute()}
</Suspense>
</main>
</div>
);
}
성능 최적화 팁
프리로딩(Preloading) 기법
import { lazy, Suspense, useState, useEffect } from 'react';
// 컴포넌트를 미리 로드하는 함수
const preloadComponent = (componentImport) => {
componentImport();
};
const HeavyComponent = lazy(() => import('./HeavyComponent'));
// 홈 화면에서 마우스를 올리면 미리 로드
function PreloadingExample() {
const [showHeavy, setShowHeavy] = useState(false);
const [isPreloaded, setIsPreloaded] = useState(false);
const handleMouseEnter = () => {
if (!isPreloaded) {
console.log('컴포넌트 프리로딩 시작');
preloadComponent(() => import('./HeavyComponent'));
setIsPreloaded(true);
}
};
return (
<div>
<h2>프리로딩 예제</h2>
<p>버튼에 마우스를 올리면 컴포넌트가 미리 로드됩니다.</p>
<button
onMouseEnter={handleMouseEnter}
onClick={() => setShowHeavy(!showHeavy)}
>
{showHeavy ? '무거운 컴포넌트 숨기기' : '무거운 컴포넌트 보기'}
</button>
{showHeavy && (
<Suspense fallback={<div>로딩 중...</div>}>
<HeavyComponent />
</Suspense>
)}
</div>
);
}
정리
Suspense와 Lazy Loading의 핵심 개념을 학습했습니다:
- React.lazy: 컴포넌트를 동적으로 로딩하여 번들 크기 최적화
- Suspense: 비동기 로딩 중 fallback UI 표시
- 코드 분할: 페이지별, 기능별로 코드를 나누어 초기 로딩 시간 단축
- 중첩 Suspense: 계층적 로딩 구조로 세밀한 로딩 상태 관리
- 에러 처리: ErrorBoundary와 함께 사용하여 로딩 실패 처리
- 성능 최적화: 프리로딩 기법으로 사용자 경험 개선
- 실전 활용: 라우터, 이미지 갤러리 등에서의 활용법
Suspense와 lazy loading은 대규모 애플리케이션에서 성능을 크게 개선할 수 있는 중요한 기능입니다. 사용자가 실제로 필요로 하는 코드만 로드하여 초기 로딩 시간을 단축하고, 네트워크 사용량을 줄일 수 있습니다.
다음 장에서는 React의 고급 Hook 패턴에 대해 알아보겠습니다.