Dynamically load remoteEntry.js files

Alex O'Callaghan

remoteEntry.js 파일을 동적으로 로드하기

원문은 Alex O'Callaghan님이 에 게재했습니다. 이 블로그 구독하기

Webpack Module Federation은 마이크로 프론트엔드 웹 애플리케이션 아키텍처를 지원하는 데 사용할 수 있다. 독립적으로 배포된 리모트 애플리케이션은 remoteEntry.js 파일을 로드하여 불러올 수 있다. 이러한 파일을 동적으로 로드하면 성능과 캐싱을 더 세밀하게 제어할 수 있으며, A/B 테스트나 브랜치별 배포 같은 기능도 가능해진다.

Mintel에서는 독립적이고 자율적인 팀들이 유지·관리하는 개별 애플리케이션을 셸(shell) 형태의 “호스트” 애플리케이션이 로드하는 구조로 Module Federation을 활용하고 있다. 이 접근 방식을 도입하게 된 과정에 대한 자세한 내용은 "Adopting a micro-frontend architecture"에서 확인할 수 있다.

기본적으로 Webpack Module Federation은 모든 리모트에 대해 remoteEntry.js 파일을 로드한다. 시간이 지나면서 팀들이 새로운 애플리케이션을 개발함에 따라 호스트에 연결된 리모트가 점점 많아졌다. 이 모든 리모트를 한 번에 로드할 필요는 없었다. 각 애플리케이션은 독립적으로 동작하고, 사용자는 다른 애플리케이션으로 이동하지 않고 하나의 애플리케이션 내에서 작업을 수행하는 경우가 많기 때문이다. 이렇게 많은 remoteEntry.js 파일을 로드하는 것은 애플리케이션의 초기 로드 시간에 영향을 주고 있었다.

Dynamic Remote Containers 방식을 사용하면 remoteEntry.js 파일 로드를 직접 제어할 수 있다.

ModuleFederationPlugin 설정에서 remotes 필드를 제거한다:

plugins: [
  new ModuleFederationPlugin({
    name: "host-app",
    remotes: {},
  }),
];

<script> 태그를 삽입해 자신의 코드에서 remoteEntry.js를 직접 로드한다:

const remoteEntryUrl = "https://something/remote-app/remoteEntry.js";
const scope = "remote-app";
const moduleName = "App";

await __webpack_init_sharing__("default");

await new Promise<void>((resolve, reject) => {
  const element = document.createElement("script");

  element.src = remoteEntryUrl;
  element.type = "text/javascript";
  element.async = true;

  element.onload = () => {
    element.parentElement?.removeChild(element);
    resolve();
  };

  element.onerror = (err) => {
    element.parentElement?.removeChild(element);
    reject(err);
  };

  document.head.appendChild(element);
});

// Initialize the federated module
const container: any = window[scope as any];
await container.init(__webpack_share_scopes__.default);

// Fetch module exposed by the federated module
const factory = await container.get(moduleName);
return factory();

module-federation-import-remote 패키지가 이 로직을 대신 제공해 줄 수 있다:

import { importRemote } from "module-federation-import-remote";

importRemote({ url: "https://something/remote-app", scope: "remote-app", module: "App" }).then((App) => {...});

// If App is a React component you can use it with lazy and Suspense just like a dynamic import:
const App = lazy(() => importRemote({ url: "https://something/remote-app", scope: "remote-app", module: "App" }));

return (
  <Suspense fallback={<div>Loading App...</div>}>
    <App />
  </Suspense>
);

이 방식을 통해 React 컴포넌트가 실제로 렌더링되어야 할 때까지 remoteEntry.js 로드를 지연시켜 초기 로드 성능을 개선할 수 있었다. 리모트를 어떻게 로드할지 직접 제어할 수 있게 되면 기능 플래그나 사용자 속성에 따라 런타임에 서로 다른 remoteEntry.js 파일을 동적으로 로드하는 것도 가능해진다.

importRemote는 기본적으로 remoteEntry.js URL에 캐시 무효화를 위한 쿼리 파라미터를 추가해 이러한 진입점이 캐시되지 않도록 한다. GitHub 스레드에서 논의된 것처럼 쿼리 스트링을 이용한 캐시 무효화에는 한계가 있다는 점을 유의해야 한다. CDN 설정이나 리모트 버전 관리를 도와줄 서비스 도입도 함께 고려하는 것이 좋다.

더 읽어보기:

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글