- Published on
 
Webpack Module Federation에 대해 알아보자
- Author
 - Name
 - yceffort
 
이 글을 위주로 번역한 글이며, 추가적으로 micro frontend에 대한 개념도 넣어두었습니다.
Table of Contents
federated application 만들어보기
./src/App을 app_one_remote라고 선언하였다. 이는 다른 애플리케이션에서 실행될 수 있다.
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
module.exports = {
  // other webpack configs...
  plugins: [
    new ModuleFederationPlugin({
      name: 'app_one_remote',
      remotes: {
        app_two: 'app_two_remote',
        app_three: 'app_three_remote',
      },
      exposes: {
        AppContainer: './src/App',
      },
      shared: ['react', 'react-dom', 'react-router-dom'],
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
      chunks: ['main'],
    }),
  ],
}
애플리케이션 헤드에, app_one_remote.js를 불러오도록 했다. 이렇게 하면 다른 웹팩 런타임에 연결되고, 런타임에 오케이스트레이션 계층을 프로비저닝 할 수 있다. 이는 특별히 설계된 웹팩 런타임과 진입점이다. 이는 일반적인 애플리케이션 진입점과 다르게, 몇 kb에 불과하다.
<head>
  <script src="http://localhost:3002/app_one_remote.js"></script>
  <script src="http://localhost:3003/app_two_remote.js"></script>
</head>
<body>
  <div id="root"></div>
</body>
App One에서 App Two에 있는 코드를 사용하고 싶다면,
const Dialog = React.lazy(() => import('app_two_remote/Dialog'))
const Page1 = () => {
  return (
    <div>
      <h1>Page 1</h1>
      <React.Suspense fallback="Loading Material UI Dialog...">
        <Dialog />
      </React.Suspense>
    </div>
  )
}
export default Page1
라우터는 일반적인 표준과 비슷하다.
import { Route, Switch } from 'react-router-dom'
import Page1 from './pages/page1'
import Page2 from './pages/page2'
import React from 'react'
const Routes = () => (
  <Switch>
    <Route path="/page1">
      <Page1 />
    </Route>
    <Route path="/page2">
      <Page2 />
    </Route>
  </Switch>
)
export default Routes
App Two에서는 Dialog를 내보낼 것이며, 이는 위에서 봤던 것 처럼 App One에서 사용한다.
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app_two_remote',
      filename: 'remoteEntry.js',
      exposes: {
        Dialog: './src/Dialog',
      },
      remotes: {
        app_one: 'app_one_remote',
      },
      shared: ['react', 'react-dom', 'react-router-dom'],
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
      chunks: ['main'],
    }),
  ],
}
루트 앱은 이런 모양이다.
import React from 'react'
import Routes from './Routes'
const AppContainer = React.lazy(() => import('app_one_remote/AppContainer'))
const App = () => {
  return (
    <div>
      <React.Suspense fallback="Loading App Container from Host">
        <AppContainer routes={Routes} />
      </React.Suspense>
    </div>
  )
}
export default App
import React from 'react'
import { ThemeProvider } from '@material-ui/core'
import { theme } from './theme'
import Dialog from './Dialog'
function MainPage() {
  return (
    <ThemeProvider theme={theme}>
      <div>
        <h1>Material UI App</h1>
        <Dialog />
      </div>
    </ThemeProvider>
  )
}
export default MainPage
App Three의 경우, <App>에서 실행되는 것이 없이 독립되어 있으므로, 아래와 같이 처리하면 된다.
new ModuleFederationPlugin({
  name: "app_three_remote",
  library: { type: "var", name: "app_three_remote" },
  filename: "remoteEntry.js",
  exposes: {
    Button: "./src/Button"
  },
  shared: ["react", "react-dom"]
}),
트위터에 제작자가 공유해준 실제 코드를 살펴보자.
네트워크 탭을 살펴보면, 세 코드가 모두 다른 번들에 존재하고 있음을 알 수 있다.
의존성 중복이 존재하지 않는다. shared 옵션에 나와있듯, remote는 host의 의존성에 의존하게 된다. 만약 호스트에 해당 의존성이 존재하지 않는다면, 리모트는 알아서 다운로드 할 것이다.
vendor나 다른 모듈을 shared에 추가하는 것은 확장성에 그다지 좋지 못하다. AutomaticModuleFederationPlugin를 제공하여, 웹팩 코어 외부에 있는 코드들을 관리할 수 있도록 할 것이다.
Server Side Rendering
Module Federation은 브라우저 node 모든 환경에서 동작한다. 단지 서버 빌드가 commonjs 라이브러리 타겟을 사용하기만 하면 된다.
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'container',
      library: { type: 'commonjs-module' },
      filename: 'container.js',
      remotes: {
        containerB: '../1-container-full/container.js',
      },
      shared: ['react'],
    }),
  ],
}
Module Federation에 대한 다양한 예제를 아래에서 살펴볼 수 있다.
- https://github.com/module-federation/module-federation-examples
 - https://github.com/module-federation/next-webpack-5
 - https://github.com/ScriptedAlchemy/mfe-webpack-demo
 - https://github.com/ScriptedAlchemy/webpack-external-import
 
결과적으로 하나의 큰 애플리케이션을 여러개의 독립된 애플리케이션으로 만든 다음, 다이나믹 로딩을 하듯이 필요한 순간에 필요한 컴포넌트 (소스)를 불러오게 한다는 개념인 것 같다. webpack5 에 포함될 예정이라고 하니, 정식 출시 될 때 실제 동작하는 예제를 만들어보고 고민해봐야겠다.