코드 스플릿팅 (Code Splitting)
개요
SPA(Single Page Application)는 기본적으로 모든 JavaScript를 하나의 번들 파일로 만든다. 앱이 커지면 이 번들도 커지고, 사용자는 페이지에 접속할 때 사용하지 않을 코드까지 전부 다운로드해야 한다. 코드 스플릿팅은 번들을 여러 청크로 나눠서 필요한 시점에 로드하는 기법이다.
왜 필요한가
번들 크기 문제
[코드 스플릿팅 없이]
bundle.js (2MB) ─────────────────────────────────▶ 전부 다운로드
├── 홈페이지 코드
├── 대시보드 코드
├── 설정 페이지 코드
├── 차트 라이브러리
└── 에디터 라이브러리
사용자가 홈페이지만 보더라도 2MB 전체를 다운로드해야 함
코드 스플릿팅 적용 후
[코드 스플릿팅 적용]
main.js (200KB) ──────────────────────────────────▶ 초기 로드
└── 홈페이지 코드, 공통 코드
dashboard.js (500KB) ─────────────────────────────▶ 대시보드 진입 시 로드
settings.js (100KB) ──────────────────────────────▶ 설정 진입 시 로드
chart-vendor.js (800KB) ──────────────────────────▶ 차트 사용 시 로드
홈페이지만 보면 200KB만 다운로드
초기 로딩 속도가 빨라지고, 사용자가 실제로 사용하는 기능만 로드한다.
동적 import
코드 스플릿팅의 핵심은 ES2020의 동적 import다.
정적 import (기존)
[코드 스플릿팅 없이]
bundle.js (2MB) ─────────────────────────────────▶ 전부 다운로드
├── 홈페이지 코드
├── 대시보드 코드
├── 설정 페이지 코드
├── 차트 라이브러리
└── 에디터 라이브러리
사용자가 홈페이지만 보더라도 2MB 전체를 다운로드해야 함
정적 import는 빌드 시점에 모듈을 번들에 포함시킨다. 해당 컴포넌트를 사용하지 않아도 번들에 들어간다.
동적 import
[코드 스플릿팅 적용]
main.js (200KB) ──────────────────────────────────▶ 초기 로드
└── 홈페이지 코드, 공통 코드
dashboard.js (500KB) ─────────────────────────────▶ 대시보드 진입 시 로드
settings.js (100KB) ──────────────────────────────▶ 설정 진입 시 로드
chart-vendor.js (800KB) ──────────────────────────▶ 차트 사용 시 로드
홈페이지만 보면 200KB만 다운로드
동적 import는 Promise를 반환한다. 해당 코드가 실행될 때 네트워크 요청으로 모듈을 가져온다.
번들러의 역할
Webpack, Vite 같은 번들러는 동적 import를 만나면 자동으로 코드를 분리한다.
import { Chart } from 'chart-library'; // 번들에 무조건 포함
function Dashboard() {
return <Chart />;
}
번들러가 하는 일:
- 동적 import 구문 감지
- 해당 모듈과 의존성을 별도 청크로 분리
- 런타임에 청크를 로드하는 코드 생성
React에서의 코드 스플릿팅
React는 React.lazy와 Suspense로 컴포넌트 단위 코드 스플릿팅을 지원한다.
React.lazy
// 함수 호출 시점에 모듈을 로드
const chartModule = await import('chart-library');
const Chart = chartModule.Chart;
lazy()는 동적 import를 반환하는 함수를 받아서, 컴포넌트가 렌더링될 때 해당 모듈을 로드한다.
Suspense
Suspense는 lazy 컴포넌트가 로드되는 동안 fallback UI를 보여준다. lazy 컴포넌트는 반드시 Suspense로 감싸야 한다.
// 이 코드를 번들러가 분석하면
const Page = await import('./pages/Dashboard');
// 빌드 결과물
// - main.js (이 import 문을 포함)
// - Dashboard-abc123.js (분리된 청크)
라우트 기반 스플릿팅
가장 효과적인 스플릿팅 지점은 **라우트(페이지)**다. 사용자가 특정 페이지에 진입할 때만 해당 페이지 코드를 로드한다.
import { lazy, Suspense } from 'react';
// 동적 import를 lazy로 감싸기
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
왜 라우트 단위가 효과적인가
- 자연스러운 분리 지점: 페이지 전환은 사용자가 "기다릴 준비가 된" 시점
- 큰 청크 분리: 페이지는 보통 여러 컴포넌트를 포함해서 분리 효과가 큼
- 예측 가능한 로딩: 어떤 페이지가 언제 로드될지 명확함
컴포넌트 단위 스플릿팅
무거운 라이브러리를 사용하는 컴포넌트도 분리할 수 있다.
<Suspense fallback={<div>로딩 중...</div>}>
<Dashboard />
</Suspense>
차트를 보기 전까지는 chart.js 라이브러리를 로드하지 않는다.
빌드 결과 확인
Vite에서는 rollup-plugin-visualizer로 번들 구성을 시각화할 수 있다.
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Profile = lazy(() => import('./pages/Profile'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
);
}
빌드 후 생성되는 bundle-stats.html에서 각 청크의 크기와 구성을 확인할 수 있다.
주의사항
과도한 스플릿팅
너무 작은 단위로 쪼개면 오히려 성능이 나빠질 수 있다. HTTP 요청 오버헤드가 있기 때문이다.
// 차트 컴포넌트 - chart.js 라이브러리 포함
const Chart = lazy(() => import('./components/Chart'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>차트 보기</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<Chart data={data} />
</Suspense>
)}
</div>
);
}
스플릿팅 기준
- 라우트(페이지) 단위 → 거의 항상 좋음
- 무거운 라이브러리 (차트, 에디터 등) → 좋음
- 조건부 렌더링되는 큰 컴포넌트 (모달 등) → 상황에 따라
- 작은 공통 컴포넌트 → 하지 않는 게 좋음
요약
| 개념 | 설명 |
|---|---|
| 코드 스플릿팅 | 번들을 여러 청크로 나눠서 필요할 때 로드 |
| 동적 import | import() 함수로 런타임에 모듈 로드 |
| React.lazy | 동적 import를 React 컴포넌트로 감싸기 |
| Suspense | lazy 컴포넌트 로딩 중 fallback UI 표시 |
| 라우트 기반 | 페이지 단위로 스플릿팅 (가장 효과적) |
코드 스플릿팅은 "지금 필요한 것만 로드한다"는 원칙을 코드에 적용하는 것이다.