마이크로프론트엔드 with Frontend Frameworks

Dan·2022년 11월 18일
0

웹팩

목록 보기
6/8
post-thumbnail

Micro Frontend with Frontend Frameworks

마이크로 프론트엔드 아키텍쳐를 잘 활용하기 위해서 몇가지 지켜야하는 필수사항들이 있다.

  1. 컨테이너 외의 프로젝트들끼리는 공유되는게 없어야한다.
    • 함수/객체/클래스
    • 상태값(ex.redux,context)
    • 라이브러리
  2. 컨테이너와 하위 프로젝트들과 공유되는것들을 최소화해야한다.
  3. 각자의 CSS가 다른 프로젝트에 영향을 주면 안된다.
  4. 버전컨트롤(monorepo vs separate)
  5. 컨테이너가 하위 프로젝트들의 버전을 결정할 수 있어야한다.(ex.최신 Marketing을 쓸지 이전 버전의 Marketing을 쓸지)

프로젝트별로 웹팩 설정하기

개발환경, 배포환경에 따라 웹팩 설정이 다르기 떄문에 공용,개발,배포 웹팩 설정을 따로 만들어주도록 하자.

  • marketing/config/webpack.common.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-react", "@babel/preset-env"],
            plugins: ["@babel/plugin-transform-runtime"],
          },
        },
      },
    ],
  },
};
  • marketing/config/webpack.dev.js
// 공용 웹팩 설정을 현재 설정가 합칠 수 있게해준다.
const { merge } = require("webpack-merge");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const commonConfig = require("./webpack.common");

const devConfig = {
  mode: "development",
  devServer: {
    port: 8081,
    historyApiFallback: {
      index: "index.html",
    },
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
};

module.exports = merge(commonConfig, devConfig);

이제 index.html 과 index.js 를 각자 public, src 폴더를 만들어서 생성해주고 아래의 명령어를 start에 지정해준다. npm start 명령어를 입력하면 웹팩 설정 파일을 개발 설정을 바라보도록 한다는 설정이다.

  • package.json
  "scripts": {
    "start": "webpack serve --config config/webpack.dev.js"
  },

그 다음엔 마케팅 프로젝트에 리액트를 추가 해보도록 하자. 프레임워크 없이 모듈페더레이션을 했을 떄도 bootstrap.js란 파일 생성하고 index.js에서 import해서 사용했는데 그걸 그대로 이 프로젝트에도 적용시켜보자.

이제 필요한 UI 들을 기존 리액트 프로젝트에서 개발하듯이 App.js 안에서 개발하고 bootstrap.js 에서 import한다.

  • marketing/src/bootstrap.js
import React from "react";
import ReactDOM from "react-dom";
// 기존 리액트 프로젝트의 APP
import App from "./App";

// Mount function to start up the app
const mount = (el) => {
  ReactDOM.render(<App />, el);
};
// If we are in development and in isolation,
// call mount immediately
if (process.env.NODE_ENV === "development") {
  const devRoot = document.querySelector("#_marketing-dev-root");

  if (devRoot) {
    mount(devRoot);
  }
}

// We are running through container
// and we should export the mount function
export { mount };

이제 컨테이너에도 똑같은 설정을 해주지만 bootstrap 파일만 좀 다르게 설정해준다.

  • container/src/boostrap.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.querySelector("#root"));

이제 두 프로젝트를 module federation을 통해 연결해주는 설정이다. 컨테이너는 remotes, 그 외 다른 모듈들은 exposes 옵션을 통해 컨테이너에서 모듈들을 가져와서 보여줄 수 있도록 노출시켜주자. 여기서 shared 옵션을 통해 공통 모듈들을 서로 공유해서 모듈의 번들 사이즈를 줄여주도록 할 수 있다.

  • container/config/webpack.dev.js
const { merge } = require("webpack-merge");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const commonConfig = require("./webpack.common");
const packageJson = require("../package.json");

const devConfig = {
  mode: "development",
  devServer: {
    port: 8080,
    historyApiFallback: {
      index: "index.html",
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "container",
      remotes: {
        marketing: "marketing@http://localhost:8081/remoteEntry.js",
      },
      // 프로젝트간의 공통 모듈 공유
      shared: packageJson.dependencies,
    }),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
};

module.exports = merge(commonConfig, devConfig);
  • marketing/config/webpack.dev.js
const { merge } = require("webpack-merge");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const commonConfig = require("./webpack.common");
const packageJson = require("../package.json");

const devConfig = {
  mode: "development",
  devServer: {
    port: 8081,
    historyApiFallback: {
      index: "index.html",
    },
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "marketing",
      filename: "remoteEntry.js",
      exposes: {
        "./MarketingApp": "./src/bootstrap",
      },
      // 프로젝트간의 공통 모듈 공유
      shared: packageJson.dependencies,
    }),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
};

module.exports = merge(commonConfig, devConfig);

마지막으로 컨테이너의 App.js에서 marketing 프로젝트를 가져와서 보여줄 것이다.

React만 사용했던 나는 여기서 의문점이 들었다. MarketingApp을 왜 바로 component형태 노출시키지 않고 mount같이 함수 형태로 노출시키는 이유는 뭘까?
이유는 단순하다, jsx형식으로 component를 불러와 render하는 방식은 오직 React라는 라이브러리에서만 사용하는 방식이다, micro-frontend을 제대로 활용하기 위해서는 Vue, Angular 같은 다양한 프레임워크를 한 프로젝트로 합쳐서 보여줘야한다. 그렇기위해서는 jsx형식이 아닌 다른 프로젝트에서도 쉽게 적용될 수 있는 함수형태로 노출을 시켜서 받아오는 것이다.

  • container/src/components/MarketingApp.js
import { mount } from "marketing/MarketingApp";
import React, { useRef, useEffect } from "react";

export default () => {
  const ref = useRef(null);

  useEffect(() => {
    mount(ref.current);
  }, []);

  return <div ref={ref}></div>;
};
  • container/src/App.js
import React from "react";
import MarketingApp from "./components/MarketingApp";

export default () => {
  return (
    <div>
      <h1>Hi there</h1>
      <hr />
      <MarketingApp />
    </div>
  );
};

이제 npm start를 해보면 결과물이 잘 나오는것을 확인 할 수 있다. 이걸로 제일 기본적인 monorepo형식의 mfa를 구현해보았다.

profile
만들고 싶은게 많은 개발자

0개의 댓글