Frontend/React

코드 스플리팅(Code Splitting)과 React.lazy로 초기 로딩 속도 높이기

미니임 2026. 3. 1. 22:40

 

현대 웹 애플리케이션은 기능이 풍부해지는 만큼 번들 사이즈도 기하급수적으로 커지고 있습니다. 사용자가 서비스에 접속했을 때, 당장 필요하지 않은 기능까지 포함된 거대한 JavaScript 파일을 다운로드하느라 빈 화면(Blank Screen)을 마주하게 된다면 어떨까요? 통계적으로 초기 로딩이 3초 이상 걸릴 경우 사용자의 50% 이상이 이탈합니다.

이러한 병목 현상을 해결하고 **첫 번째 의미 있는 페인트(First Contentful Paint)**를 앞당기기 위한 핵심 전략이 바로 **코드 스플리팅(Code Splitting)**입니다.


1. Deep Dive: 코드 스플리팅의 작동 원리와 철학

전통적인 방식에서는 모든 컴포넌트와 라이브러리를 하나의 main.js 파일로 묶어 제공합니다. 하지만 코드 스플리팅은 이 거대한 덩어리를 논리적인 단위로 쪼개어, 사용자가 요청하는 시점에 필요한 조각(Chunk)만 전송하는 기법입니다.

🧩 비유로 이해하기: 도서관의 서가 시스템

모든 책을 한꺼번에 가방에 넣고 다니는 학생을 상상해 보세요. 가방은 너무 무거워 이동 속도가 현저히 느려질 것입니다. 코드 스플리팅은 이를 도서관 시스템으로 바꾸는 작업입니다. 학생은 도서관(서버)에 가서 지금 당장 공부할 '수학 책(특정 페이지)'만 빌려옵니다. 국어 책은 국어 시간이 되었을 때 비로소 서가에서 꺼내오면 되니까요. 가방은 가벼워지고 이동은 빨라집니다.

핵심 기술: Dynamic Import

React에서 코드 스플리팅이 가능한 이유는 JavaScript의 dynamic import() 문법 덕분입니다. 정적 임포트(import ... from ...)는 빌드 시점에 모든 코드를 결합하지만, 동적 임포트는 프라미스(Promise)를 반환하며 런타임에 해당 모듈을 로드합니다.


2. Hands-on: 실전 이커머스 대시보드 구현

단순한 예제가 아닌, 복잡한 차트 라이브러리를 사용하는 이커머스 관리자 통계 페이지를 가정해 봅시다. 통계 차트는 무겁기 때문에 사용자가 '통계 탭'을 클릭했을 때만 로드하는 것이 효율적입니다.

단계별 코드 구현

JavaScript
 
import React, { Suspense, lazy, useState } from 'react';

// 1. Heavy한 컴포넌트를 lazy하게 로드합니다.
// 분석 라이브러리(recharts 등)가 포함된 컴포넌트라고 가정합니다.
const StatisticsPanel = lazy(() => import('./components/StatisticsPanel'));

const AdminDashboard = () => {
  const [showStats, setShowStats] = useState(false);

  return (
    <div className="dashboard-container">
      <header>
        <h1>이커머스 관리 콘솔</h1>
        <button 
          onClick={() => setShowStats(true)}
          className="nav-button"
        >
          매출 통계 보기
        </button>
      </header>

      <main>
        <section className="summary-cards">
          <div>오늘의 주문: 120건</div>
          <div>결제 완료: 115건</div>
        </section>

        {/* 2. Suspense를 통해 로딩 중 UI를 정의합니다. */}
        {showStats && (
          <Suspense fallback={<div className="loader">차트 데이터를 불러오는 중...</div>}>
            {/* 3. 실제로 호출되는 시점에 StatisticsPanel.js 청크 파일이 로드됩니다. */}
            <StatisticsPanel />
          </Suspense>
        )}
      </main>
    </div>
  );
};

export default AdminDashboard;

핵심 로직 상세 주석

  • lazy(() => import(...)): 빌드 도구(Webpack/Vite)에게 이 지점에서 번들을 분리하라고 명령합니다. 결과물로 별도의 .js 파일이 생성됩니다.
  • Suspense: 지연 로딩된 컴포넌트가 준비될 때까지 렌더링을 유예하고, fallback 프롭에 전달된 UI를 대신 보여줍니다. 이는 사용자에게 서비스가 중단된 것이 아니라 '준비 중'이라는 시각적 피드백을 제공합니다.

3. Troubleshooting: 발생 가능한 이슈와 해결책

⚠️ ChunkLoadError (네트워크 오류)

사용자가 앱을 켜둔 상태에서 새로운 버전이 배포되어 서버의 청크 파일 해시값이 변경되면, 이전 해시를 참조하던 사용자의 브라우저에서 에러가 발생합니다.

  • Tip: ErrorBoundary를 Suspense 상위에 배치하여, 로드 실패 시 "새로고침"을 유도하거나 기본 UI를 보여주는 방어 로직을 구축하세요.

⚠️ Layout Shift (레이아웃 흔들림)

fallback UI와 실제 로드될 컴포넌트의 높이가 다르면 화면이 덜컥거리는 현상이 발생합니다.

  • Tip: 스켈레톤(Skeleton) UI를 활용하여 실제 컴포넌트와 유사한 크기의 가상 박스를 배치하는 것이 좋습니다.

4. Trade-offs: 모든 곳에 lazy를 써야 할까?

코드 스플리팅은 공짜 점심이 아닙니다. 다음과 같은 비용을 고려해야 합니다.

장점 단점 및 고려사항
초기 번들 크기 감소: FCP와 TTI가 개선됨 네트워크 왕복 비용: 너무 잘게 쪼개면 HTTP 요청 횟수가 늘어남
대역폭 절약: 사용자가 보지 않는 페이지의 코드는 받지 않음 사용자 경험 지연: 클릭 시 즉각적인 반응 대신 로딩 스피너를 봐야 함

최적의 전략:

  1. Route-based Splitting: 페이지 단위로 먼저 쪼갭니다 (가장 권장됨).
  2. Interaction-based Splitting: 모달, 탭, 복잡한 에디터 등 특정 액션 후에만 필요한 컴포넌트에 적용합니다.

5. 결론 및 제언

React의 lazy와 Suspense는 단순한 성능 최적화 도구를 넘어, 사용자에게 더 매끄러운 경험을 제공하기 위한 설계 철학의 일부입니다. 무조건적인 분할보다는 성능 측정 도구(Lighthouse)를 통해 번들 크기 분석($Total \ Bundle \ Size > 200KB$인 경우 권장)을 선행하고, 유의미한 지점에 적용하는 안목이 필요합니다.

현재 진행 중인 프로젝트에서 가장 무거운 외부 라이브러리를 사용하는 컴포넌트는 무엇인가요? 그것을 lazy로 분리했을 때 어느 정도의 초기 로딩 속도 개선이 있을지 측정해 보시는 것은 어떨까요?

더 깊이 있는 최적화를 원하신다면, 이후에 다룰 Pre-fetching 전략을 통해 로딩 스피너조차 보이지 않게 만드는 기법을 살펴보시는 것을 추천합니다.

반응형