avatar
Published on

Webpack Module Federation에 대해 알아보자

Author
  • avatar
    Name
    yceffort

이 글을 위주로 번역한 글이며, 추가적으로 micro frontend에 대한 개념도 넣어두었습니다.

Table of Contents

federated application 만들어보기

./src/Appapp_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 옵션에 나와있듯, remotehost의 의존성에 의존하게 된다. 만약 호스트에 해당 의존성이 존재하지 않는다면, 리모트는 알아서 다운로드 할 것이다.

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에 대한 다양한 예제를 아래에서 살펴볼 수 있다.

결과적으로 하나의 큰 애플리케이션을 여러개의 독립된 애플리케이션으로 만든 다음, 다이나믹 로딩을 하듯이 필요한 순간에 필요한 컴포넌트 (소스)를 불러오게 한다는 개념인 것 같다. webpack5 에 포함될 예정이라고 하니, 정식 출시 될 때 실제 동작하는 예제를 만들어보고 고민해봐야겠다.