코드스플리팅 (Code Splitting)
앱의 JS 번들을 여러 청크로 나누어 필요한 시점에 필요한 코드만 로드하는 기술
리액트와 같은 SPA 프로젝트는, 초기 진입시 앱 전체 Javascript 파일을 다운로드 받은 이후 화면을 렌더링하기 시작한다. 앱이 커질수록 번들의 크기도 커지며 초기 로딩 속도가 저하된다. 코드스플리팅이 적용되면 첫 화면 렌더링에 필요한 최소한의 코드만 먼저 다운로드 받을 수 있다. 나머지 코드는 사용자가 해당 기능을 사용할 때 로드된다.
빌드할 때 코드를 여러 chunk 파일로 쪼개두고 브라우저가 필요할 때 서버에서 필요한 파일만 받아오는 방식이다.
React에서의 코드스플리팅
Lazy - 컴포넌트 비동기 로드
번들링 도구인 Webpack이나 Vite가 코드스플리팅을 수행한다. React가 코드스플리팅을 트리거 하기 위해 React.lazy() 구문을 선언한다.
const MyComponent = lazy(() => import ('./MyComponent'));이제 빌드 툴은 MyComponent를 별도 청크로 분리한다. lazy를 통해 코드스플리팅은 되지만 컴포넌트가 로딩중임을 나타내는 상태 관리는 해주지 않는다. 이를 해결해주는 것이 바로 Suspense이다.
Suspense
lazy로 로드 중인 컴포넌트의 '로딩중 상태'를 보여주기 위한 React 컴포넌트
Lazy -> 코드 스플리팅 발생 -> Suspense -> 로딩상태 표시
(당연히) lazy와 세트로 사용된다.
사용 예시
import React, { Suspense, lazy } from 'react';
import Header from './Header';
const Chart = lazy(() => import('./Chart')); // Chart.js는 별토 청크로 분리될 것이다.
export default function App() {
return (
<div>
<Header />
<Suspense fallback={<div>차트 로딩 중...</div>}>
<Chart />
</Suspense>
</div>
);
}이로 인해 앱의 초기 로딩 속도가 개선되며, 로딩중 fallback 표시로 사용자 경험이 최적화될 수 있다.
Next.js에서는?
Next.js는 라우트 기반(페이지 단위) 코드스플리팅이 기본으로 내장되어 있다. Lazy + Suspense 를 별도로 해주지 않아도 대부분 자동으로 처리된다. 하지만 무거운 서드파티 라이브러리를 사용하는 등의 특정 상황에서는 Lazy를 추가로 활용할 수 있다.
// Header - 서버컴포넌트export default function Header() {
return <h1>대시보드</h1>;
}'use client'; // HeaveChart - 클라이언트 컴포넌트 (브라우저에서 실행)export default function HeavyChart() {
return <div>📊 차트 로드 완료!</div>;
}import Header from './components/Header';
import { Suspense, lazy } from 'react';
// 클라이언트 컴포넌트를 Lazy로 import (동적 import로 코드스플리팅 발생)const HeavyChart = lazy(() => import('./components/HeavyChart'));
export default function Page() {
return (
<div>
<Header />
{/* Lazy 컴포넌트 로딩 상태 표시 */}
<Suspense fallback={<div>차트 로딩 중...</div>}>
<HeavyChart />
</Suspense>
</div>
);
}Next.js는 페이지 라우터 단위 코드스플리팅까지는 기본 적용해준다. 위 코드는 컴포넌트 단위의 추가 코드스플리팅 제어 케이스이다.
아래와 같은 순서로 진행된다.
서버에서 Header 렌더링
lazy로 HeavyChart 코드스플리팅 발생
Suspense fallback으로 로딩 UI 표시
HeavyChart JS 다운로드 완료시 실제 컴포넌트 렌더링
dynamic()
하지만 Next.js에서는 위와 같은 방식보다 dynamic()을 활용하는 것이 더욱 추천된다.
import Header from './components/Header';
import dynamic from 'next/dynamic';
// dynamic import (자동 suspense 적용 가능)const HeavyChart = dynamic(() => import('./components/HeavyChart'), {
loading: () => <div>차트 로딩 중...</div>,
ssr: false, // 서버에서는 렌더하지 않음 (완전 클라이언트 전용)
});
export default function Page() {
return (
<div>
<Header />
<HeavyChart />
</div>
);
}Next.js의 서버컴포넌트에서도 코드스플리팅 적용이 가능!
// 서버컴포넌트async function getData() {
// 예시: DB 호출, API 호출 등 (느린 작업 가정)await new Promise((resolve) => setTimeout(resolve, 3000));
return '서버 데이터 불러오기 완료!';
}
export default async function SlowServerComponent() {
const data = await getData();
return <div>{data}</div>;
}'use client';
export default function ClientButton() {
const handleClick = () => {
alert('클라이언트에서 실행!');
};
return <button onClick={handleClick}>클릭</button>;
}import Header from './components/Header';
import ClientButton from './components/ClientButton';
import SlowServerComponent from './components/SlowServerComponent';
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Header />
<Suspense fallback={<div>서버 데이터 로딩 중...</div>}>
<SlowServerComponent />
</Suspense>
<ClientButton />
</div>
);
}Header 컴포넌트는 서버에서 바로 렌더됨
SlowServerComponent는 3초간 데이터를 기다리고, 그동안 이를 Suspense가 fallback으로 감싸서 '서버 데이터 로딩중...' 표시됨
브라우저에서는 Header + fallback이 먼저 표시됨
서버에서 데이터를 다 가져오면 SlowServerComponent가 화면에 붙음
ClientButton은 클라이언트 전용 코드로 hydrate 됨