코드 분할 및 지연 로딩
웹 애플리케이션의 성능 최적화에 있어 이미지와 폰트 외에도, 페이지 로딩 시 전송되는 JavaScript 코드의 양을 줄이는 것이 매우 중요합니다. 애플리케이션의 규모가 커질수록 JavaScript 번들의 크기는 기하급수적으로 증가할 수 있으며, 이는 초기 로딩 시간을 지연시키고 사용자 경험을 저해하는 주된 원인이 됩니다.
코드 분할(Code Splitting) 과 지연 로딩(Lazy Loading) 은 이러한 문제를 해결하기 위한 핵심적인 기술입니다. Next.js는 이러한 기법들을 자동으로 적용하거나, 개발자가 직접 제어할 수 있도록 강력한 도구를 제공합니다.
이 절에서는 코드 분할과 지연 로딩의 개념, Next.js App Router에서 이를 구현하는 방법, 그리고 관련 최적화 기법들을 상세히 알아보겠습니다.
코드 분할(Code Splitting)이란?
코드 분할은 애플리케이션의 전체 JavaScript 코드를 하나의 큰 번들 파일로 만드는 대신, 여러 개의 작은 파일(청크, chunks)로 나누는 기법입니다. 이렇게 분할된 코드 청크는 필요할 때만 로드되므로, 초기 페이지 로딩 시 브라우저가 다운로드하고 파싱해야 하는 JavaScript의 양을 줄일 수 있습니다.
코드 분할의 이점
- 초기 로딩 시간 단축: 사용자가 처음 방문하는 페이지에 필요한 코드만 다운로드되므로, 페이지가 더 빠르게 표시됩니다.
- 리소스 효율성: 사용자가 아직 방문하지 않은 페이지의 코드는 미리 다운로드되지 않으므로, 네트워크 대역폭과 브라우저 리소스를 절약합니다.
- 캐싱 효율성: 코드 변경 시 전체 번들을 다시 다운로드할 필요 없이 변경된 청크만 다시 다운로드하면 되므로, 브라우저 캐싱 효율이 높아집니다.
Next.js는 파일 시스템 기반 라우팅을 사용하므로, 각 페이지나 라우트 그룹은 기본적으로 별도의 코드 청크로 자동 분할됩니다. (예: app/dashboard/page.tsx
는 app/settings/page.tsx
와 별도의 JS 파일로 분할됨)
지연 로딩(Lazy Loading)이란?
지연 로딩은 특정 컴포넌트나 모듈이 실제로 필요할 때(예: 사용자가 특정 섹션으로 스크롤하거나, 특정 버튼을 클릭했을 때) 비동기적으로 로드하는 기법입니다. 이는 코드 분할과 함께 사용하여 특정 페이지 내에서도 중요도가 낮은 컴포넌트의 로딩을 지연시켜 초기 로딩 성능을 더욱 최적화할 수 있습니다.
App Router에서 구현
Next.js App Router는 컴포넌트 및 라이브러리를 지연 로드하는 다양한 방법을 제공합니다.
next/dynamic
을 사용한 지연 로딩
Next.js에서 클라이언트 컴포넌트를 지연 로드하는 가장 기본적인 방법은 next/dynamic
유틸리티를 사용하는 것입니다. 이는 React의 React.lazy()
및 Suspense
와 유사하게 작동하지만, Next.js의 SSR(Server-Side Rendering) 환경에서 더 잘 통합됩니다.
실습: 다이내믹 임포트를 사용하여 지도 컴포넌트 지연 로딩
가정: 지도 라이브러리(react-leaflet
, google-maps-react
등)는 용량이 커서 초기 로딩 시 번들에 포함되는 것을 피하고 싶습니다.
-
지연 로드할 클라이언트 컴포넌트 생성 (
src/components/MapComponent.tsx
)src/components/MapComponent.tsx // src/components/MapComponent.tsx // 이 컴포넌트는 클라이언트 컴포넌트여야 합니다. "use client"; import { useEffect, useState } from 'react'; interface MapProps { latitude: number; longitude: number; zoom: number; } export default function MapComponent({ latitude, longitude, zoom }: MapProps) { const [mapLoaded, setMapLoaded] = useState(false); useEffect(() => { // 실제 지도 라이브러리 로딩 및 초기화 로직 // 여기서는 가상으로 2초 지연을 시뮬레이션합니다. const timer = setTimeout(() => { setMapLoaded(true); console.log('지도 컴포넌트가 로드되었습니다.'); }, 2000); // 지도 라이브러리 로딩 시간 시뮬레이션 return () => clearTimeout(timer); }, []); if (!mapLoaded) { return ( <div style={{ height: '300px', backgroundColor: '#e0e0e0', display: 'flex', justifyContent: 'center', alignItems: 'center', color: '#666', border: '1px dashed #ccc' }}> 지도 로딩 중... </div> ); } return ( <div style={{ height: '300px', backgroundColor: '#f0f8ff', border: '1px solid #007bff', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', color: '#007bff' }}> <h3>지도 표시 (위도: {latitude}, 경도: {longitude})</h3> <p>확대: {zoom}</p> <p>실제 지도 라이브러리가 여기에 렌더링됩니다.</p> </div> ); }
-
next/dynamic
을 사용하여MapComponent
지연 로드 (src/app/locations/page.tsx
)src/app/locations/page.tsx // src/app/locations/page.tsx import dynamic from 'next/dynamic'; // next/dynamic 임포트 import { Suspense } from 'react'; // 로딩 상태 처리를 위한 Suspense 임포트 // 1. `next/dynamic`을 사용하여 MapComponent를 동적으로 임포트 // { ssr: false } 옵션은 이 컴포넌트가 서버 사이드 렌더링되지 않음을 의미합니다. // 지도 라이브러리는 보통 브라우저 환경에서만 작동하므로 ssr: false가 적절합니다. const DynamicMapComponent = dynamic(() => import('@/components/MapComponent'), { ssr: false, // 이 컴포넌트는 클라이언트에서만 렌더링됩니다. loading: () => <p style={{ textAlign: 'center', padding: '20px', border: '1px dashed #ccc' }}>지도를 불러오는 중입니다...</p>, // 로딩 중 표시될 컴포넌트 }); export default function LocationsPage() { return ( <div style={{ padding: '20px', maxWidth: '800px', margin: '20px auto', border: '1px solid #28a745', borderRadius: '10px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)', textAlign: 'center' }}> <h1 style={{ color: '#28a745', marginBottom: '20px' }}>위치 정보 페이지</h1> <p style={{ marginBottom: '30px' }}>아래는 지연 로딩된 지도 컴포넌트입니다.</p> {/* 2. <Suspense>로 감싸서 fallback UI 제공 */} {/* loading 옵션이 DynamicMapComponent에 있어 Suspense는 선택적입니다. */} {/* 하지만 여러 지연 로딩 컴포넌트를 함께 묶거나, 더 복잡한 로딩 UI를 제공할 때 유용합니다. */} <Suspense fallback={<p style={{ textAlign: 'center', padding: '20px', border: '1px dashed #ccc' }}>지도 영역 로딩 중...</p>}> <DynamicMapComponent latitude={37.5665} longitude={126.9780} zoom={10} /> </Suspense> <p style={{ marginTop: '30px', fontSize: '0.9em', color: '#555' }}> (지도 컴포넌트는 페이지 로딩 시점에 즉시 다운로드되지 않고, 클라이언트에서 렌더링될 때 비동기적으로 로드됩니다.) </p> </div> ); }
dynamic
함수의 주요 옵션
ssr: false
: 이 컴포넌트가 서버 사이드 렌더링되지 않고, 오직 클라이언트에서만 렌더링되도록 지정합니다. 브라우저 API(예:window
,document
)에 의존하는 라이브러리에 필수적입니다.loading
: 컴포넌트가 로드되는 동안 표시될 React 컴포넌트나 JSX를 정의합니다.suspense: true
: (Next.js 14부터 권장)loading
옵션 대신 React의Suspense
를 사용하여 로딩 UI를 제공할 때 사용합니다.
Server Components에서 코드 분할
Next.js App Router의 Server Components는 기본적으로 자동으로 코드 분할됩니다. 각 Server Component는 서버에서 렌더링되며, 필요한 경우에만 클라이언트 컴포넌트(Interactive Part)와 함께 최소한의 JavaScript를 클라이언트로 전송합니다. 개발자가 별도로 설정할 필요 없이 Next.js가 최적의 번들 크기를 위해 노력합니다.
라우트 그룹을 사용한 코드 분할
Next.js App Router의 라우트 그룹(Route Groups) 은 URL 경로에 영향을 주지 않으면서 라우트를 논리적으로 그룹화하는 기능입니다. 이를 사용하여 특정 라우트 그룹에만 필요한 리소스를 묶어 별도의 청크로 분할하고, 나머지 애플리케이션의 번들 크기를 줄일 수 있습니다.
예를 들어, 관리자 대시보드와 사용자 대시보드가 완전히 다른 기능과 UI를 가진다면, 다음과 같이 라우트 그룹으로 분리하여 각 그룹의 코드를 별도로 번들링할 수 있습니다.
app/
├── (main)/ # 사용자 관련 페이지 (e.g., /dashboard, /profile)
│ ├── dashboard/
│ │ └── page.tsx
│ └── profile/
│ └── page.tsx
├── (admin)/ # 관리자 관련 페이지 (e.g., /admin, /admin/users)
│ ├── admin/
│ │ ├── page.tsx
│ │ └── users/
│ │ └── page.tsx
│ └── layout.tsx # 관리자 레이아웃 (관리자 관련 코드만 포함)
├── page.tsx
└── layout.tsx # 전역 레이아웃
이렇게 하면 (main)
그룹의 페이지를 로딩할 때 (admin)
그룹의 JavaScript 코드가 다운로드되지 않고, 그 반대도 마찬가지입니다. 이는 페이지 라우터의 pages/admin
폴더와 유사하게 작동하여 코드 분할을 촉진합니다.
Server Actions 및 Route Handlers에서
Server Actions와 Route Handlers도 기본적으로 코드 분할되어 필요한 경우에만 로드됩니다. 이는 특히 Server Actions를 사용하면 클라이언트 측 JavaScript를 거의 보내지 않고도 서버에서 작업을 수행할 수 있다는 큰 이점을 제공합니다.
코드 분할 및 지연 로딩 최적화 팁
- 컴포넌트 단위로 생각하기: 큰 컴포넌트나, 특정 상호작용 후에만 필요한 컴포넌트는
next/dynamic
을 사용하여 지연 로드하는 것을 고려합니다. - 서드파티 라이브러리 분석:
bundle-analyzer
와 같은 도구를 사용하여 번들 크기를 분석하고, 어떤 서드파티 라이브러리가 가장 많은 공간을 차지하는지 확인합니다. 불필요한 라이브러리를 제거하거나, 필요한 부분만 임포트하는 방법을 찾습니다. - Lighthouse 감사: Google Lighthouse와 같은 성능 감사 도구를 정기적으로 실행하여 "Reduce unused JavaScript"와 같은 제안을 확인하고 개선합니다.
use client
경계 최소화: App Router에서use client
지시어는 그 파일과 그 안에서 임포트되는 모든 모듈을 클라이언트 번들에 포함시킵니다. 따라서use client
의 사용 범위를 최소화하고, 가능한 한 Server Components 내에서 렌더링되도록 노력합니다.- 이미지 및 폰트 최적화와 결합: 코드 분할 및 지연 로딩은 이미지, 폰트 최적화와 함께 웹 성능을 극대화하는 시너지 효과를 냅니다.
코드 분할과 지연 로딩은 초기 로딩 시간을 줄이고 전반적인 웹 애플리케이션의 성능을 향상시키는 데 필수적인 전략입니다. Next.js는 이러한 복잡한 최적화 작업을 추상화하여 개발자가 더욱 쉽고 효율적으로 고성능 애플리케이션을 구축할 수 있도록 돕습니다.