성능 최적화 16편 - React.lazy와 코드 스플리팅으로 초기 로딩 속도 개선하기
15편에서는 이미 화면에 그려진 컴포넌트가 불필요하게 다시 그려지는 문제를 다뤘습니다. 이번 글에서는 그보다 앞선 단계, 애초에 페이지가 처음 열릴 때 얼마나 많은 코드를 한 번에 내려받는가의 문제를 다룹니다. 이걸 해결하는 방법이 코드 스플리팅(Code Splitting)입니다.
1. 번들이 하나로 뭉쳐 있을 때의 문제
React 앱을 별도 설정 없이 빌드하면, 기본적으로 모든 페이지의 코드가 하나의 JS 번들 파일로 합쳐집니다.
main.bundle.js (2.8MB)
├─ 로그인 페이지 코드
├─ 상품 목록 페이지 코드
├─ 상품 상세 페이지 코드
├─ 마이페이지 코드
├─ 관리자 대시보드 코드 (일반 사용자는 거의 안 씀)
└─ 결제 페이지 코드
사용자가 로그인 페이지만 보러 왔는데도, 브라우저는 관리자 대시보드나 결제 페이지 코드까지 전부 다운로드하고 파싱해야 합니다. 번들이 커질수록 첫 화면이 뜨기까지의 시간(초기 로딩 속도)이 느려집니다.
2. dynamic import로 코드 나누기
ES 모듈의 import() 문법을 사용하면, 특정 코드를 필요한 시점에만 별도 파일로 불러오도록 나눌 수 있습니다.
// 기존 방식 - 항상 함께 번들링됨
import AdminDashboard from './pages/AdminDashboard';
// dynamic import - 별도 파일로 분리되어 필요할 때만 로드
const loadAdminDashboard = () => import('./pages/AdminDashboard');
빌드 도구(Webpack, Vite 등)는 import() 구문을 만나면 해당 모듈을 별도의 청크(chunk) 파일로 분리합니다.
main.bundle.js (450KB) <- 초기 로딩에 필요한 최소 코드
admin-dashboard.chunk.js (600KB) <- 관리자 페이지 방문 시에만 로드
payment.chunk.js (300KB) <- 결제 페이지 방문 시에만 로드
3. React.lazy로 컴포넌트 단위 분리하기
React에서는 React.lazy로 컴포넌트를 감싸면, 라우팅과 자연스럽게 연결해서 코드 스플리팅을 적용할 수 있습니다.
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const ProductList = lazy(() => import('./pages/ProductList'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/products" element={<ProductList />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/admin" element={<AdminDashboard />} />
</Routes>
</Suspense>
);
}
lazy(() => import(...))로 감싼 컴포넌트는 실제로 해당 경로에 접근할 때 비로소 코드가 다운로드됩니다. Suspense는 코드를 불러오는 동안 보여줄 로딩 화면(fallback)을 지정합니다.
4. 페이지 단위가 아닌 컴포넌트 단위 분리
라우트 전체가 아니라, 페이지 안의 특정 무거운 컴포넌트만 분리할 수도 있습니다. 예를 들어 상품 상세 페이지에 리뷰 작성용 이미지 에디터처럼 무거운 라이브러리가 포함된 컴포넌트가 있다면, 그 컴포넌트만 지연 로딩할 수 있습니다.
const ReviewImageEditor = lazy(() => import('./components/ReviewImageEditor'));
function ProductDetailPage() {
const [showEditor, setShowEditor] = useState(false);
return (
<div>
<ProductInfo />
<button onClick={() => setShowEditor(true)}>리뷰 작성하기</button>
{showEditor && (
<Suspense fallback={<LoadingSpinner />}>
<ReviewImageEditor />
</Suspense>
)}
</div>
);
}
이렇게 하면 상품 상세 페이지에 처음 진입할 때는 이미지 에디터 코드를 아예 내려받지 않고, 사용자가 "리뷰 작성하기" 버튼을 눌렀을 때 비로소 로드됩니다.
5. 빌드 결과로 효과 확인하기
빌드 시 각 청크의 크기를 확인하면 코드 스플리팅이 실제로 잘 적용되었는지 검증할 수 있습니다.
File sizes after gzip:
145.2 KB build/static/js/main.[hash].js
89.6 KB build/static/js/admin-dashboard.[hash].chunk.js
52.3 KB build/static/js/payment.[hash].chunk.js
31.7 KB build/static/js/review-image-editor.[hash].chunk.js
초기 로딩에 필요한 main.js가 145KB 수준으로 줄어들고, 나머지는 필요한 시점에만 개별적으로 로드되는 것을 확인할 수 있습니다.
6. 네트워크 탭으로 실제 효과 확인하기
브라우저 개발자 도구의 Network 탭에서, 페이지 진입 시 로드되는 JS 파일 목록을 비교하면 효과를 직접 확인할 수 있습니다.
[코드 스플리팅 전]
main.bundle.js 2.8MB 로딩 시간 1,850ms
[코드 스플리팅 후 - 상품 목록 페이지 진입 시]
main.js 145KB 로딩 시간 180ms
product-list.chunk.js 95KB 로딩 시간 110ms
초기 진입 시 다운로드해야 하는 용량이 크게 줄면서, 첫 화면이 뜨는 속도(FCP, First Contentful Paint)가 개선됩니다.
7. 코드 스플리팅 시 주의할 점
- 너무 잘게 쪼개면 오히려 손해입니다. 청크가 지나치게 많아지면, 네트워크 요청 자체가 늘어나 오버헤드가 커질 수 있습니다. 페이지 단위, 또는 확실히 무거운 컴포넌트 단위로 나누는 것이 일반적입니다.
- Suspense의 fallback UI도 신경 써야 합니다. 로딩 중 빈 화면이 오래 보이면 사용자 체감은 오히려 나빠질 수 있어, 스켈레톤 UI 등을 활용하는 것이 좋습니다.
- 자주 이동하는 경로는 prefetch를 고려합니다. 예를 들어 상품 목록에서 상세 페이지로 이동할 확률이 높다면, 목록 페이지가 로드된 후 유휴 시간에 상세 페이지 청크를 미리 받아두는 방식(
import()를 마우스 hover 시점에 미리 호출)도 함께 쓸 수 있습니다.
8. 정리
- 기본 빌드는 모든 페이지 코드를 하나의 번들로 합치기 때문에, 페이지가 많아질수록 초기 로딩이 느려진다
React.lazy+Suspense로 라우트 단위, 또는 무거운 컴포넌트 단위로 코드를 나눠 필요한 시점에만 로드할 수 있다- 빌드 결과와 네트워크 탭으로 청크가 의도대로 분리되었는지, 실제로 초기 로딩이 개선되었는지 확인해야 한다
- 너무 잘게 쪼개는 것도 오버헤드가 될 수 있으므로, 페이지나 확실히 무거운 단위로 적절히 나누는 것이 중요하다
1편에서 다룬 N+1 문제가 "필요 이상으로 많은 쿼리를 보내는 것"이 문제였다면, 코드 스플리팅이 없는 상태는 "필요 이상으로 많은 코드를 미리 받아두는 것"이 문제입니다. 두 경우 모두 "지금 당장 필요한 만큼만 가져온다"는 같은 원칙으로 해결됩니다.
'성능 최적화' 카테고리의 다른 글
| 성능 최적화 STEP 18 - 번들 사이즈 분석과 축소 (0) | 2026.07.14 |
|---|---|
| 성능 최적화 STEP 17 - 이미지 최적화 (0) | 2026.07.14 |
| 성능 최적화 STEP 15 - 불필요한 리렌더링 줄이기 (0) | 2026.07.13 |
| 성능 최적화 STEP 14 - 캐시 스탬피드 방지하기 (0) | 2026.07.10 |
| 성능 최적화 STEP 13 - JVM GC 튜닝 (0) | 2026.07.10 |