avatar
Published on

Webpack Module Federation 직접해보기

Author
  • avatar
    Name
    yceffort

https://yceffort.kr/2020/09/webpack-module-federation 에서 이어진다.

webpack 5가 발표 되면서 동시에 module federation도 직접해볼 수 있게 되었다. 한번 직접 적용해보면서 정말로 게임 체인저가 될 수 있는지 살펴보자.

해당 예제 프로젝트 저장소는 여기다.

react v17과 webpack 5를 바탕으로, 아주 기초적인 세팅만 해서 빠르게 개발을 진행해보았다.

main 설정

일단 메인 프로젝트가 있고, 여기저기에 있는 컴포넌트를 가져다 쓰는 모습을 상상해보면서 프로젝트를 만들어보자. main은 module federation으로 서빙되는 다른 프로젝트를 가져다가 쓰는 federation의 중심이라고 보면 될 것 같다.

webpack.config.js

const path = require('path')

const HtmlWebpackPlugin = require('html-webpack-plugin')
const { ModuleFederationPlugin } = require('webpack').container

module.exports = {
  entry: './src/index',
  mode: 'development',
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    port: 3001,
  },
  output: {
    publicPath: 'http://localhost:3001/',
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['@babel/preset-react'],
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'main',
      remotes: {
        app1: 'app1',
      },
      shared: ['react', 'react-dom'],
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
}

ModuleFederationPlugin을 사용한 것을 볼 수 있다. 이 플러그인은 ContainerPluginContainerReferencePlugin 를 합친 개념이라고 보면 될 것 같다.

여기는 단순히 expose 한 다른 federation을 가져다 쓰는 역할만 하기 때문에, exposes를 하기 않고 있다.

app1 설정

main에서 가져다 쓸 실제 컴포넌트를 expose하는 곳이다.

webpack.config.js

const path = require('path')

const HtmlWebpackPlugin = require('html-webpack-plugin')
const { ModuleFederationPlugin } = require('webpack').container

module.exports = {
  entry: './src/index',
  mode: 'development',
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    port: 3002,
  },
  output: {
    publicPath: 'http://localhost:3002/',
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['@babel/preset-react'],
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      library: { type: 'var', name: 'app1' },
      filename: 'remoteEntry.js',
      exposes: {
        './Counter': './src/components/counter/index.jsx',
      },
      shared: ['react', 'react-dom'],
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
}

main과 차이점은 exposes가 있다는 것이다. 여기에서는 간단한 Counter를 내보내도록 하고 있다. 그리고 이렇게 내보낸 Counterhttps://localhost:3000/remoteEntry.js에서 서비스 하도록 설정해주었다.

main

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Main App</title>
  </head>

  <body>
    <div id="root"></div>
    <script src="http://localhost:3002/remoteEntry.js"></script>
  </body>
</html>

아까 서빙하기로 작성해둔 remoteEntry.js를 땡겨오는 모습이다. 물론 더 빠르게 만들기 위해서는 async 등을 사용할 수 도 있다.

bootstrap.js

이름이 bootstrap인 이유는 공식 문서에서 그렇게 하고 있길래 그렇게 했다. 👀 뜻과도 연관이 있을듯.

import React, { Suspense } from 'react'
import ReactDOM from 'react-dom'

const Counter = React.lazy(() => import('app1/Counter'))

function App() {
  return (
    <>
      <h1>Hello from React component</h1>
      <Suspense fallback="Loading Counter...">
        <Counter title={'hello, counter'} />
      </Suspense>
    </>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

Reactlazysuspense를 활용하여 app1에서 expose한 Counter를 가져다 쓰고 있다.

결과

카운터가 잘 나오고 있고

result1

정상적으로 remoteEntry에서 가져다 쓰는 것을 볼 수 있다.

result2

그리고 두 컴포넌트 모두 share['react', 'react-dom']을 쓰고 있었는데, 이것 역시 중복되지 않고 main에서 묶어서 쓰고 있는 것을 알 수 있었다.

좋은 점 내지는 기대하는 미래

요즘 유행이라고 하는 Micro Frontend를 달성할 수 있는 좋은 방법 중 하나 인 것 같다. 하나의 앱이 덩치가 너무 커서, 싱글 폴트의 위험 내지는 개발환경에서 쓸 데 없이 다 불러와야 하는 문제 등등이 존재하는데, module federation이 그것을 훌륭하게 해결해 줄 수 있을 것 같다. (물론 main이 고장나버리면 답이 없겠지만)

https://micro-frontends.org/ressources/diagrams/organisational/verticals-headline.png

아쉬운 점

문서가 잘 나와있으면 좋을 것 같은데 아직 webpack의 문서가 좀 부실한 것 같다.

그래서

를 그냥 참고 하면서 했다. 다른 여타 기능들 처럼 webpack document에서 옵션으로 들어갈 수 있는 object의 특징이나 값을 명시해주었으면 좋겠다.

https://webpack.js.org/concepts/module-federation/#containerplugin-low-level

아직은 문서가 그냥 아주 간단한 예제와 컨셉정도만 보여주고 있어서 아쉽다.

이러한 Documentation의 아쉬움 말고는 아직 이렇다할 단점을 느끼지 못했다. (물론 대규모 서비스에 직접 써보지는 않았지만) 향 후에 create-react-app이라든지, 다른 프론트엔드 생태계에서 적극적으로 사용되어서 더욱 발전해나갔으면 좋겠다.

다양한 예제들

더욱 다양한 예제들은 여기에서 볼 수 있다.