마이크로 프론트엔드 - shared 옵션이란?

ModuleFederationPlugin의 shared 옵션을 통해 마이크로 앱들이 런타임에 사용하는 여러 라이브러리를 어떻게 공유해서 사용할 것인지 설정할 수 있다.

 

패키지 설치하기

먼저 아래의 명령어를 통해 main-app과 component-app에 lodash와 @types/lodash를 설치해주도록 하자

pnpm --filter main-app add lodash@4.17.21   
pnpm --filter main-app add @types/lodash -D

pnpm --filter component-app add lodash@4.17.21
pnpm --filter component-app add @types/lodash -D

 

이렇게 되면 main-app과 component-app 모두 같은 버전의 lodash가 설치된다.

 

컴포넌트 변경하기

다음과 같이 component-app의 Button.tsx에 lodash를 추가해주도록 하자.

import { ButtonHTMLAttributes, PropsWithChildren } from "react";
import { join, map } from "lodash";

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "warning" | "primary";
}

export default function Button({
  variant = "primary",
  children,
  onClick,
}: PropsWithChildren<ButtonProps>) {
  const buttonType = variant === "warning" ? "warning" : "primary";

  return (
    <button style={styleMapping[buttonType]} onClick={onClick}>
      {children} {join(map(["1", "2"]), "-")}
    </button>
  );
}

const styleMapping = {
  primary: {
    marginLeft: "10px",
    color: "#fff",
    backgroundColor: "#409eff",
    borderColor: "#409eff",
    padding: "12px 20px",
    fontSize: "14px",
    borderRadius: "4px",
    outline: "none",
    border: "1px solid #dcdfe6",
    cursor: "pointer",
  },
  warning: {
    marginLeft: "10px",
    color: "#fff",
    backgroundColor: "#e6a23c",
    borderColor: "#e6a23c",
    padding: "12px 20px",
    fontSize: "14px",
    borderRadius: "4px",
    outline: "none",
    border: "1px solid #dcdfe6",
    cursor: "pointer",
  },
};

 

그리고 main-app의 App.tsx에도 lodash를 사용해주도록 하자

import React from "react";
import ReactDOM from "react-dom/client";
import Button from "component_app/Button";
import { join, map } from "lodash";

import "./index.css";

const App = () => (
  <div className="mt-10 text-3xl mx-auto max-w-6xl">
    <div>Name: main-app</div>
    <div>Framework: react-18</div>
    <div>{join(map(["1", "2"]), "-")}</div>
    <Button
      onClick={() => {
        alert("Clicked");
      }}
    >
      Primary
    </Button>
    <Button variant="warning">Warning</Button>
  </div>
);

const root = ReactDOM.createRoot(document.getElementById("app") as HTMLElement);
root.render(<App />);

 

Module Federation shared 설정

 

먼저 각 마이크로 앱 별 rspack.config 설정을 보도록 하자

// main-app rspack.config.ts
const deps = require("./package.json").dependencies;

new ModuleFederationPlugin({
  name: "main_app",
  exposes: {},
  remotes: {
    component_app: "component_app@http://localhost:3001/remoteEntry.js",
  },
  shared: {
    ...deps,
    react: {
      singleton: true,
      requiredVersion: deps.react,
    },
    "react-dom": {
      singleton: true,
      requiredVersion: deps["react-dom"],
    },
  },
}),

// component-app rspack.config.ts
const deps = require("./package.json").dependencies;

new ModuleFederationPlugin({
  name: "component_app",
  filename: "remoteEntry.js",
  exposes: {
    "./Button": "./src/components/Button",
  },
  shared: {
    ...deps,
    react: {
      singleton: true,
      requiredVersion: deps.react,
    },
    "react-dom": {
      singleton: true,
      requiredVersion: deps["react-dom"],
    },
  },
}),

 

이렇게 설정을 한채로 main-app과 component-app을 실행해보도록 하자.

 

두 앱의 lodash의 버전이 같기 때문에 네트워크에서 하나의 lodash만 가져오게 된다.

 

이번에는 다른 방식으로 shared 옵션을 설정해보도록 하자.

// component-app rspack.config.ts
 new ModuleFederationPlugin({
  name: "component_app",
  filename: "remoteEntry.js",
  exposes: {
    "./Button": "./src/components/Button",
  },  
  shared: ["lodash"],
}),

// main-app rspack.config.ts
new ModuleFederationPlugin({
  name: "main_app",
  exposes: {},
  remotes: {
    component_app: "component_app@http://localhost:3001/remoteEntry.js",
  },
  shared: ["lodash"],
}),

 

이번에도 두 마이크로앱의 패키지 버전이 같기 때문에 하나의 lodash만 받아오게 된다.

 

다른 버전의 lodash 패키지 설치

component-app의 lodash 버전을 다음과 같이 낮춰서 재설치해보도록 하자

pnpm --filter component-app add lodash@4.17.20

 

이후 마이크로 앱들을 재시작하면 다음과 같이 다른 버전의 두 라이브러리를 한 번의 네트워크 요청 시 받아오게 된다.

 

이번에는 component-app의 package.json의 lodash 버전에 ^을 붙여 시맨틱 버전으로 만들어주도록 하자. 변경 후 재설치를 하지 않은 채로 서버를 재시작해보도록 하자

"lodash": "^4.17.20",

 

실제 component-app에 설치된 버전은 4.17.20이지만 네트워크상 4.17.21 버전의 lodash만 전송되는 것을 확인할 수 있다.

 

