루트 프로젝트 생성

➜ mkdir module-federation-isolated-example

➜ cd module-federation-isolated-example

➜ pnpm init

➜ corepack use [email protected]

➜ code .
# module-federation-isolated-example/pnpm-workspace.yaml

packages:
  - "apps/*"

2개의 워크스페이스 생성

➜ mkdir apps

➜ cd apps

➜ pnpm create mf-app # (main-app, 3000)

➜ pnpm create mf-app # (isolated-app, 3001)

➜ cd ...

➜ pnpm i
{
  "name": "main-app",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack --mode production",
    "build:dev": "webpack --mode development",
    "build:start": "cd dist && PORT=3000 npx serve",
    "start": "webpack serve --open --mode development",
    "start:live": "webpack serve --open --mode development --live-reload --hot"
  },
  "license": "MIT",
  "author": {
    "name": "Jack Herrington",
    "email": "[email protected]"
  },
  "devDependencies": {
    "@babel/core": "^7.15.8",
    "@babel/plugin-transform-runtime": "^7.15.8",
    "@babel/preset-env": "^7.15.8",
    "@babel/preset-react": "^7.14.5",
    "@babel/preset-typescript": "^7.10.4",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "autoprefixer": "^10.1.0",
    "babel-loader": "^8.2.2",
    "css-loader": "^6.3.0",
    "html-webpack-plugin": "^5.3.2",
    "postcss": "^8.2.1",
    "postcss-loader": "^4.1.0",
    "style-loader": "^3.3.0",
    "typescript": "^4.5.2",
    "webpack": "^5.57.1",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.3.1"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}
{
  "name": "isolated-app",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack --mode production",
    "build:dev": "webpack --mode development",
    "build:start": "cd dist && PORT=3001 npx serve",
    "start": "webpack serve --open --mode development",
    "start:live": "webpack serve --open --mode development --live-reload --hot"
  },
  "license": "MIT",
  "author": {
    "name": "Jack Herrington",
    "email": "[email protected]"
  },
  "devDependencies": {
    "@babel/core": "^7.15.8",
    "@babel/plugin-transform-runtime": "^7.15.8",
    "@babel/preset-env": "^7.15.8",
    "@babel/preset-react": "^7.14.5",
    "@babel/preset-typescript": "^7.10.4",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "autoprefixer": "^10.1.0",
    "babel-loader": "^8.2.2",
    "css-loader": "^6.3.0",
    "html-webpack-plugin": "^5.3.2",
    "postcss": "^8.2.1",
    "postcss-loader": "^4.1.0",
    "style-loader": "^3.3.0",
    "typescript": "^4.5.2",
    "webpack": "^5.57.1",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.3.1"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}
➜ pnpm i

isolated-app 워크스페이스

// module-federation-isolated-example/apps/isolated-app/src/index.ts

import("./bootstrap");
// module-federation-isolated-example/apps/isolated-app/src/bootstrap.tsx

import App from "./App";
import React from "react";
import { createRoot } from "react-dom/client";

createRoot(document.getElementById("app")!).render(<App name="isolated" />);
// module-federation-isolated-example/apps/isolated-app/src/App.jsx

import React from "react";

import "./index.css";

export interface AppProps {
  name: string;
}

const App: React.FC<AppProps> = ({ name }) => (
  <div className="container">
    <div>Name: isolated-app</div>
    <div>Framework: react</div>
    <div>Language: TypeScript</div>
    <div>CSS: Empty CSS</div>
    <div>{name}</div>
  </div>
);

export default App;
// module-federation-isolated-example/apps/isolated-app/src/injector.jsx

import App, { AppProps } from "./App";
import React from "react";
import { createRoot } from "react-dom/client";

export const inject = (parentElementId: string, props: AppProps) => {
  const root = createRoot(document.getElementById(parentElementId)!);
  root.render(<App {...props} />);

  return () => {
    root.unmount();
  };
};
// module-federation-isolated-example/apps/isolated-app/webpack.config.js

const HtmlWebPackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

const deps = require("./package.json").dependencies;
module.exports = (_, argv) => ({
  output: {
    publicPath: "<http://localhost:3001/>",
  },

  resolve: {
    extensions: [".tsx", ".ts", ".jsx", ".js", ".json"],
  },

  devServer: {
    port: 3001,
    historyApiFallback: true,
  },

  module: {
    rules: [
      {
        test: /\\.m?js/,
        type: "javascript/auto",
        resolve: {
          fullySpecified: false,
        },
      },
      {
        test: /\\.(css|s[ac]ss)$/i,
        use: ["style-loader", "css-loader", "postcss-loader"],
      },
      {
        test: /\\.(ts|tsx|js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
        },
      },
    ],
  },

  plugins: [
    new ModuleFederationPlugin({
      name: "isolated_app",
      filename: "remoteEntry.js",
      remotes: {},
      exposes: {
        "./injector": "./src/injector.tsx",
      },
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      },
    }),
    new HtmlWebPackPlugin({
      template: "./src/index.html",
    }),
  ],
});
➜ pnpm --filter isolated-app start:live

http://localhost:3001/

http://localhost:3001/remoteEntry.js

main-app 워크스페이스

// module-federation-isolated-example/apps/main-app/src/App.tsx

import React, { useEffect } from "react";
import { createRoot } from "react-dom/client";

import "./index.css";

const elementId = "isolated-app";

const App = () => {
  useEffect(() => {
    let unmount: () => void = () => {};
    import("isolated_app/injector").then(({ inject }) => {
      unmount = inject(elementId, { name: "main" });
    });
    return () => {
      unmount();
    };
  }, []);

  return (
    <div className="container">
      <div>Name: main-app</div>
      <div>Framework: react</div>
      <div>Language: TypeScript</div>
      <div>CSS: Empty CSS</div>
      <div id={elementId}></div>
    </div>
  );
};
createRoot(document.getElementById("app")!).render(<App />);
// module-federation-basic-example/apps/main-app/webpack.config.js

const HtmlWebPackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

const deps = require("./package.json").dependencies;
module.exports = (_, argv) => ({
  output: {
    publicPath: "<http://localhost:3000/>",
  },

  resolve: {
    extensions: [".tsx", ".ts", ".jsx", ".js", ".json"],
  },

  devServer: {
    port: 3000,
    historyApiFallback: true,
  },

  module: {
    rules: [
      {
        test: /\\.m?js/,
        type: "javascript/auto",
        resolve: {
          fullySpecified: false,
        },
      },
      {
        test: /\\.(css|s[ac]ss)$/i,
        use: ["style-loader", "css-loader", "postcss-loader"],
      },
      {
        test: /\\.(ts|tsx|js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
        },
      },
    ],
  },

  plugins: [
    new ModuleFederationPlugin({
      name: "main_app",
      filename: "remoteEntry.js",
      remotes: {
        isolated_app: "isolated_app@<http://localhost:3001/remoteEntry.js>",
      },
      exposes: {},
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      },
    }),
    new HtmlWebPackPlugin({
      template: "./src/index.html",
    }),
  ],
});