(번역) Module Federation을 언제, 어떻게 사용해야 하나요?

박종훈·2022년 3월 26일
5
post-thumbnail

원문: When should you leverage Module Federation, and how?

모듈 페더레이션(Module Federation)을 언제, 왜, 어디서 활용해야 하는지에 관해 기술의 창안자의 생각과 패턴을 공유합니다.

웹팩(Webpack)의 페더레이션(Federation)은 적절히 사용될 경우 굉장히 유용합니다.

어려운 점은 기술을 어디에 어떻게 활용하는지에 대한 정보가 많지 않다는 것입니다. 이 기술을 2년 가까이 다루어 오면서, 대규모 애플리케이션을 개발할 때 사용되는 몇 가지 패턴과 고려해야 하는 점을 발견했습니다.

본 글에서는, 프런트엔드에서의 모듈 페더레이션에 집중할 것입니다. 백엔드에 페더레이션을 적용하는 것은 완전히 다른 이야기입니다.

언제 사용하나요?

페더레이션의 주요 사용 사례는 런타임에 소프트웨어를 독립적으로 배포하는 것입니다. 모듈 페더레이션은 특히 다음 몇 가지 분류에 잘 어울립니다.

헤더(Header)푸터(Footer)와 같은 글로벌 컴포넌트는 훌륭한 페더레이션 대상입니다. 일반적으로 이 컴포넌트들은 애플리케이션 내에서 통일된 모습과 행동을 가지고, 비교적 독립적이며, 이상적으로는 모든 애플리케이션 내에서 일관되어야 합니다. 헤더 컴포넌트를 업데이트하고 NPM에 배포하고 나서, 5~10~15개의 애플리케이션에 각각 헤더를 설치하고, 배포하는 것은 빠르게 피로가 쌓이게 합니다. 빌드와 배포 파이프라인을 전부 통과하는데 20~30분은 소요되기 때문에, 많은 시간을 낭비하게 됩니다. 스테이지 환경에서의 테스트는 고통스럽고, 모든 사용자에게 배포하는 것은 고됩니다.

다른 팀에서 관리하지만 다양한 유저 플로우와 애플리케이션에서 사용되는 피쳐도 페더레이션 대상이 될 수 있습니다. 하지만 이는 양날의 검입니다. "컴포넌트 레벨 오너십" 모델 같은 협의 없이 임의적으로 앱 사이에 모듈을 공유해선 안 됩니다.

AB 테스팅 팀애널리틱 팀 또는 플랫폼 팀과 같은 목적 조직은 편리한 런타임 오케스트레이션을 통해 큰 이득을 얻을 수 있습니다. 이런 팀들은 잘 협의된 API 만을 이용하고 주로 다른 기능과 분리된 코드를 사용하는 경향이 있어, 페더레이션 대상으로 아주 적합합니다.

시스템 이전과 아키텍처 고도화는 페더레이션이 도입되면 가장 큰 이점을 얻는 사례였습니다.

  • LOSA - 수 많은 작은 애플리케이션 (lots of small applications)
  • 마이크로 프런트엔드
  • 폴리리스 (Poliliths)
  • 레거시 시스템에 대한 교살자 패턴
  • 멀티쓰레드 컴퓨팅

저는 위에서 언급한 대부분의 사례를 작업해본 적이 있습니다. 이 작업들은 굉장히 복잡하고 제약이 많았습니다. 대부분의 전통적인 환경에서 앱을 서로 연결하는 것은 매우 소모적인 일입니다. 외부 모듈을 사용하거나 완전히 별도의 엔트리포인트 빌드를 사용하면, 제약 안에서 작업을 "할 수는" 있었습니다. 여러 개의 마이크로 프런트엔드를 활성화하는 것은 한 번의 하이드레이션 스윕보다는 느립니다.

이 밖에도 모듈 페더레이션이 유용한 경우는 많습니다. 하지만 만약 이런 종류의 아키텍처에 생소하다면, 단순한 것부터 시작하세요.

저는 NPM을 다루는 게 너무 귀찮아지면 모듈 페더레이션을 사용해 볼 것을 추천합니다. 가지고 있는 모든 것이 페더레이트 되게 하진 말고, 어떤 모듈은 NPM에, 어떤 모듈은 모노레포에, 또 다른 모듈들은 페더레이션을 통해 가져오게 하는 거죠. 오너십의 경계가 어디있는지에 따라 유동적으로요.

