named export 컴포넌트의 비동기적 CodeSplitting

pengooseDev·2023년 9월 1일
2
post-thumbnail

해당 글에선 named export한 컴포넌트를 codeSplitting하기 위해, default export로 바꾸는 과정에서 휴먼에러를 방지하는 lazyExport 유틸함수에 대해 다룬다.

문제가 발생하지 않는 상황

필자는 ReactJS의 Routes 컴포넌트 내부에 들어가는 Route를 JSX 컴포넌트 타입으로 관리한다.

import { Home, Products, Login, SignInFunnel, OrderFunnel } from '@/pages';
import { Route } from '@/types';

export const ROUTES: Route = Object.freeze({
  '/': {
    COMPONENT: Home,
    AUTH: false,
  },
  '/products': {
    COMPONENT: Products,
    AUTH: false,
  },
  '/order': {
    COMPONENT: OrderFunnel,
    AUTH: true,
  },
  // ... codes
});

타입은 다음과 같다.


export interface RouteConfig {
  COMPONENT: () => JSX.Element;
  // ... Route 설정들
}

export type Route = Record<string, Readonly<RouteConfig>>;

이제 Router 컴포넌트에 적용하기 위해 Router.tsx를 생성해보자.

import { Routes, Route } from 'react-router-dom';
import { ROUTES } from '@/constants';
import { NotFound } from '@/pages';

export function Router() {
  return (
    <Routes>
      {Object.entries(ROUTES).map(([ROUTE, DATA]) => {
        return (
          <Route
            key={ROUTE}
            path={ROUTE}
            element={
              <>
                <DATA.COMPONENT />
              </>
            }
          ></Route>
        );
      })}
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

ROUTE 상수의 AUTH 로직은 이 글에서 다룬다.


CodeSplitting 적용(문제 발생)

import { Routes, Route } from 'react-router-dom';
import { ROUTES } from '@/constants';
import { NotFound } from '@/pages';

export function Router() {
  return (
    <Routes>
      {Object.entries(ROUTES).map(([ROUTE, DATA]) => {
        const LazyComponent = React.lazy(DATA.COMPONENT);

        return (
          <Route
            key={ROUTE}
            path={ROUTE}
            element={
              <React.Suspense fallback={<div>Loading...</div>}>
                <LazyComponent />
              </React.Suspense>
            }
          ></Route>
        );
      })}
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

큰 문제가 없어보이는 현재 코드에선 에러가 발생한다.

codeSplitting이 default export만 지원하기 때문이다.

물론, 이를 해결하는 간단한 방법이 많다. 다만, 채택하지 않은 근거는 다음과 같다.

1. export default로 변경

예상치 못한 휴먼에러에 코드를 노출시키는 것이 꺼림칙하여 배제하였다.

2. default export를 하는 파일 생성

index파일처럼 이렇게 진행할까 생각해봤지만, 파일이 변경될때마다 수정해야한다는 번거로움과 humanError로부터 여전히 위험하다는 생각에 배제하였다.


해결책 : Dynamic Import 유틸 함수

사실 이 글을 쓰는 이유도 ESM을 지원하는 환경에 대해 고찰하다, 이전에 내가 고민했던 Dynamic Import가 나오게되어 문득 블로그 글로 정리하고자 함이다.

Dynamic Import는 비동기적으로 모듈을 import하는 Promise를 return한다. 이러한 특성을 이용하여 codeSplitting 컴포넌트에 마운트 되기 전, 이를 가로채 named export된 JSX.element 컴포넌트 대신 default export로 변경하는 전략을 채택한 것이다.

export const lazyExportLoader = async (path: string, componentName: string) => {
  const module = await import(path);

  return { default: module[componentName] };
};

이를 ROUTE 상수에 적용해주자.

import { Route } from '@/types/route';
import { lazyExportLoader } from '@/utils/lazyExportLoader';

/* DynamicExport 적용 */
const Home = Object.freeze({
  COMPONENT: () => lazyExportLoader('../pages/Home', 'Home'),
});
// ...실제 사용시엔, 반복문을 사용하여 적용하도록 하자. 위 코드는 예시!

export const ROUTES: Route = Object.freeze({
   '/': {
    COMPONENT: Home,
    AUTH: false,
  },
  '/products': {
    COMPONENT: Products,
    AUTH: false,
  },
  '/order': {
    COMPONENT: OrderFunnel,
    AUTH: true,
  },
  // ... codes
});

COMPONENT의 type도 Promise type으로 변경해준다.

export interface RouteConfig {
  COMPONENT: () => Promise<{ default: React.ComponentType<any> }>;
}

export type Route = Record<string, Readonly<RouteConfig>>;

그럼 가지고 있던 모든 문제가 해결된다!
(과연?)


주의사항 (테스트코드 수정)

테스팅 라이브러리를 사용하고 있는가?
Cypress를 이용한 E2E Test, Jest를 이용한 유닛테스트를 적용했다면 TC를 수정해줘야 한다. Page Component가 비동기적으로 import되기 때문이다.

도메인 단위테스트야 사실 큰 영향이 없겠지만, Router 내에 존재하는 유닛테스트나, Cypress를 이용한 E2E Test를 진행하는 사람들이라면 페이지 컴포넌트(정확히는 Router단)에서 비동기적으로 import하는 것을 인지하여 테스트 코드를 수정하자.

다시 코드가 변경될 것을 대비해 설정 방법을 변경하는 유틸함수를 하나 작성해두는 것도 좋은 방법이다. :)

0개의 댓글