junyeokk
Blog
React·2026. 01. 27

코드 스플릿팅 (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 (기존)

javascript
[코드 스플릿팅 없이]
bundle.js (2MB) ─────────────────────────────────▶ 전부 다운로드
  ├── 홈페이지 코드
  ├── 대시보드 코드
  ├── 설정 페이지 코드
  ├── 차트 라이브러리
  └── 에디터 라이브러리

사용자가 홈페이지만 보더라도 2MB 전체를 다운로드해야 함

정적 import는 빌드 시점에 모듈을 번들에 포함시킨다. 해당 컴포넌트를 사용하지 않아도 번들에 들어간다.

동적 import

javascript
[코드 스플릿팅 적용]
main.js (200KB) ──────────────────────────────────▶ 초기 로드
  └── 홈페이지 코드, 공통 코드

dashboard.js (500KB) ─────────────────────────────▶ 대시보드 진입 시 로드
settings.js (100KB) ──────────────────────────────▶ 설정 진입 시 로드
chart-vendor.js (800KB) ──────────────────────────▶ 차트 사용 시 로드

홈페이지만 보면 200KB만 다운로드

동적 import는 Promise를 반환한다. 해당 코드가 실행될 때 네트워크 요청으로 모듈을 가져온다.


번들러의 역할

Webpack, Vite 같은 번들러는 동적 import를 만나면 자동으로 코드를 분리한다.

javascript
import { Chart } from 'chart-library'; // 번들에 무조건 포함

function Dashboard() {
  return <Chart />;
}

번들러가 하는 일:

  1. 동적 import 구문 감지
  2. 해당 모듈과 의존성을 별도 청크로 분리
  3. 런타임에 청크를 로드하는 코드 생성

React에서의 코드 스플릿팅

React는 React.lazySuspense로 컴포넌트 단위 코드 스플릿팅을 지원한다.

React.lazy

jsx
// 함수 호출 시점에 모듈을 로드
const chartModule = await import('chart-library');
const Chart = chartModule.Chart;

lazy()는 동적 import를 반환하는 함수를 받아서, 컴포넌트가 렌더링될 때 해당 모듈을 로드한다.

Suspense

Suspense는 lazy 컴포넌트가 로드되는 동안 fallback UI를 보여준다. lazy 컴포넌트는 반드시 Suspense로 감싸야 한다.

jsx
// 이 코드를 번들러가 분석하면
const Page = await import('./pages/Dashboard');

// 빌드 결과물
// - main.js (이 import 문을 포함)
// - Dashboard-abc123.js (분리된 청크)

라우트 기반 스플릿팅

가장 효과적인 스플릿팅 지점은 **라우트(페이지)**다. 사용자가 특정 페이지에 진입할 때만 해당 페이지 코드를 로드한다.

jsx
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>
  );
}

왜 라우트 단위가 효과적인가

  1. 자연스러운 분리 지점: 페이지 전환은 사용자가 "기다릴 준비가 된" 시점
  2. 큰 청크 분리: 페이지는 보통 여러 컴포넌트를 포함해서 분리 효과가 큼
  3. 예측 가능한 로딩: 어떤 페이지가 언제 로드될지 명확함

컴포넌트 단위 스플릿팅

무거운 라이브러리를 사용하는 컴포넌트도 분리할 수 있다.

jsx
<Suspense fallback={<div>로딩 중...</div>}>
  <Dashboard />
</Suspense>

차트를 보기 전까지는 chart.js 라이브러리를 로드하지 않는다.


빌드 결과 확인

Vite에서는 rollup-plugin-visualizer로 번들 구성을 시각화할 수 있다.

javascript
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 요청 오버헤드가 있기 때문이다.

jsx
// 차트 컴포넌트 - 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>
  );
}

스플릿팅 기준

  • 라우트(페이지) 단위 → 거의 항상 좋음
  • 무거운 라이브러리 (차트, 에디터 등) → 좋음
  • 조건부 렌더링되는 큰 컴포넌트 (모달 등) → 상황에 따라
  • 작은 공통 컴포넌트 → 하지 않는 게 좋음

요약

개념설명
코드 스플릿팅번들을 여러 청크로 나눠서 필요할 때 로드
동적 importimport() 함수로 런타임에 모듈 로드
React.lazy동적 import를 React 컴포넌트로 감싸기
Suspenselazy 컴포넌트 로딩 중 fallback UI 표시
라우트 기반페이지 단위로 스플릿팅 (가장 효과적)

코드 스플릿팅은 "지금 필요한 것만 로드한다"는 원칙을 코드에 적용하는 것이다.