어디에 사용하나요?

위에서 언급된 '언제 사용하나요'와 비슷하지만, 조금 더 자세히 살펴봅시다. 아무 모듈이나 직접 노출해선 안된다고 이미 언급한 바 있습니다. 모듈이 어떤 취약성을 가져올지 고려해야 합니다. 코드는 일관된 뭔가로 감싸거나, 모듈 프록시 등을 노출하여 피쳐가 바뀌어도 사용자가 전달하는 인자는 유지되게 할 수 있습니다. 트랜스듀서(transducer)를 이용하거나 피쳐의 모양에 대한 계약을 변경하는 등의 방법으로 말이죠.

피쳐의 크기도 고려되어야 합니다. 너무 세세하게 모듈을 나누면 부작용이 클 수 있습니다. 이런 면에서 컴포넌트 레벨 오너십이 크게 도움이 될 수 있습니다.

컴포넌트 레벨 오너십(Component Level Ownership, CLO)이 뭔가요?

CLO는 컴포넌트에 가능한 많은 책임을 전가하는 소프트웨어 디자인 패턴입니다. 바보 컴포넌트(dumb components)에 관해서 들어보셨을 것입니다. 이 컴포넌트들은 똑똑한 컴포넌트(smart components)입니다.

기본이 되는 아이디어는 이 컴포넌트들은 독자적이거나 거의 독자적으로 행동한다는 것입니다. 모놀리식 앱 안에서, 페이지 레벨에 위치한 많은 로직을 발견할 수 있을 겁니다. 데이터, 상태, 프롭(props) 등이 바보 컴포넌트에게 하달됩니다. 똑똑한 컴포넌트에서는, 페이지에서 컴포넌트가 어떤 일을 해야 하는지에 대한 힌트가 전달됩니다. 이론적으로, CRA 앱에 컴포넌트를 마운트하고, 아주 기본적인 프롭과 힌트만을 전달해 컴포넌트가 모든 일을 하게 할 수 있습니다.

예로, 여기 일반적인 소프트웨어 디자인을 가진 프로덕트 페이지가 있습니다. PurchaseAttributes는 페이지로부터 다양한 프롭을 받고, 데이터를 바인딩하고 있습니다.

const PDP = ({productData, deviceType, flags,active})=>(
  <div>
    <PurchaseAttributes productData={productData} isMobile={deviceType.isMobile} abTestInfo={flags} selectedColor={active}/>
  </div>
)

export default PDP

CLO에서는, 이렇게 될 것입니다:

const PDP = ({sku,active})=>(
  <div>
    <PurchaseAttributes sku={sku} selectedColor={active}/>
  </div>
)

export default PDP

이제 PurchaseAttributes는 독립적으로 productData를 가져올 수 있습니다. 혹은, 페이지에서 여러 컴포넌트가 동일한 데이터 여러 번 요청하는 것을 줄이고 싶다면, 인메모리 캐시 등을 이용해 productData를 스스로 가져올 수 있습니다. 중복된 요청을 보내는 대신 이미 호출된 프라미스를 이용해 여러 컴포넌트가 한 번의 ajax 요청으로 productData를 얻게할 수도 있습니다.

다소 생각이 요구되지만, 리액트 18의 비동기 세상에서는 어차피 해결해야 하는 문제이기도 합니다.

PurchaseAttributes는 어떻게 생겼을까요?

저는 CLO 컴포넌트에 "3 export 규칙"을 사용합니다. 3개보다 많은 export가 있을 수는 있지만, 사용자에게 충분한 유연성과 구성성을 제공해야 합니다.

결국 3개 export는 데이터 유틸리티, 바보 컴포넌트, 그리고 이 둘을 조합한 똑똑한 컴포넌트가 되게 됩니다.

개발팀은 API 유틸이나 GraphQL 프래그먼트를 이용해 데이터를 구성하거나, 직접 데이터를 호출해 바보 컴포넌트에 주입할 수 있습니다. 하지만 "그냥 작동하는" 자급자족하는 export도 있습니다.

CLO를 어디에 도입하는지도 중요합니다. \<Title> 같은 컴포넌트에는 이 패턴이 필요하지 않을테니까요. 피쳐에 알맞은 경계를 설정해야 할 것입니다. 컴포넌트를 에러 경계를 설정해 감싸서, 단일 앱 트리를 공유한다 해도 충격 범위를 줄일 수 있습니다.

