본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
12장 : 성능 최적화

코드 분할 및 지연 로딩

라우트 단위 코드 분할과 next/dynamic 지연 로딩으로 초기 JavaScript를 줄이고 무거운 UI를 필요할 때 불러옵니다.

웹 애플리케이션의 성능 최적화에 있어 이미지와 폰트 외에도, 페이지 로딩 시 전송되는 JavaScript 코드의 양을 줄이는 것이 매우 중요합니다.

애플리케이션의 규모가 커질수록 JavaScript 번들의 크기는 기하급수적으로 증가할 수 있으며, 이는 초기 로딩 시간을 지연시키고 사용자 경험을 저해하는 주된 원인이 됩니다.

코드 분할(Code Splitting)지연 로딩(Lazy Loading)은 이러한 문제를 해결하기 위한 핵심적인 기술입니다.

Next.js는 이러한 기법들을 자동으로 적용하거나, 개발자가 직접 제어할 수 있는 API를 제공합니다.

이 절에서는 코드 분할과 지연 로딩의 개념, Next.js App Router에서 이를 구현하는 방법, 그리고 관련 최적화 기법들을 상세히 알아보겠습니다.


코드 분할(Code Splitting)이란?

코드 분할은 애플리케이션의 전체 JavaScript 코드를 하나의 큰 번들 파일로 만드는 대신, 여러 개의 작은 파일(청크, chunks)로 나누는 기법입니다.

이렇게 분할된 코드 청크는 필요할 때만 로드되므로, 초기 페이지 로딩 시 브라우저가 다운로드하고 파싱해야 하는 JavaScript의 양을 줄일 수 있습니다.

코드 분할의 이점
  • 초기 로딩 시간 단축: 사용자가 처음 방문하는 페이지에 필요한 코드만 다운로드되므로, 페이지가 더 빠르게 표시됩니다.
  • 리소스 효율성: 사용자가 아직 방문하지 않은 페이지의 코드는 미리 다운로드되지 않으므로, 네트워크 대역폭과 브라우저 리소스를 절약합니다.
  • 캐싱 효율성: 코드 변경 시 전체 번들을 다시 다운로드할 필요 없이 변경된 청크만 다시 다운로드하면 되므로, 브라우저 캐싱 효율이 높아집니다.

Next.js는 파일 시스템 기반 라우팅을 사용하므로, 각 페이지나 라우트 그룹은 기본적으로 별도의 코드 청크로 자동 분할됩니다. (예: app/dashboard/page.tsxapp/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
// 이 컴포넌트는 클라이언트 컴포넌트여야 합니다.
"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>
  );
}
클라이언트 경계에서 MapComponent 지연 로드 (src/app/locations/page.tsx)
src/app/locations/page.tsx
"use client";

import dynamic from 'next/dynamic';

// ssr: false는 Client Component에서만 선언할 수 있습니다.
// 지도처럼 브라우저 API에 의존하는 라이브러리를 클라이언트에서만 렌더링합니다.
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>

      <DynamicMapComponent latitude={37.5665} longitude={126.9780} zoom={10} />

      <p style={{ marginTop: '30px', fontSize: '0.9em', color: '#555' }}>
        (지도 코드는 메인 번들에서 분리되며, 초기 클라이언트 렌더링 과정에서 별도 청크로 비동기 요청됩니다.)
      </p>
    </div>
  );
}
dynamic 함수의 주요 옵션
  • ssr: false: 이 컴포넌트가 서버에서 렌더링되지 않고 클라이언트에서만 렌더링되도록 지정합니다. 이 옵션은 Client Component 안에서만 선언할 수 있습니다.
  • loading: 컴포넌트가 로드되는 동안 표시될 React 컴포넌트나 JSX를 정의합니다.

React 자체의 지연 로딩을 선택한다면 React.lazy로 컴포넌트를 불러오고 Suspensefallback으로 로딩 UI를 제공합니다.

무엇을 분할할지는 “첫 화면에 꼭 필요한가”, “무거운가”, “사용자 행동 뒤에 필요한가”를 순서대로 확인하면 판단하기 쉽습니다.

Server Components에서 코드 분할

Next.js App Router의 Server Components는 기본적으로 자동으로 코드 분할됩니다.

각 Server Component는 서버에서 렌더링되며, 필요한 경우에만 클라이언트 컴포넌트(Interactive Part)와 함께 최소한의 JavaScript를 클라이언트로 전송합니다.

