본문 바로가기

React

React 앱에서 코드 분할과 지연 로딩을 통한 성능 최적화

대규모 React 애플리케이션에서는 초기 로딩 속도가 사용자 경험을 크게 좌우합니다. 모든 컴포넌트를 한 번에 번들링하면 번들 크기가 커지고, 네트워크 전송 및 파싱에 오래 걸리게 됩니다. 이를 해결하기 위해 코드 분할(Code Splitting)지연 로딩(Lazy Loading)을 활용해 필요한 시점에만 코드를 로드하는 전략을 적용합니다.

1. 코드 분할이란?

코드 분할은 하나의 큰 번들을 여러 개의 작은 청크(chunk)로 나누는 작업입니다. Webpack, Vite 등 번들러가 이를 지원하며, React에서는 React.lazySuspense를 사용해 손쉽게 구현할 수 있습니다.

2. 기본적인 구현 방법

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

// 페이지별 컴포넌트를 lazy 로 로드
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

function App() {
  return (
    <Suspense fallback={<div>Loading…</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  );
}

export default App;

위 예시처럼 라우트 레벨에서 컴포넌트를 분리하면, 사용자가 해당 경로에 접근할 때만 해당 청크를 다운로드합니다.

3. 라우트 기반 코드 분할 (React Router v6)

React Router v6에서는 lazyOutlet을 결합해 중첩 라우트까지 손쉽게 분할할 수 있습니다.

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { Suspense, lazy } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />, // 공통 레이아웃
    children: [
      { index: true, element: <Home /> },
      {
        path: 'dashboard',
        element: (
          <Suspense fallback={<div>Loading Dashboard…</div>}> 
            <Dashboard />
          </Suspense>
        ),
        children: [
          {
            path: 'settings',
            element: (
              <Suspense fallback={<div>Loading Settings…</div>}> 
                <Settings />
              </Suspense>
            ),
          },
        ],
      },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

export default App;

4. 동적 import 로 컴포넌트 외부 라이브러리 지연 로딩

특정 UI 라이브러리나 차트 라이브러리는 무겁기 때문에, 사용자가 실제로 차트를 볼 때만 로드하도록 할 수 있습니다.

const Chart = lazy(() => import('react-apexcharts'));

function Stats() {
  const [show, setShow] = React.useState(false);
  return (
    <div>
      <button onClick={() => setShow(true)}>차트 보기</button>
      {show && (
        <Suspense fallback={<div>Loading chart…</div>}> 
          <Chart options={...} series={...} type="line" />
        </Suspense>
      )}
    </div>
  );
}

5. Webpack Chunk 이름 지정

디버깅과 네트워크 모니터링을 편리하게 하기 위해 청크 이름을 직접 지정할 수 있습니다.

const AdminPanel = lazy(() => import(/* webpackChunkName: "admin-panel" */ './pages/AdminPanel'));

Chrome DevTools > Network 탭에서 admin-panel.js 파일이 별도로 로드되는 것을 확인할 수 있습니다.

6. 성능 모니터링

코드 분할 적용 후 반드시 LighthouseWeb Vitals를 통해 다음 지표를 검증합니다.

  • First Contentful Paint (FCP) 감소
  • Largest Contentful Paint (LCP) 개선
  • Total Blocking Time (TBT) 감소

또한 webpack-bundle-analyzer를 이용해 청크 크기를 시각화하고, 불필요하게 큰 청크가 없는지 점검합니다.

7. 실전 팁

  • 페이지 진입 직전 preload 혹은 prefetch 를 활용해 사용자가 곧 방문할 가능성이 높은 청크를 미리 받아두세요.
  • SSR(서버 사이드 렌더링)과 결합할 경우 loadable-components 를 사용하면 서버와 클라이언트 모두에서 청크 매핑을 일관되게 처리할 수 있습니다.
  • React 18부터 도입된 Suspense for data fetching 과 함께 사용하면 API 데이터 로드와 컴포넌트 로드를 동시에 최적화할 수 있습니다.

코드 분할과 지연 로딩을 적절히 적용하면 초기 번들 크기를 크게 줄이고, 사용자에게 빠른 인터랙티브 경험을 제공할 수 있습니다. 프로젝트에 맞는 청크 전략을 설계하고, 지속적인 성능 모니터링을 통해 최적화 상태를 유지하세요.