import React from "react";
import { useSSE } from "use-sse";

export const dataUtility = (sku)=>{
  return fetch(`https://myapi.example.com/api/products?sku=${sku}`).then((res) => res.json());
}

export const PurchaseAttributes = (data)=>(<div>{data.title}</div>)

const PurchaseAttributesSmart = ({sku}) => {
  const [data, error] = useSSE(() => {
    return dataUtility(sku)
  }, []);

  return <PurchaseAttributes {...data}/>;
};

export default PurchaseAttributesSmart

페더레이션에서 노출된 모듈의 경우, 추상화 레이어를 한 겹 더해 테스트되고, 인터페이스 계약을 가질 수 있게 합니다.

// exposedPurchaseAttributes.js
import PurchaseAttributes from '../components/PurchaseAttributes'
// 독립적으로 유닛 테스트 될 수 있습니다
const moduleProxy = ({inventoryID, color})=>{
  // 인자가 변경될 수 있지만, 프롭 인터페이스는 사용자에게 직접 노출되지 않습니다
  return <PurchaseAttributes sku={inventoryID} selectedColor={color}/>
}

export default moduleProxy

모듈 페더레이션은 빈약한 소프트웨어 디자인을 위한 대체제가 아닙니다. 모듈성과 명확한 오너십의 경계는 항상 중요합니다.

로깅도 고려되어야 합니다. 모듈을 사용하는 팀이 아니라 모듈에 오너십을 가진 팀에 에러가 전달되어야 하기 때문입니다.

왜 모듈 페더레이션을 이용하나요?

스케일링에 초점이 맞춰졌다면, 모듈 페더레이션이 인생을 더 편하게 해줍니다. 모노레포와 빠른 빌드는 다음의 이슈를 해결해주지 못합니다: 반복적인 릴리즈, 싱글톤 문제, 패키지의 여러 버전이 필요할 때. 터보레포(turborepo)와 같은 툴들은 언제나 좋습니다. 하지만, 여러 팀에 걸쳐 독립된 파이프라인이 필요하다면 모듈 페더레이션이 도움이 될 수 있습니다. 어떤 책임들은 빌드 시에 가장 잘 처리되고, 다른 책임들은 런타임에 처리되는 것이 좋습니다.

만약 모듈 페더레이션이 무엇을 하는지 알지만 왜 필요한지 모르겠다면, 아마 필요하지 않으실 겁니다. 모듈 페더레이션은 대규모 문제의 해결책입니다.

저는 모노레포를 완전히 격리되어 있지만 같은 팀에 속한 동일한 인프라스트럭처 또는 흐름을 재활용하는 서브앱을 포함하는 인스턴스를 정리하는 수단으로 사용합니다. 모노레포는 npm 패키지 같은 느낌과 추상성을 가지고 있기 때문에 인스턴스가 어지러워 지는 것을 방지합니다. CLO와 페더레이션을 사용하면, 편리함을 넘어, 코드가 인스턴스와 결합되지 않기 때문에 리포지토리를 반으로 분할하거나 "드래그 앤 드롭" 리팩토링을 배포할 수 있습니다.

페더레이션과 모노레포를 함께 사용하는 것은 리포지토리 안팎의 수직/수평적 확장에 좋은 패턴으로 보입니다. 만약 단일 인스턴스나 리포지토리가 너무 커진다면, 서브앱을 새로운 인스턴스에 분할하면 됩니다.

CLO 패턴이 특정 크기의 컴포넌트, 피쳐 클래스, 유즈 케이스에 모두 적용되기 때문에, 설계 상 비교적 큰 구성요소가 코드베이스 속에서 재배치 되는데 어려움이 없습니다. 분산 방식은 중요하지 않습니다. 페더레이션, NPM, 모노레포 워크스페이스 등, 작동하기만 한다면 바꿔 끼울 수 있습니다.

즉각적인 롤백이나 부분 코드 프리징, 또는 동적 카나리 배포 등이 필요할 수 있습니다. 이럴 때 웹팩 그래프의 일부를 클라이언트의 런타임에서 교체할 수 있는 것은 아주 강력합니다. 갖가지의 정적 배포물이 필요하지 않고, 대신에 서로 다른 앱을 런타임에 서로 다른 런타임에 링크할 수 있고, 전체 애플리케이션 스택을 재배포할 필요 없이 웹팩 그래프를 언제든 바꿀 수 있습니다.

