웹 애플리케이션 성능 최적화를 위해, React에서 Webpack을 사용하여 JavaScript 번들을 여러 개의 작은 청크(단위)로 나누고, 필요할 때만 동적으로 로드하는 것을 코드 스플리팅(Code Splitting)이라고 한다.
기존 방식은 모든 JavaScript 코드를 하나의 거대한 번들 파일로 묶어서 제공하기 때문에, 사용자가 특정 페이지에 접속하면 당장 필요하지 않은 코드까지 전부 다운로드해야 한다.
이로 인해 초기 로딩 시간이 길어지고, 특히 앱의 규모가 커질수록 번들 크기가 비대해져 성능 저하가 심화되는 문제가 있다.
코드 스플리팅을 적용하면, 라우트(페이지)나 컴포넌트 단위로 코드를 분리해 필요한 시점에만 불러올 수 있다.
그 결과 초기 로딩 시 다운로드해야 할 코드량이 줄어들어 First Contentful Paint(FCP), Time to Interactive(TTI) 등 핵심 성능 지표가 개선되고, 사용자가 실제로 방문하지 않는 페이지의 코드는 아예 로드되지 않아 불필요한 리소스 낭비도 막을 수 있다.

간단한 캘린더 애플리케이션이 MiniCalendar 컴포넌트와 EventDetailPanel 컴포넌트로 구성되어 있다고 가정하자.
| 컴포넌트 | 특징 |
|---|---|
| MiniCalendar | 날짜 선택, 월 이동 등 UI 기능만 수행하는 가벼운 컴포넌트 코드량이 적어 초기 번들에 포함되어도 부담이 없음 |
| EventDetailPanel | 선택한 날짜의 일정 상세를 보여주는 컴포넌트 차트·에디터 등 외부 라이브러리를 포함해 코드량이 크고, 날짜를 클릭하기 전에는 필요하지 않음 |
기존 방식은 두 컴포넌트를 파일 상단에서 한 번에 불러온다.
import MiniCalendar from './MiniCalendar'; // 최상단 import
import EventDetailPanel from './EventDetailPanel'; // 최상단 import
function App() {
return (
<div>
<MiniCalendar />
<EventDetailPanel />
</div>
);
}
이렇게 하면 EventDetailPanel의 코드가 초기 번들에 함께 포함된다. 그래서 달력만 보여도 되는 상황에서 EventDetailPanel의 코드까지 모두 내려받아야 하고, 그만큼 초기 로딩이 느려지게 된다.
이때 코드 스플리팅을 적용하면, EventDetailPanel의 코드를 애플리케이션 로드 시점에 내려받지 않고 필요한 시점에 불러오도록 바꿀 수 있다.
코드 스플리팅은 크게 두 단위로 적용할 수 있다.
페이지별로 번들을 쪼개서, 해당 경로에 진입할 때만 그 페이지의 코드를 불러오는 방식이다.
import { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import MiniCalendar from './MiniCalendar';
function App() {
const location = useLocation();
const [StatsDashboard, setStatsDashboard] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (location.pathname === '/stats') { // /stats 경로일 때 실행됨
setLoading(true);
import('./pages/StatsDashboard').then((module) => {
setStatsDashboard(() => module.default);
setLoading(false);
});
}
}, [location.pathname]);
if (location.pathname !== '/stats') return <MiniCalendar />;
if (loading || !StatsDashboard) return <div>페이지를 불러오는 중...</div>;
return <StatsDashboard />;
}
페이지 전체가 아니라, 특정 컴포넌트 하나만 필요한 시점에 불러오는 방식이다.
예를 들어, 날짜 클릭 시에만 필요한 EventDetailPanel처럼, 처음부터 보여줄 필요 없는 무거운 컴포넌트를 분리한다.
import { useState } from 'react';
import MiniCalendar from './MiniCalendar';
function App() {
const [selectedDay, setSelectedDay] = useState(null);
const [EventDetailPanel, setEventDetailPanel] = useState(null);
const [loading, setLoading] = useState(false);
const handleSelect = async (day) => {
setSelectedDay(day);
if (day) { // 날짜가 선택됐을 때 실행됨
setLoading(true);
const module = await import('./EventDetailPanel');
setEventDetailPanel(() => module.default);
setLoading(false);
}
};
return (
<div>
<MiniCalendar onSelect={handleSelect} />
{selectedDay && loading && <div>일정을 불러오는 중...</div>}
{selectedDay && !loading && EventDetailPanel && (
<EventDetailPanel day={selectedDay} />
)}
</div>
);
}
앞에서 살펴본 코드 스플리팅은 React가 제공하는 lazy와 Suspense를 쓰면 훨씬 간단하게 적용할 수 있다.
lazy()는 코드 스플리팅 시 사용하면 가독성 및 유지보수성 측면에서 좋은 함수이다.
import { lazy, useState } from 'react';
import MiniCalendar from './MiniCalendar';
const EventDetailPanel = lazy(() => import('./EventDetailPanel'));
function App() {
const [selectedDay, setSelectedDay] = useState(null);
return (
<div>
<MiniCalendar onSelect={setSelectedDay} />
{selectedDay && <EventDetailPanel day={selectedDay} />}
</div>
);
}
코드 작성이후 빌드과정에서 lazy()를 적용한 컴포넌트에는 각각의 청크 파일이 생성된다.
이 청크는 처음 렌더링되는 시점에 다운로드되고, 한 번 받은 뒤에는 추가 요청 없이 바로 사용된다.
다만 lazy()로 만든 컴포넌트는 코드가 아직 도착하지 않았을 때 "아직 준비되지 않았다"는 신호를 내부적으로 던지는throw 방식으로 동작한다. 그래서 위 코드는 Suspense 없이는 에러가 나며, 반드시 Suspense와 함께 써야 한다.
앞서 언급했듯 React.lazy는 코드가 아직 도착하지 않으면 Promise를 throw하는 방식으로 동작한다. throw는 실행을 멈추고 위로 거슬러 올라가며 받아 줄 곳을 찾는데, 아무도 받지 않으면 렌더링이 중단되고 에러가 난다. 그렇다면 이 Promise를 받아 줄 곳은 어디일까? 바로 Suspense다. React는 던져진 Promise를 잡아 가장 가까운 Suspense까지 올라가 fallback을 보여주고, Promise가 완료되면 다시 렌더링한다. throw를 catch가 받아 주는 구조와 비슷하다.
import { lazy, Suspense, useState } from 'react';
import MiniCalendar from './MiniCalendar';
const EventDetailPanel = lazy(() => import('./EventDetailPanel'));
function App() {
const [selectedDay, setSelectedDay] = useState(null);
return (
<div>
{/* 캘린더에서 날짜가 선택되면 */}
<MiniCalendar onSelect={setSelectedDay} />
{selectedDay && (
// 청크를 내려받는 동안 fallback UI를 보여주고, 완료되면 EventDetailPanel로 교체
<Suspense fallback={<div>일정을 불러오는 중...</div>}>
<EventDetailPanel day={selectedDay} />
</Suspense>
)}
</div>
);
}
코드 스플리팅의 개념과 방법을 간략히 알아보며, 초기 로딩 부담을 줄여 성능을 최적화하는 방법을 살펴보았다.
프론트엔드 작업은 물론, 프론트엔드 개발자와의 협업 및 다른 영역에 이를 응용할 때에도 코드 스플리팅에 대해 학습하며 얻은 경험이 도움 되기를 바란다.