Suspense & Lazy Loading: 필요할 때 불러오기

React 앱을 빌드하면 보통 하나의 큰 JavaScript 번들이 만들어집니다. 앱이 커질수록 이 번들도 커지고, 사용자는 첫 화면에서 필요하지도 않은 코드까지 모두 다운로드해야 합니다. React.lazySuspense는 이 문제를 해결합니다.

React.lazy: 컴포넌트를 필요할 때 불러오기

React.lazy는 컴포넌트를 처음 렌더링될 때 동적으로 불러옵니다. 처음부터 번들에 포함하지 않습니다.

import { lazy } from 'react';
 
// 기존 방식: 앱 시작 시 즉시 로드
import HeavyComponent from './HeavyComponent';
 
// lazy 방식: 처음 렌더링될 때 로드
const HeavyComponent = lazy(() => import('./HeavyComponent'));

lazyimport()를 반환하는 함수를 받습니다. import()는 해당 파일을 별도의 번들 청크로 분리합니다.

Suspense: 로딩 중 UI 표시

lazy로 불러오는 컴포넌트는 처음 렌더링될 때 잠깐 로딩 상태가 됩니다. Suspense는 이 로딩 중인 동안 보여줄 fallback UI를 지정합니다.

import { lazy, Suspense } from 'react';
 
const HeavyChart = lazy(() => import('./HeavyChart'));
 
function Dashboard() {
  return (
    <div>
      <h1>대시보드</h1>
      <Suspense fallback={<div>차트 불러오는 중...</div>}>
        <HeavyChart />
      </Suspense>
    </div>
  );
}

HeavyChart가 로드되기 전까지 “차트 불러오는 중…”이 보이고, 로드 완료 후 차트가 표시됩니다.

라우트 단위 코드 스플리팅

가장 효과적인 스플리팅은 페이지(라우트) 단위입니다. 현재 보고 있는 페이지의 코드만 불러오면 됩니다.

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
 
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
 
function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<PageLoadingSpinner />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

사용자가 /dashboard에 접속하면 Dashboard 코드만 다운로드됩니다. About, Settings 코드는 해당 페이지에 가야 비로소 다운로드됩니다.

여러 Suspense 중첩하기

Suspense는 중첩해서 세밀하게 제어할 수 있습니다.

function App() {
  return (
    <Suspense fallback={<AppShell />}> {/* 앱 전체 로딩 */}
      <Header />
      <main>
        <Suspense fallback={<SidebarSkeleton />}> {/* 사이드바만 */}
          <Sidebar />
        </Suspense>
        <Suspense fallback={<ContentSkeleton />}> {/* 콘텐츠만 */}
          <MainContent />
        </Suspense>
      </main>
    </Suspense>
  );
}

Sidebar가 로딩 중이어도 MainContent가 준비되면 바로 보여줄 수 있습니다.

Error Boundary와 함께

코드 로딩에 실패할 수도 있습니다. ErrorBoundary와 함께 사용해 에러 상태도 처리합니다.

import { ErrorBoundary } from 'react-error-boundary';
 
const HeavyComponent = lazy(() => import('./HeavyComponent'));
 
function SafeLazyComponent() {
  return (
    <ErrorBoundary fallback={<div>컴포넌트를 불러오지 못했습니다.</div>}>
      <Suspense fallback={<div>불러오는 중...</div>}>
        <HeavyComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

어떤 컴포넌트를 분리해야 할까?

모든 컴포넌트를 lazy로 만들 필요는 없습니다. 오히려 너무 잘게 나누면 네트워크 요청이 많아져 역효과가 납니다.

좋은 분리 대상

  • 페이지 컴포넌트 (라우트 단위)
  • 모달, 드로어처럼 처음에 보이지 않는 UI
  • 차트, 에디터처럼 무거운 서드파티 라이브러리를 포함한 컴포넌트

분리 효과가 적은 경우

  • 항상 바로 보이는 작은 컴포넌트
  • 코드가 매우 작은 컴포넌트

정리

항목설명
React.lazy컴포넌트를 동적으로 import, 별도 번들 청크로 분리
Suspense로딩 중일 때 보여줄 fallback UI 지정
핵심 전략라우트 단위로 분리하면 가장 효과적
Error 처리ErrorBoundary로 로딩 실패 상황도 처리

초기 번들 크기를 줄이면 첫 화면 로딩 속도가 빨라집니다. 특히 대규모 앱에서 체감 성능 개선에 효과적입니다.