suspense-and-lazy
React Suspense와 Lazy: 스마트한 코드 분할로 성능 최적화하기
웹 애플리케이션이 점점 복잡해지면서 초기 로딩 시간이 길어지는 문제에 직면하게 됩니다. 사용자가 당장 필요하지 않은 코드까지 모두 다운로드해야 할까요? React의 Suspense와 Lazy를 활용하면 필요한 시점에만 코드를 로드하여 애플리케이션 성능을 크게 개선할 수 있습니다.
코드 분할이 필요한 이유
전통적인 번들링의 문제점
일반적인 React 애플리케이션은 모든 컴포넌트와 라이브러리를 하나의 큰 번들로 만듭니다:
javascript
// 모든 컴포넌트가 초기에 로드됨
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import ContactPage from './pages/ContactPage';
import AdminDashboard from './pages/AdminDashboard'; // 관리자만 사용
import HeavyChart from './components/HeavyChart'; // 큰 차트 라이브러리
이런 방식의 문제점:
- 초기 로딩 시간 증가: 사용하지 않는 코드도 모두 다운로드
- 메모리 낭비: 불필요한 컴포넌트들이 메모리에 상주
- 네트워크 비용: 모바일 환경에서 데이터 사용량 증가
코드 분할의 장점
javascript
// 필요할 때만 로드되는 컴포넌트들
const HomePage = lazy(() => import('./pages/HomePage'));
const AboutPage = lazy(() => import('./pages/AboutPage'));
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
장점:
- 빠른 초기 로딩: 필수 코드만 먼저 로드
- 효율적인 리소스 사용: 필요한 시점에만 다운로드
- 더 나은 사용자 경험: 페이지 전환이 더 빠르게 느껴짐
React.lazy() 기본 사용법
기본 문법
React.lazy()
는 동적 import를 사용하여 컴포넌트를 지연 로딩합니다:
javascript
import React, { lazy, Suspense } from 'react';
// 지연 로딩할 컴포넌트 정의
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<div>
<h1>메인 애플리케이션</h1>
<Suspense fallback={<div>로딩 중...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
조건부 렌더링과 함께 사용
javascript
import React, { lazy, Suspense, useState } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
const [showHeavy, setShowHeavy] = useState(false);
return (
<div>
<button onClick={() => setShowHeavy(!showHeavy)}>
{showHeavy ? '숨기기' : '무거운 컴포넌트 보기'}
</button>
{showHeavy && (
<Suspense fallback={<div>컴포넌트 로딩 중...</div>}>
<HeavyComponent />
</Suspense>
)}
</div>
);
}
Suspense 심화 활용
중첩된 Suspense 경계
여러 레벨의 Suspense를 사용하여 세밀한 로딩 제어가 가능합니다:
javascript
import React, { lazy, Suspense } from 'react';
const Header = lazy(() => import('./Header'));
const Sidebar = lazy(() => import('./Sidebar'));
const MainContent = lazy(() => import('./MainContent'));
function Dashboard() {
return (
<div className="dashboard">
{/* 헤더는 빠르게 로드 */}
<Suspense fallback={<div className="header-skeleton">헤더 로딩...</div>}>
<Header />
</Suspense>
<div className="dashboard-body">
{/* 사이드바와 메인 콘텐츠는 별도로 로드 */}
<Suspense fallback={<div className="sidebar-skeleton">사이드바 로딩...</div>}>
<Sidebar />
</Suspense>
<Suspense fallback={<div className="content-skeleton">콘텐츠 로딩...</div>}>
<MainContent />
</Suspense>
</div>
</div>
);
}
에러 경계와 함께 사용
javascript
import React, { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Lazy loading error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div>컴포넌트 로딩에 실패했습니다. 페이지를 새로고침해주세요.</div>;
}
return this.props.children;
}
}
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<div>로딩 중...</div>}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}
실제 사용 패턴과 예시
1. 라우트 기반 코드 분할
가장 일반적인 패턴으로, 페이지별로 코드를 분할합니다:
javascript
import React, { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 페이지별 지연 로딩
const HomePage = lazy(() => import('./pages/HomePage'));
const AboutPage = lazy(() => import('./pages/AboutPage'));
const ProductPage = lazy(() => import('./pages/ProductPage'));
const AdminPage = lazy(() => import('./pages/AdminPage'));
function App() {
return (
<BrowserRouter>
<div className="app">
<nav>
{/* 네비게이션은 즉시 로드 */}
</nav>
<main>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/products" element={<ProductPage />} />
<Route path="/admin" element={<AdminPage />} />
</Routes>
</Suspense>
</main>
</div>
</BrowserRouter>
);
}
// 페이지 로딩 컴포넌트
function PageLoader() {
return (
<div className="page-loader">
<div className="spinner"></div>
<p>페이지를 불러오는 중...</p>
</div>
);
}
2. 기능별 코드 분할
특정 기능이나 모듈별로 분할하는 방법:
javascript
import React, { lazy, Suspense, useState } from 'react';
// 기능별 지연 로딩
const ChartModule = lazy(() => import('./modules/ChartModule'));
const TableModule = lazy(() => import('./modules/TableModule'));
const MapModule = lazy(() => import('./modules/MapModule'));
function Dashboard() {
const [activeTab, setActiveTab] = useState('chart');
const renderContent = () => {
switch (activeTab) {
case 'chart':
return (
<Suspense fallback={<ModuleLoader />}>
<ChartModule />
</Suspense>
);
case 'table':
return (
<Suspense fallback={<ModuleLoader />}>
<TableModule />
</Suspense>
);
case 'map':
return (
<Suspense fallback={<ModuleLoader />}>
<MapModule />
</Suspense>
);
default:
return <div>탭을 선택해주세요.</div>;
}
};
return (
<div className="dashboard">
<div className="tabs">
<button
className={activeTab === 'chart' ? 'active' : ''}
onClick={() => setActiveTab('chart')}
>
차트
</button>
<button
className={activeTab === 'table' ? 'active' : ''}
onClick={() => setActiveTab('table')}
>
테이블
</button>
<button
className={activeTab === 'map' ? 'active' : ''}
onClick={() => setActiveTab('map')}
>
지도
</button>
</div>
<div className="content">
{renderContent()}
</div>
</div>
);
}
function ModuleLoader() {
return (
<div className="module-loader">
<div className="skeleton-box"></div>
<div className="skeleton-text"></div>
</div>
);
}
3. 조건부 기능 로딩
사용자 권한이나 특정 조건에 따라 로딩하는 패턴:
javascript
import React, { lazy, Suspense } from 'react';
// 관리자 전용 컴포넌트
const AdminPanel = lazy(() => import('./AdminPanel'));
const UserSettings = lazy(() => import('./UserSettings'));
function UserDashboard({ user }) {
return (
<div className="user-dashboard">
<h1>안녕하세요, {user.name}님!</h1>
{/* 모든 사용자가 볼 수 있는 설정 */}
<Suspense fallback={<div>설정 로딩 중...</div>}>
<UserSettings user={user} />
</Suspense>
{/* 관리자만 볼 수 있는 패널 */}
{user.role === 'admin' && (
<Suspense fallback={<div>관리자 패널 로딩 중...</div>}>
<AdminPanel />
</Suspense>
)}
</div>
);
}
성능 최적화 기법
1. 프리로딩 (Preloading)
사용자가 필요로 할 가능성이 높은 컴포넌트를 미리 로드합니다:
javascript
import React, { lazy, Suspense, useEffect } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
// 프리로딩 함수
const preloadHeavyComponent = () => {
import('./HeavyComponent');
};
function App() {
useEffect(() => {
// 컴포넌트 마운트 후 3초 뒤에 프리로드
const timer = setTimeout(preloadHeavyComponent, 3000);
return () => clearTimeout(timer);
}, []);
const [showHeavy, setShowHeavy] = useState(false);
return (
<div>
<button
onMouseEnter={preloadHeavyComponent} // 마우스 호버 시 프리로드
onClick={() => setShowHeavy(true)}
>
무거운 컴포넌트 보기
</button>
{showHeavy && (
<Suspense fallback={<div>로딩 중...</div>}>
<HeavyComponent />
</Suspense>
)}
</div>
);
}
2. 청크 이름 지정
Webpack의 매직 코멘트를 사용하여 청크 이름을 지정합니다:
javascript
// 청크 이름을 명시적으로 지정
const HomePage = lazy(() =>
import(/* webpackChunkName: "home-page" */ './pages/HomePage')
);
const AdminDashboard = lazy(() =>
import(/* webpackChunkName: "admin-dashboard" */ './pages/AdminDashboard')
);
// 관련된 컴포넌트들을 같은 청크로 그룹화
const ChartComponents = lazy(() =>
import(/* webpackChunkName: "chart-components" */ './components/Charts')
);
3. 리소스 힌트 활용
javascript
// 중요한 청크는 prefetch로 미리 로드
const CriticalComponent = lazy(() =>
import(/* webpackChunkName: "critical", webpackPrefetch: true */ './CriticalComponent')
);
// 다음 네비게이션에서 필요할 가능성이 높은 청크는 preload
const NextPageComponent = lazy(() =>
import(/* webpackChunkName: "next-page", webpackPreload: true */ './NextPageComponent')
);
로딩 상태 개선하기
스켈레톤 UI 구현
javascript
import React, { lazy, Suspense } from 'react';
const ProductList = lazy(() => import('./ProductList'));
function ProductListSkeleton() {
return (
<div className="product-list-skeleton">
{Array.from({ length: 6 }).map((_, index) => (
<div key={index} className="product-card-skeleton">
<div className="skeleton-image"></div>
<div className="skeleton-title"></div>
<div className="skeleton-price"></div>
</div>
))}
</div>
);
}
function ProductPage() {
return (
<div className="product-page">
<h1>상품 목록</h1>
<Suspense fallback={<ProductListSkeleton />}>
<ProductList />
</Suspense>
</div>
);
}
점진적 로딩
javascript
import React, { lazy, Suspense, useState, useEffect } from 'react';
const BasicContent = lazy(() => import('./BasicContent'));
const EnhancedContent = lazy(() => import('./EnhancedContent'));
function ProgressiveLoadingPage() {
const [loadEnhanced, setLoadEnhanced] = useState(false);
useEffect(() => {
// 기본 콘텐츠 로드 후 향상된 기능 로드
const timer = setTimeout(() => {
setLoadEnhanced(true);
}, 2000);
return () => clearTimeout(timer);
}, []);
return (
<div className="progressive-loading-page">
<Suspense fallback={<div>기본 콘텐츠 로딩 중...</div>}>
<BasicContent />
</Suspense>
{loadEnhanced && (
<Suspense fallback={<div>향상된 기능 로딩 중...</div>}>
<EnhancedContent />
</Suspense>
)}
</div>
);
}
에러 처리 및 재시도 로직
네트워크 오류 처리
javascript
import React, { lazy, Suspense } from 'react';
// 재시도 로직이 포함된 지연 로딩
const createRetryableLazy = (importFunc, retries = 3) => {
return lazy(() => {
return new Promise((resolve, reject) => {
const attemptImport = (remainingRetries) => {
importFunc()
.then(resolve)
.catch((error) => {
if (remainingRetries > 0) {
console.warn(`Import failed, retrying... (${remainingRetries} attempts left)`);
setTimeout(() => attemptImport(remainingRetries - 1), 1000);
} else {
reject(error);
}
});
};
attemptImport(retries);
});
});
};
const ReliableComponent = createRetryableLazy(
() => import('./ReliableComponent'),
3 // 3번까지 재시도
);
function App() {
return (
<div>
<Suspense fallback={<div>로딩 중...</div>}>
<ReliableComponent />
</Suspense>
</div>
);
}
사용자 친화적인 에러 처리
javascript
import React, { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./LazyComponent'));
class LazyLoadErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
handleRetry = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return (
<div className="lazy-load-error">
<h3>컴포넌트 로딩에 실패했습니다</h3>
<p>네트워크 연결을 확인하고 다시 시도해주세요.</p>
<button onClick={this.handleRetry}>다시 시도</button>
<details>
<summary>오류 세부사항</summary>
<pre>{this.state.error?.message}</pre>
</details>
</div>
);
}
return this.props.children;
}
}
function App() {
return (
<LazyLoadErrorBoundary>
<Suspense fallback={<div>로딩 중...</div>}>
<LazyComponent />
</Suspense>
</LazyLoadErrorBoundary>
);
}
Next.js에서의 활용
동적 import와 함께 사용
javascript
import dynamic from 'next/dynamic';
import { Suspense } from 'react';
// Next.js의 dynamic import
const DynamicComponent = dynamic(
() => import('../components/HeavyComponent'),
{
loading: () => <p>로딩 중...</p>,
ssr: false // 클라이언트 사이드에서만 로드
}
);
// React의 lazy와 Suspense 조합
const LazyComponent = lazy(() => import('../components/LazyComponent'));
function NextJsPage() {
return (
<div>
<h1>Next.js 페이지</h1>
{/* Next.js 방식 */}
<DynamicComponent />
{/* React 방식 */}
<Suspense fallback={<div>React Lazy 로딩 중...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
export default NextJsPage;
성능 측정 및 모니터링
로딩 시간 측정
javascript
import React, { lazy, Suspense, useEffect } from 'react';
const MeasuredComponent = lazy(() => {
const startTime = performance.now();
return import('./HeavyComponent').then((module) => {
const endTime = performance.now();
console.log(`Component loaded in ${endTime - startTime}ms`);
// 성능 데이터를 분석 도구로 전송
if (window.gtag) {
window.gtag('event', 'lazy_component_load', {
event_category: 'performance',
event_label: 'HeavyComponent',
value: Math.round(endTime - startTime)
});
}
return module;
});
});
function App() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
<MeasuredComponent />
</Suspense>
);
}
번들 크기 분석
bash
# 번들 분석기를 사용하여 코드 분할 효과 확인
npm install --save-dev webpack-bundle-analyzer
# package.json에 스크립트 추가
{
"scripts": {
"analyze": "npm run build && npx webpack-bundle-analyzer build/static/js/*.js"
}
}
모범 사례 및 주의사항
권장사항
- 적절한 분할 단위: 너무 작게 나누면 오히려 성능이 저하될 수 있습니다
- 의미있는 로딩 상태: 사용자에게 명확한 피드백을 제공하세요
- 에러 처리: 네트워크 오류나 로딩 실패에 대비하세요
- 프리로딩 활용: 사용자 행동을 예측하여 미리 로드하세요
피해야 할 패턴
javascript
// ❌ 너무 작은 컴포넌트 분할
const TinyComponent = lazy(() => import('./TinyComponent')); // 1KB 미만
// ❌ 즉시 필요한 컴포넌트 분할
const CriticalHeader = lazy(() => import('./Header')); // 항상 표시되는 헤더
// ❌ 에러 처리 없음
function BadExample() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
<LazyComponent /> {/* 에러 경계 없음 */}
</Suspense>
);
}
// ✅ 적절한 크기와 에러 처리
const FeatureModule = lazy(() => import('./FeatureModule')); // 의미있는 크기
function GoodExample() {
return (
<ErrorBoundary>
<Suspense fallback={<FeatureModuleSkeleton />}>
<FeatureModule />
</Suspense>
</ErrorBoundary>
);
}
마무리
React의 Suspense와 Lazy를 활용한 코드 분할은 현대 웹 애플리케이션의 성능 최적화에 필수적인 기법입니다. 적절히 활용하면:
- 초기 로딩 시간 단축: 필요한 코드만 먼저 로드
- 사용자 경험 개선: 빠른 페이지 전환과 반응성
- 리소스 효율성: 네트워크와 메모리 사용량 최적화
- 확장성: 애플리케이션이 커져도 성능 유지
하지만 무분별한 코드 분할은 오히려 성능을 저하시킬 수 있으므로, 사용자의 행동 패턴과 애플리케이션의 특성을 고려하여 전략적으로 적용하는 것이 중요합니다.
성능 측정 도구를 활용하여 분할 효과를 지속적으로 모니터링하고, 사용자 피드백을 바탕으로 최적화를 개선해 나가시기 바랍니다.