즉, 배열로만 shared를 설정한 경우에는 package.json의 시맨틱 버전에 의존하게 된다.

 

이번에는 객체 형태로 shared 옵션을 설정해보도록 하자.

먼저 다시 component-app의 lodash를 4.17.21로 설치하도록 한다.

 pnpm --filter component-app add lodash@4.17.21

 

그리고 각 마이크로 앱의 rspack 설정을 다음과 같이 수정하도록 하자.

// main-app rspack.config.ts
new ModuleFederationPlugin({
  name: "main_app",
  exposes: {},
  remotes: {
    component_app: "component_app@http://localhost:3001/remoteEntry.js",
  },
  shared: {
    lodash: "4.17.21",
  },
})

// component-app rspack.config.ts
new ModuleFederationPlugin({
  name: "component_app",
  filename: "remoteEntry.js",
  exposes: {
    "./Button": "./src/components/Button",
  },
  shared: {
    lodash: "4.17.21",
  },
}),

 

그럼 이전처럼 동일한 버전(4.17.21)의 lodash를 한 번만 응답받는 것을 확인할 수 있다.

 

이번에는 component-app의 package.json은 그대로 두고, shared 옵션의 lodash 버전만 낮춰보도록 하자

shared: {
  lodash: "4.17.20",
},

 

이렇게 하면 main-app의 shared에 명시된 lodash 버전은 4.17.21이고, component-app의 버전은 4.17.20이 된다. 이 상태로 두 마이크로 앱을 다시 시작해보자.

위처럼 각 마이크로 앱이 다른 버전을 사용한다고 명시되어있으므로 두 마이크로 앱에 필요한 lodash를 각각 받게 된다. 그러나 component-app에 설치된 lodash 버전은 4.17.21이므로 4.17.21 버전의 lodash를 받게 된다.

 

이번에는 component-app에 4.17.20버전의 lodash를 설치해보도록 하자.

pnpm --filter component-app add lodash@4.17.20

 

그러고 나서 마이크로 앱을 다시 시작하면 다음과 같이 4.17.21 버전과 4.17.20 버전의 lodash를 받게 되는 것을 볼 수 있다.

 

 

이번에는 component-app의 shared 옵션에 `^`을 추가하여 시맨틱 버전으로 명시해보도록 하자

shared: {
  lodash: "^4.17.20",
},

 

설정 후, 마이크로 앱을 다시 시작한다면 4.17.21 버전의 lodash를 받아오게 된다. 

 

이번에는 좀 더 복잡한 shared 옵션을 적용해보도록 할 것이다.

component-app의 shared 옵션만 변경해주도록 하자.

// main-app rspack.config.ts
new ModuleFederationPlugin({
  name: "main_app",
  exposes: {},
  remotes: {
    component_app: "component_app@http://localhost:3001/remoteEntry.js",
  },
  shared: {
    lodash: "4.17.21",
  },
}),

// component-app rspack.config.ts

new ModuleFederationPlugin({
  name: "component_app",
  filename: "remoteEntry.js",
  exposes: {
    "./Button": "./src/components/Button",
  },
  shared: {
    lodash: {
      requiredVersion: "4.17.20"
    }
  }
}),

 

변경 후, 마이크로 앱을 재실행하면 두 버전의 lodash를 받게 된다.

 

singleton 옵션을 true로 변경해보도록 하자.

shared: {
  lodash: {
    requiredVersion: "4.17.20",
    singleton: true,
  },
},

singleton을 true로 변경하면 하나의 lodash 버전을 받게 된다. 

다만 4.17.20 버전과 4.17.21 버전은 엄연히 코드가 다를 것이고, 이를 하나로 관리하는 것은 오류 발생 가능성을 높이기 때문에 콘솔에 다음과 같은 경고 문구를 띄워준다. 즉 requiredVersion이 다른데, singleton으로 관리하는 경우 다음과 같은 문구를 볼 수 있다.

 

 

이번에는 strictVersion 옵션을 true로 설정해보도록 하자.

shared: {
  lodash: {
    requiredVersion: "4.17.20",
    singleton: true,
    strictVersion: true,
  },
},

 

그러고 마이크로 앱을 다시 실행하면, 4.17.21 버전의 lodash를 받지만 화면이 뜨지 않는다. 

그리고 콘솔을 보게 되면 에러 문구가 뜬다.

 

 

 

이번에는 shareScope 옵션을 내가 설정한 scope 문자열(chat)로 변경해보도록 하자. 기본값은 default이다.

shared: {
  lodash: {
    requiredVersion: "4.17.20",
    singleton: true,
    strictVersion: true,
    shareScope: "chat",
  },
},

 

main-app의 scope(default)과 component-app(chat)의 scope는 달라졌으므로 다른 버전의 lodash를 받아오게 되는 것을 볼 수 있다.

 

 

결론

shared 옵션을 사용하는 방법은 3가지이다.

  • `["lodash"]` 처럼 라이브러리명을 배열에 넣으면 package.json에 있는 버전(시맨틱 버전 포함)으로 공유해서 사용할 라이브러리의 기준이 정해진다.
  • `{lodash: "4.17.21"}`과 같이 라이브러리와 버전을 키과 값으로 지정하면 package.json의 버전과 관계없이 함께 공유해서 사용할 라이브러리의 기준을 정할 수 있다.
  • `{lodash: { requiredVersion: "4.17.20", singleton: true }}`와 같이 라이브러리명을 키로 값이 객체 형태로 지정하면 복잡한 설정을 할 수 있다.