이런 시나리오에서 고정된 리모트 버전(pinned remote versions)와 고급 플러그인 API가 빛을 발합니다. 고정된 리모트를 통해, npm의 락파일과 같이 버전 컨트롤을 할 수 있습니다. 필요하다면 NPM처럼, 중첩된 import 속에서도 같은 리모트의 다른 고정 버전을 리졸브하게 할 수도 있습니다. 이런 방식을 사용하면 안정적인 운영이 필요할 때, 항상 최신인 버전을 사용하지 않을 수 있습니다.

이 예시에서, 웹팩이 명령 및 제어 API를 통해 누가 호스트이고 호스트가 어떤 리모트를 요구하는지 지정합니다. 이를 통해 고정된 리모트의 URL과 글로벌 네임스페이스가 포함된 객체를 얻게 됩니다.

그러고 나서 이 객체를 DOM에 주입하고 글로벌을 리졸브합니다. 만약 여기서 소비되는 응답시간이 아깝다면, SSR을 통해 윈도우에 이 데이터를 주입하거나, 런타임 네트워크 의존적이지 않은 다른 방식을 사용할 수도 있습니다.

new ModuleFederationPlugin(   {
    name: 'someapp',
    filename: "static/chunks/remoteEntry.js",
    exposes: {},
    remotes: {
      remote1:`promise new Promise(resolve=>{
      fetch('http://commandAndControl.com/remoteManager?host=someApp&remote=remote1').then(res=>{
        return res.json()
      }).then(res=>{
        const {remoteUrl, globalName} = res
        // http:myCDN.com/remotes/v1.1/remoteEntry.js->window.remote1_v1_1
          injectScript(remoteUrl).then(()=>{
            resolve(window[globalName]) // window.remote1_v1_1
          })
        })
      })`
    },
    shared: {
      react: {
        requiredVersion: false,
        singleton: true,
      },
    },
  },
)

추가적인 고려 사항

라우팅과 서비스 디스코버리(service discovery)는 특정 규모를 넘어가면 꽤 유용해집니다. 동적으로 리모트를 로드하거나 특정 export를 찾기 위해 리모트를 탐색할 수 있는 것은 환경설정과 라우팅 로직을 분산하는데 크게 도움이 됩니다.

동적으로 리모트를 로드하는 좋은 방법입니다.

/**
 *
 * @param {string} remote - 리모트의 글로벌 네임
 * @param {object | string} shareScope - shareScope 객체 또는 scope key
 * @param {string} remoteFallbackUrl - 리모트 모듈의 대비책 url
 * @returns {Promise<object>} - 페더레이트 모듈의 컨테이너
 */
const getOrLoadRemote = (remote, shareScope, remoteFallbackUrl = undefined) =>
  new Promise((resolve, reject) => {
    // window에 리모트 모듈이 있는지 확인
    if (!window[remote]) {
      // DOM에 리모트 태그가 있는지 탐색, 하지만 아직 로딩 중일 수 있다 (비동기)
      const existingRemote = document.querySelector(
        `[data-webpack="${remote}"]`
      );
      // 리모트가 로드되면..
      const onload = async () => {
        // 초기화 되었는지 확인
        if (!window[remote].__initialized) {
          // 만일 share scope가 존재하지 않는다면 (webpack 4에서처럼) shareScope이 직접 주입한 객체라고 가정 
          if (typeof __webpack_share_scopes__ === "undefined") {
            // 주입된 share scope 객체의 default를 사용
            await window[remote].init(shareScope.default);
          } else {
            // 그게 아니라면, 일반적인 방식으로 share scope 초기화
            await window[remote].init(__webpack_share_scopes__[shareScope]);
          }
          // 리모트가 초기화되었다고 표시
          window[remote].__initialized = true;
        }
        // 리모트가 로드됐음을 표시하며 프라미스를 리졸브
        resolve();
      };
      if (existingRemote) {
        // 리모트가 존재하지만 로드되지 않았을 때, onload에 발을 걸쳐 로드를 기다린다
        existingRemote.onload = onload;
        existingRemote.onerror = reject;
        // 함수에 인자로서 전달된 리모트 대비책이 있는지 확인
        // TODO: 오버라이드가 존재하지 않을 때, 퍼블릭 설정을 스캔해야 한다
      } else if (remoteFallbackUrl) {
        // 대비책이 존재할 경우 리모트를 주입하고 같은 onload 함수를 호출한다
        var d = document,
          script = d.createElement("script");
        script.type = "text/javascript";
        // data-wepback 이라고 표시하여 런타임이 내부적으로 추적할 수 있게 한다
        script.setAttribute("data-webpack", `${remote}`);
        script.async = true;
        script.onerror = reject;
        script.onload = onload;
        script.src = remoteFallbackUrl;
        d.getElementsByTagName("head")[0].appendChild(script);
      } else {
        // 리모트와 대비책이 모두 없다면, 리젝트
        reject(`Cannot Find Remote ${remote} to inject`);
      }
    } else {
      // 리모트가 이미 초기화됐다면, 리졸브
      resolve();
    }
  });