개발자가 별도로 설정할 필요 없이 Next.js가 최적의 번들 크기를 위해 동작합니다.

라우트 그룹으로 레이아웃과 폴더 구조 나누기

Next.js App Router의 라우트 그룹(Route Groups)은 URL 경로에 영향을 주지 않으면서 라우트를 논리적으로 그룹화하는 기능입니다.

라우트별 코드 분할은 괄호 폴더의 유무와 관계없이 Next.js가 자동으로 수행합니다.

Route Group은 URL을 바꾸지 않고 폴더를 정리하거나 서로 다른 레이아웃을 적용할 때 사용합니다.

예를 들어 관리자 화면과 일반 사용자 화면의 내비게이션과 권한 경계가 다르다면 다음처럼 구조를 나눌 수 있습니다.

page.tsx
page.tsx
page.tsx
page.tsx
layout.tsx # 관리자 레이아웃 (관리자 관련 코드만 포함)
page.tsx
layout.tsx # 전역 레이아웃

각 페이지에 필요한 Client Component JavaScript만 전송되는 것은 자동 라우트 분할의 결과이며 Route Group이 별도 청크를 강제해서가 아닙니다.

그룹마다 서로 다른 root layout을 두면 두 root layout 사이 이동은 전체 페이지 로드가 될 수 있으므로, 단순한 번들 최적화 수단으로 그룹을 늘리지 않습니다.

Server Actions 및 Route Handlers에서

Server Actions와 Route Handlers도 기본적으로 코드 분할되어 필요한 경우에만 로드됩니다.

이는 특히 Server Actions를 사용하면 클라이언트 측 JavaScript를 거의 보내지 않고도 서버에서 작업을 수행할 수 있다는 큰 이점을 제공합니다.

아래 보드는 App Router에서 자동 분할에 맡길지, next/dynamic으로 지연 로딩할지, 서버 경계로 옮길지 판단하는 절차를 정리한 것입니다.


코드 분할 및 지연 로딩 최적화 팁

아래 다이어그램은 코드가 클라이언트 번들로 새어 들어가는 대표 경로와 줄이는 방법을 정리합니다.

다음 다이어그램은 코드 분할과 지연 로딩을 라우트, 컴포넌트, 서버 경계, 상호작용 비용으로 나누어 적용 위치를 정리합니다.

아래 다이어그램은 next/dynamic, 서버 컴포넌트, 라우트 그룹이 번들 경계를 나누는 방식을 보여줍니다.

  • 컴포넌트 단위로 생각하기: 큰 컴포넌트나, 특정 상호작용 후에만 필요한 컴포넌트는 next/dynamic을 사용하여 지연 로드하는 것을 고려합니다.
  • 서드파티 라이브러리 분석: bundle-analyzer와 같은 도구를 사용하여 번들 크기를 분석하고, 어떤 서드파티 라이브러리가 가장 많은 공간을 차지하는지 확인합니다. 불필요한 라이브러리를 제거하거나, 필요한 부분만 임포트하는 방법을 찾습니다.
  • Lighthouse 감사: Google Lighthouse와 같은 성능 감사 도구를 정기적으로 실행하여 Reduce unused JavaScript와 같은 제안을 확인하고 개선합니다.
  • use client 경계 최소화: App Router에서 use client 지시어는 그 파일과 그 안에서 임포트되는 모든 모듈을 클라이언트 번들에 포함시킵니다. 따라서 use client의 사용 범위를 최소화하고, 가능한 한 Server Components 내에서 렌더링되도록 노력합니다.
  • 이미지 및 폰트 최적화와 결합: 코드 분할 및 지연 로딩은 이미지, 폰트 최적화와 함께 웹 성능을 극대화하는 시너지 효과를 냅니다.

코드 분할과 지연 로딩은 초기 로딩 시간을 줄이고 전반적인 웹 애플리케이션의 성능을 향상시키는 데 필수적인 전략입니다.

Next.js는 이러한 복잡한 최적화 작업을 추상화하여 개발자가 더욱 쉽고 효율적으로 고성능 애플리케이션을 구축할 수 있도록 돕습니다.

아래 다이어그램은 코드 분할 및 지연 로딩을 렌더링 비용, 전송 비용, 측정 지표 순서로 좁혀 봅니다.