본문 바로가기
C.W.K.
Stream
Lesson 02 of 07 · published

Code Splitting — lazy 라우트, lazy 컴포넌트

~11 min · code-splitting, lazy, performance

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
사용자가 방문하지 않은 화면의 코드까지 처음부터 내려받을 이유는 없어. 코드 분할로 현재 경험에 필요한 JavaScript만 보내는 방법을 익혀 보자.

동적 import가 chunk 경계를 만들어

const Settings = lazy(() => import('./Settings'))처럼 import를 함수 안에 두면 Vite와 Rollup이 Settings를 별도 chunk로 만들어. 런타임에서는 Settings를 처음 렌더링할 때 그 파일을 받고, React는 기다리는 동안 가장 가까운 Suspense의 대체 UI를 보여 줘.

같은 모듈을 다른 곳에서 정적으로 import하면 메인 bundle에도 포함될 수 있어. 빌드 뒤 dist/assets와 브라우저 Network 패널을 함께 봐야 실제 경계가 생겼는지 확인할 수 있어.

사용자가 구분하는 경험 단위로 나눠

  • Route는 가장 자연스러운 경계야. 홈을 방문한 사용자는 admin 화면의 비용을 낼 필요가 없어.
  • 무거운 기능은 chart, Markdown editor, image editor처럼 일부 화면에서만 쓰는 큰 의존성을 따로 받을 수 있어.
  • Modal과 dialog는 사용자가 열기 전까지 필요하지 않다면 늦게 받아도 돼.

작은 utility 함수마다 경계를 만들면 요청 수와 관리 비용만 늘어. 사용자가 별도의 로딩 상태를 이해할 수 있는 단위인지 먼저 물어봐.

Chunk에도 네트워크 비용이 있어

별도 chunk 하나마다 요청과 parsing 비용이 생겨. 너무 잘게 나누면 작은 요청이 연달아 이어지는 waterfall 때문에 큰 bundle 하나보다 느려질 수 있어. 30KB에서 200KB 정도의 gzip 결과는 출발점일 뿐이고, 실제 판단은 사용자 기기와 네트워크에서 측정해야 해.

첫 화면의 제목, navigation, 주요 동작처럼 바로 보여야 하는 내용은 지연 로딩하지 마. 항상 필요한 화면을 늦추면 짧은 loading flash만 추가되고 초기 경험은 더 나빠져.

곧 필요할 화면은 미리 받아

사용자가 Settings 링크에 pointer를 올렸거나 브라우저가 한가해졌다면 해당 module을 미리 요청할 수 있어. HTML의 modulepreload를 사용하거나 같은 동적 import를 먼저 호출해 cache를 데울 수 있어. Vite도 entry chunk의 직접 의존성에는 modulepreload hint를 자동으로 만들어.

미리 받기는 추측이 틀리면 사용하지 않을 bytes를 소비해. 자주 이어지는 navigation처럼 근거가 있는 경로에만 적용하고 Network 패널에서 실제 요청 시점을 확인해.

코드 경계는 사용자가 느끼는 화면 경계와 맞춰. Route, 무거운 기능, 선택적으로 여는 dialog는 좋은 후보야. 첫 화면의 핵심 콘텐츠와 작은 helper는 처음부터 함께 보내는 편이 낫다.

Code

React Router의 route별 코드 분할·tsx
import { lazy, Suspense } from "react";
import { Routes, Route } from "react-router-dom";

// 각 lazy() 호출은 프로덕션 빌드에서 별도 chunk 경계를 만들어.
const Home = lazy(() => import("./pages/Home"));
const Settings = lazy(() => import("./pages/Settings"));
const Admin = lazy(() => import("./pages/Admin"));

export function App() {
  return (
    <Suspense fallback={<p>Loading page…</p>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/admin" element={<Admin />} />
      </Routes>
    </Suspense>
  );
}

// 처음에는 메인 bundle과 Home chunk만 받아. /settings로 이동하면 Settings
// chunk를 요청하고, Admin chunk는 해당 route를 방문할 때까지 받지 않아.
Lazy 모달: 열 때만 로드·tsx
import { lazy, Suspense, useState } from "react";

const HeavySettingsModal = lazy(() => import("./HeavySettingsModal"));

function Header() {
  const [open, setOpen] = useState(false);
  return (
    <header>
      <button onClick={() => setOpen(true)}>Settings</button>
      {open && (
        <Suspense fallback={<p>Loading settings…</p>}>
          <HeavySettingsModal onClose={() => setOpen(false)} />
        </Suspense>
      )}
    </header>
  );
}

// 사용자가 Settings 클릭 전엔 모달의 번들 다운로드 안 됨.

External links

Exercise

부트스트랩 프로젝트의 route를 eager import에서 React.lazy와 Suspense로 바꿔. 빌드 뒤 dist/assets에 route별 chunk가 생겼는지 보고, Network 패널에서 처음 방문할 때만 해당 chunk를 받는지 확인해. 이어서 버튼으로 여는 lazy modal도 같은 방식으로 시험해.
Hint
경계는 lazy(() => import('./Home'))처럼 함수 안의 동적 import가 만들어. 같은 모듈을 다른 곳에서 정적으로 import하면 메인 번들에 다시 포함될 수 있어.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.