React.lazy()


React에서 코드 분할을 목적으로(Chunk 분리) 컴포넌트를 Dynamic Import 할 때 React.lazy() 함수를 사용한다. 컴포넌트가 필요한 시점에만 로드되기 때문에, 로딩 중 표시할 컴포넌트나 메시지는 <Suspense>를 활용하여 설정한다. 여러 개의 lazy 컴포넌트를 묶어서 fallback을 한 번에 표시할 수도 있다.

또한, React.lazy()로 불러오려는 컴포넌트는 기본적으로 default export를 사용해서 내보내야 한다.

export default function Child() { /* ... */ }
import { Suspense } from 'react';

// 컴포넌트 바디에서 호출하면 정상 작동하지 않으므로 주의
const Child = React.lazy(() => import('./Child'));

const Parent = () => {
  return (
    <Suspense fallback={<Loading />}>
      <Child />
    </Suspense>
  );
}

컴포넌트가 named export 라면 아래처럼 모듈의 프로퍼티(컴포넌트 이름)를 default 속성에 담아서 반환하는 Workaround(차선책)를 사용해야 한다.

const Child = React.lazy(() =>
  import('./Child').then((module) => ({ default: module.Child })),
);

Proxy를 활용한 커스텀 LazyImport


객체 프로퍼티의 읽기 / 쓰기 작업을 중간에 가로채서 원하는 작업을 수행할 때 Proxy를 사용한다. Proxy로 React.lazy() 함수를 랩핑해서 커스텀하면 named export 컴포넌트도 깔끔하게 동적 import할 수 있다.

import { ComponentType, lazy } from 'react';

type ComponentName = string;
type Loader<T> = () => Promise<T>;
type NamedComponents = Record<ComponentName, unknown>;

export const lazyImport = <T extends NamedComponents>(loader: Loader<T>) => {
  return new Proxy({} as T, {
    get: (_target, name: ComponentName) => {
      return lazy(async () => {
        const module = await loader();
        const Component = module[name] as ComponentType<T>;

        return { default: Component };
      });
    },
  });
};
// lazyImport 사용
const { Home, About } = lazyImport(() => import('@/features/misc'));

lazyImport는 Promise를 반환하는 loader 함수(() => import('path'))를 인자로 받아 Proxy 객체를 반환한다. 이 반환된 Proxy 객체는 GET 트랩을 통해 세 단계의 커스터마이징된 동작을 수행한다.

  1. 모듈의 프로퍼티(컴포넌트 이름)에 접근할 때 GET 트랩 실행
  2. loader 함수를 통해 비동기적으로 모듈 로드(import)
  3. 불러온 모듈의 프로퍼티(컴포넌트) 반환

예를들어 아래와 같이 두 개의 컴포넌트를 내보내고 있는 misc.tsx 파일이 있다고 가정해보자. 이 파일을 불러오면 모듈은 {Home: (props) => {...}, About: (props) => {...}} 형태가 된다.

// src/features/misc.tsx
export const Home = (props) => { /* ... */ };
export const About = (props) => { /* ... */ };