블로그: 초기 로딩 1.5MB → 300KB — 코드 스플리팅 적용기
블로그: 초기 로딩 1.5MB → 300KB에 대해 "왜?"라고 물으면 답할 수 있으신가요?
\n\n> 블로그: 초기 로딩 1.5MB → 300KB에 대해 "왜?"라고 물으면 답할 수 있으신가요?\n기능을 하나둘 추가하다 보니까 빌드 결과가 점점 커졌습니다. 어느 순간 확인해보니 번들이 1.5MB 를 넘어가고 있었습니다.
홈페이지만 보러 온 사람한테 마크다운 파서(523KB), 게임 코드, 터미널 코드까지 전부 내려보내는 건 말이 안 됩니다. 코드 스플리팅을 적용하기로 했습니다.
코드 스플리팅이란
SPA는 기본적으로 모든 페이지 코드를 하나의 JS 파일 로 번들링합니다. 홈만 방문해도 About, Posts, PostDetail 등 모든 코드를 다운로드하는 거죠.
코드 스플리팅은 번들을 여러 조각(chunk)으로 나눠서, 사용자가 실제로 방문하는 페이지의 코드만 로드 하게 하는 기법입니다.
[스플리팅 전]
index.js (1.5MB) ← 전부 다 들어있음
[스플리팅 후]
index.js (300KB) ← 공통 코드
HomePage.js (5KB) ← 홈 방문 시만
PostsPage.js (6KB) ← 글 목록 방문 시만
PostDetailPage.js (10KB) ← 글 상세 방문 시만
vendor-markdown.js (523KB) ← 글 상세에서만 필요
React.lazy()
React는 lazy()로 동적 import를 지원합니다.
// 기존: 정적 import — 번들에 무조건 포함
import PostsPage from './pages/PostsPage.jsx';
// 변경: lazy — 해당 라우트 접근 시에만 로드
const PostsPage = lazy(() => import('./pages/PostsPage.jsx'));
lazy()는 컴포넌트가 실제로 렌더링될 때 비동기로 코드를 가져옵니다. Vite(Rollup)가 빌드 시 import()를 감지해서 자동으로 별도 chunk로 분리합니다.
적용
// App.jsx
const HomePage = lazy(() => import('./pages/HomePage.jsx'));
const PostsPage = lazy(() => import('./pages/PostsPage.jsx'));
const PostsResolver = lazy(() => import('./components/PostsResolver.jsx'));
const ProjectsPage = lazy(() => import('./pages/ProjectsPage.jsx'));
const AboutPage = lazy(() => import('./pages/AboutPage.jsx'));
const NotFoundPage = lazy(() => import('./pages/NotFoundPage.jsx'));
function App() {
return (
<ErrorBoundary>
<Suspense fallback={
<div style={{ minHeight: '100dvh', background: 'var(--bg)' }} aria-busy="true" />
}>
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<HomePage />} />
<Route path="/posts" element={<PostsPage />} />
<Route path="/posts/*" element={<PostsResolver />} />
<Route path="/projects" element={<ProjectsPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</Suspense>
</ErrorBoundary>
);
}
Suspense는 lazy 컴포넌트가 로드되는 동안 fallback UI 를 보여줍니다. 저는 배경색만 있는 빈 div를 넣었습니다. 복잡한 로딩 스피너 대신 배경만 깔아주면 화면 깜빡임이 줄어듭니다.
빌드 결과
스플리팅 적용 후 빌드 결과입니다.
dist/assets/HomePage-dSI37zOI.js 4.57 kB │ gzip: 1.19 kB
dist/assets/PostsPage-B3izJ7MB.js 6.44 kB │ gzip: 2.48 kB
dist/assets/AboutPage-BOwSsSJo.js 9.74 kB │ gzip: 2.34 kB
dist/assets/PostsResolver-D-Te57b0.js 10.21 kB │ gzip: 3.94 kB
dist/assets/vendor-react-D2kupECk.js 47.13 kB │ gzip: 16.74 kB
dist/assets/vendor-markdown-CLfPX4qs.js 523.02 kB │ gzip: 161.12 kB
dist/assets/index-BcJ88B4w.js 925.87 kB │ gzip: 283.02 kB
홈페이지 첫 방문 시 다운로드:
index.js— 283KB (gzip)vendor-react.js— 17KB (gzip)HomePage.js— 1.2KB (gzip)- ** 총 ~301KB**
마크다운 라이브러리(161KB gzip)를 안 받아도 됩니다. 글 상세 페이지에 들어갈 때만 추가 로드됩니다.
LoadingScreen: 부팅 애니메이션
코드 스플리팅을 적용하면 첫 방문 시 chunk 다운로드에 약간의 시간이 걸립니다. 이 시간 동안 빈 화면을 보여주는 대신, ** 터미널 부팅 애니메이션 **을 넣었습니다.
function App() {
const [showLoading, setShowLoading] = useState(true);
return (
<ErrorBoundary>
{showLoading && <LoadingScreen onDone={() => setShowLoading(false)} />}
<Suspense fallback={/* ... */}>
{/* Routes */}
</Suspense>
</ErrorBoundary>
);
}
[BOOT], [OK], [DONE] 같은 메시지가 순차적으로 표시되면서 마치 시스템이 부팅되는 것 같은 연출을 합니다. 실제 로딩 시간과 맞추면 자연스러운데, 워낙 빨라서 약간의 딜레이를 일부러 넣었습니다.
언제 lazy()를 써야 하나
모든 컴포넌트에 lazy()를 적용할 필요는 없습니다.
| 적용 O | 적용 X |
|---|---|
| 페이지 단위 컴포넌트 | Layout, Header (항상 렌더링) |
| 무거운 라이브러리 사용 컴포넌트 | 가벼운 공통 컴포넌트 |
| 조건부 모달, 다이얼로그 | 자주 쓰는 버튼, 입력 필드 |
Header나 Layout처럼 매번 렌더링되는 컴포넌트를 lazy로 감싸면 오히려 불필요한 비동기 오버헤드가 생깁니다. 페이지 단위, 또는 확실히 무거운 컴포넌트에만 적용하는 게 효과적입니다.
체감
1.5MB를 한 번에 내려받던 것에서 300KB로 줄이니까, 특히 모바일에서 체감 속도가 확실히 달라졌습니다. 3G 환경에서 테스트해보면 차이가 극명합니다.
코드 스플리팅의 핵심은 결국 "지금 당장 필요하지 않은 코드는 나중에" 입니다. React.lazy()가 이걸 아주 간단하게 해줍니다.