// getOrLoadRemote('checkout', {default: {//shimShareScope}} || 'default', 'http://theRemote.com')

이 방법은 다른 로딩 방법과 함께 사용될 수 있습니다. 예로, 빌드 시에 모든 리모트를 웹팩 글로벌에 전달하고, 런타임에 추가적으로 탐색할 수도 있습니다. 페이지로 라우팅하기 위해서는, 여전히 동적으로 리모트 로딩을 해야할 것입니다.

이제 저는 네트워크에 연결된 어떤 리모트든 검색할 수 있을 뿐만 아니라, 로드하고 초기화할 수도 있습니다.

저는 한 발자국 더 가서, 이 방식을 웹팩 플러그인에 직접 통합시켰습니다. 덕분에 웹팩이 빌드 시에 promise new Promise 로직과 같은 것을 이용할 수 있습니다.

async function matchFederatedPage(remotes, path) {
  if (!remotes) {
    console.error(
      "No __REMOTES__ webpack global defined or no remotes passed to catchAll"
    );
  }
  const maps = await Promise.all(
    Object.entries(remotes).map(([remote, loadRemote]) => {
      console.log("page map", remote, loadRemote);
      const loadOrReferenceRemote = !window[remote]
        ? loadRemote()
        : window[remote];
      return Promise.resolve(loadOrReferenceRemote).then((container) => {
        return container
          .get("./pages-map")
          .then((factory) => ({ remote, config: factory().default }))
          .catch(() => null);
      });
    })
  );
  const config = {};

  for (let map of maps) {
    if (!map) continue;

    for (let [path, mod] of Object.entries(map.config)) {
      config[path] = {
        remote: map.remote,
        module: mod,
      };
    }
  }

  const matcher = createMatcher(config);
  const match = matcher(path);

  return match;
}

이 예에서는 다른 플러그인을 통해 모듈 페더레이션의 기본 설정을 구성하고 있습니다. 이 경우, 표준 플러그인 옵션을 파싱함으로써 추론할 수 있는 메타데이터를 추가할 수 있습니다.

이것은 페더레이션 인터페이스를 "확장"하는 지극히 간단한 방법입니다. 실제로 하는 일이 복잡하지 않은 소수의 플러그인으로 많은 작업을 수행할 수 있습니다.

const clientRemotes = Object.entries(
      federationPluginOptions.remotes || {}
    ).reduce((acc, [name, config]) => {
      const scriptUrl = config.split("@")[1];
      const moduleName = config.split("@")[0];
      // 리모트 모듈의 두 버전을 생성
      // 하나는 빌드 타임에 페더레이션이 참조할 수 있게
      if (!acc.buildTime) {
        acc.buildTime = {};
      }
      // 또 하나는 러타임과 동적 리모트를 위해
      if (!acc.runtime) {
        acc.runtime = {};
      }
      // TODO: 함수 호출 시 인자를 건네받고, 화살표 함수를 사용하지 않는다
      const remotePromiseLoad = `new Promise((res,rej)=>{
      console.log('share scope from runtime args',arguments[2].I);
      // 만약 웹팩 require가 없으면, 모듈 인자로부터 생성한다
      var ${webpack.RuntimeGlobals.require} = ${
        webpack.RuntimeGlobals.require
      } ? ${webpack.RuntimeGlobals.require} : arguments[2]
      var existingScript = document.querySelector("[data-webpack=${name}")    

   
      var d = document, script = d.createElement('script');
script.type = 'text/javascript';
script.setAttribute("data-webpack", ${JSON.stringify(name)});
script.async = true;
script.onerror = function(error){rej(error)};
script.onload = function(){
  if(!${moduleName}.__initialized) {
      Promise.resolve(${moduleName}.init(${
        webpack.RuntimeGlobals.shareScopeMap
      }.default)).then(function(){
        ${moduleName}.__initialized = true;
        console.log('resolved', JSON.stringify(${moduleName}));
        res(${moduleName});
      });
  } else {
    ${moduleName}.__initialized = true;
    res(${moduleName});
  }
};
script.src = '${scriptUrl}';
if(existingScript) {
  if(${moduleName}) {
    script.onload()
  }
  existingScript.onload = script.onload
  existingScript.onerror = script.onerror
} else {
  d.getElementsByTagName('head')[0].appendChild(script);
}
      })`;

      acc.runtime[name] = `()=> ${remotePromiseLoad}`;
      acc.buildTime[name] = `promise ${remotePromiseLoad}`;
      return acc;
    }, {});

    // TODO: buildRemotes를 사용해 두 엔드포인트를 만든다
    if (!options.isServer) {
      config.plugins.push(
        new webpack.DefinePlugin({
          "process.env.REMOTES": clientRemotes.runtime,
        })
      );
    }

이를 통해, process.env.REMOTES를 순회하여 반환값이 프라미스인 함수들이 담긴 객체를 얻을 수 있습니다. 이 프라미스들은 import('remote1/thing')과 같은 방식으로 웹팩에 등록됩니다.

공유 모듈에 대한 가이드라인

모듈 공유는 여전히 개발자들이 다루기 까다로워 보입니다. 몇 가지 공통적으로 조심해야 하는 것들을 공유합니다.

모듈 공유 시, 같은 참조 포인트로 여겨지지 않는 것들이 있습니다.

new ModuleFederationPlugin(   {
    name: 'someapp',
    filename: "static/chunks/remoteEntry.js",
    exposes: {},
    remotes: {}
    shared: {
     'myComponentLib': {singleton:true}
    },
  },
)

// 파일 안에서, 다음 줄은
import {Carousel} from 'myComponentLib'
// 다음 줄과 같지 않습니다
import Carousel from 'myComponentLib/lib/carousel

웹팩 내에서는 index.js 파일일 가능성이 높은 myComponentLib의 기본 모듈 위치를 공유할 것입니다.

만약 중첩된 import 경로를 정확히 처리하고 싶다면, 플러그인 설정에서 경로 끝에 '/'를 추가하세요.

new ModuleFederationPlugin(   {
    name: 'someapp',
    filename: "static/chunks/remoteEntry.js",
    exposes: {},
    remotes: {}
    shared: {
      // index.js 뿐만 아니라 다른 경로도 처리합니다 
     'myComponentLib/': {singleton:true}
    },
  },
)

언제 싱글톤을 사용해야 하나요?

리액트 진영에서는, 언제 싱글톤을 사용해야 하는지 알기 쉽습니다. 라이브러리가 어떤식으로든 컨텍스트, 즉 redux, 테마 프로바이더, 데이터 프로바이더를 사용하면 싱글톤을 사용해야 합니다. 이들은 모두 알아채기 제법 쉽습니다.

개발 중에 다른 페더레이트 모듈을 활성화하면 데이터가 사라지는 것처럼 애플리케이션이 이상하게 동작한다면, 아마도 중복된 싱글톤이 초기화되면서 데이터 추적이 실패하는 것일 겁니다.

무엇이 싱글톤이 되어야 하는지를 알 수 있는 기계적인 방법은 없습니다. 싱글톤을 사용하면 shareScope에 여러 버전의 모듈이 존재할 수 없게 됩니다. 취사선택하셔야 합니다.

eager는 어떻게 하죠?

저는 eager:true를 어떻게든 피하려고 합니다. next.js에서 처럼 모종의 비호환성이 있을 때만 사용합니다. eager를 참으로 하면 공유 모듈을 사전에 로드하여 사용되지 않는 라이브러리도 내려받게 됩니다. 웹팩이 eager 모듈을 별도의 파일에 분리하지 않기 때문이죠. 제2의 해결책이 있을 순 있으나, 아직 우연히 발견된 적밖에 없고, 검증되지 않았습니다.

대부분의 예시에서, bootstrap 파일을 비동기적으로 불러오는 것을 볼 수 있을 겁니다. 보통 리액트를 공유하고 있고, 파일이 마운팅 시점을 갖고 있기 때문입니다.

profile
유쾌한 동행과, 함께하는 성장을 사랑하는 Arch 리눅스 유저입니다.

0개의 댓글