【入門編】Webpackで実現する『Module Federation』のバージョン競合戦略:共有パッケージの依存関係を制御しランタイムエラーを回避する – ビルド・パッケージ管理ツール生産性向上バイブル

こんにちは!フロントエンド開発の現場で、日々ビルドやパッケージ管理の最適化に頭を悩ませていませんか?

「複数のマイクロフロントエンドアプリを結合したら、なぜかReactのフックでエラーが起きた…」
「別チームが管理するコンポーネントを読み込んだら、バージョンの不一致でバンドルサイズが巨大化した…」

そんな絶望的な状況に直面したことはありませんか?今回は、Webpackの真骨頂である Module Federation において、複数チームの大規模開発で確実に問題になる「バージョン競合とランタイムエラーの回避戦略」を、現場の知見を交えて徹底的に解説します。

これをマスターすれば、バラバラのチームが独立してデプロイしながらも、ブラウザのランタイム上で完璧に依存関係調停を行い、無駄な重複ダウンロードを防ぐ洗練されたアーキテクトになれますよ。毎日の開発が劇的に楽になる感覚を、一緒に味わっていきましょう!

—

そもそも Module Federation とは?なぜバージョン競合が起きるのか

Module Federation(モジュールフェデレーション)は、Webpack 5で導入された革命的な機能です。これまでは1つのアプリケーションとして一体でビルドしていたものを、「完全に独立してビルド・デプロイされた複数のSPA(シングルページアプリケーション)同士が、ブラウザのランタイム上でリアルタイムにJavaScriptのコードを読み込み合う」 ことが可能になります。

例えば、以下のような構成を考えてみてください。

  • Host(ホストアプリ): 全体のレイアウトやルーターを管理する親アプリ
  • Remote(リモートアプリA): 「商品詳細機能」を担当する子アプリ
  • Remote(リモートアプリB): 「カート機能」を担当する子アプリ

ここで問題になるのが 「依存関係(dependencies)の重複」 です。HostもRemote AもRemote Bも、共通して `react` や `react-dom` を使いますよね。もし、それぞれが勝手に `react` の異なるバージョンをバンドルしてしまったらどうなるでしょうか?

1. ブラウザに巨大な `react` が3つもロードされ、ネットワーク帯域とメモリを無駄に食う。
2. さらに最悪なことに、Reactの内部状態(Contextなど)がインスタンスごとに分離してしまい、「Invalid hook call」などの致命的なランタイムエラーを引き起こします。

この地獄を回避するのが、Webpackの `shared` オプションによるバージョン制御です。

—

基礎セットアップ:安全な Module Federation の土台を作る

理屈はこれくらいにして、実際に動く最小限の環境を作りながら、その仕組みを体得していきましょう。今回はシンプルに、Host(親) と Remote(子) の2つのプロジェクトを想定し、Webpackの設定を紐解きます。

1. 最小構成のディレクトリ構造

今回は、モノレポまたは別々のディレクトリにあると仮定し、Host側の設定にフォーカスします。まずは、最も堅牢な `webpack.config.js` の設定を見てください。

2. ホスト側(Host)の `webpack.config.js`

const HtmlWebpackPlugin = require(‘html-webpack-plugin’);
const { ModuleFederationPlugin } = require(‘webpack’).container;
const path = require(‘path’);

module.exports = {
mode: ‘development’,
entry: ‘./src/index.js’,
output: {
publicPath: ‘http://localhost:3000/’, // ホスト自体の配信オリジン
clean: true,
},
devServer: {
port: 3000,
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: ‘babel-loader’,
exclude: /node_modules/,
options: {
presets: [‘@babel/preset-react’],
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: ‘host_app’,
// リモート側から読み込むモジュールの定義
remotes: {
remoteApp: ‘remote_app@http://localhost:3001/remoteEntry.js’,
},
// 超重要:共有パッケージの定義
shared: {
react: {
singleton: true, // ブラウザ上で単一のインスタンスのみを強制する
strictVersion: true, // 指定バージョンが一致しない場合にビルドエラー/警告を発生させる
requiredVersion: ‘^18.2.0’, // 許容するバージョンの範囲
},
‘react-dom’: {
singleton: true,
strictVersion: true,
requiredVersion: ‘^18.2.0’,
},
},
}),
new HtmlWebpackPlugin({
template: ‘./public/index.html’,
}),
],
};

ここがプロのこだわりポイント!設定の急所解説

  • `singleton: true`:

これが最大の命綱です。複数のマイクロフロントエンドが同じパッケージを要求した際、ブラウザのメモリ上に「たった1つのインスタンス」だけをロードし、それを共有させます。これにより、ReactのContextやHooksの多重インスタンス化によるバグを根絶します。

  • `strictVersion: true`:

「バージョンが少しぐらい違っても動くやろ」という甘い期待を断ち切る設定です。要求されたバージョン(`requiredVersion`)と、実際に提供されるバージョンのセマンティックバージョニング(SemVer)が一致しない場合、ランタイムで警告を出したり、フォールバック挙動を厳密にコントロールします。

  • `requiredVersion`:

チーム間で「我が社のマイクロフロントエンド群は、React 18.2系をベースにする」という合意をコードレベルで強制力を持たせます。

—

精度高い HelloWorld 的な動作確認:依存関係の衝突をシミュレートする

それでは、この設定がどのように機能しているのか、ローカル環境での動作確認をしてみましょう。

Step 1: リモート側の用意

別のポート(3001)で動くRemoteアプリ側でも、同様に `shared` セクションで `react` を共有設定しておきます。

// remote側の webpack.config.js の抜粋
plugins: [
new ModuleFederationPlugin({
name: ‘remote_app’,
filename: ‘remoteEntry.js’,
exposes: {
‘./Button’: ‘./src/Button’,
},
shared: {
react: { singleton: true, requiredVersion: ‘^18.2.0’ },
‘react-dom’: { singleton: true, requiredVersion: ‘^18.2.0’ },
},
}),
]

Step 2: ホスト側からリモートコンポーネントを呼び出す

ホスト側のコード(`src/App.jsx`)で、リモートアプリのコンポーネントを非同期インポート(Dynamic Import)します。

import React, { Suspense } from ‘react’;

// Module Federation経由でリモートのコンポーネントを遅延ロード
const RemoteButton = React.lazy(() => import(‘remoteApp/Button’));

const App = () => {
return (

メインホストアプリケーション (Port: 3000)

以下のボタンは、別サーバー(Port: 3001)から動的に読み込まれています。

Loading Remote Button…

}>

);
};

export default App;

Step 3: 実行とブラウザでの検証

それぞれの開発サーバーを起動します。

Terminal 1 (Remote App)
npm run start — –port 3001

Terminal 2 (Host App)
npm run start — –port 3000

ブラウザで `http://localhost:3000` にアクセスしてください。「Loading Remote Button…」の瞬間の後、リモート側でレンダリングされたボタンが綺麗に表示されるはずです。

ここでブラウザの Developer Tools(開発者ツール)を開き、Networkタブを確認してみてください。
`react` や `react-dom` が、Host側とRemote側で重複してダウンロードされていない(一度しかフェッチされていない)ことが確認できます。これが、Module Federationにおける依存関係制御の美しさです。

—

大規模チーム運用で知っておくべき「現場の知見(裏技)」

最後に、プロダクション環境で何十人ものエンジニアが同時にコードを書き、バラバラにデプロイし合う現場で生き残るための実践的なアドバイスを授けます。

1. `requiredVersion` は厳格にしすぎない(caret rangeを活用する)
すべてのチームに全く同一のパッチバージョン(例: `18.2.0`)を強制すると、セキュリティパッチの当て逃げなどでデプロイが膠着します。基本的には `^18.2.0` のようにセマンティックバージョニングの互換性を活かせるレンジを指定し、メジャーバージョンの不一致だけを弾く設計にするのが、組織の機動力と安全性を両立するコツです。
2. バージョン不一致時のフォールバック(Eager Loadの罠)
もしどうしてもバージョンが一致しない外部パッケージを共有しなければならない場合、Webpackはコンソールに警告を出して、ホスト側のバージョンを強制適用しようとします。予期せぬ挙動を防ぐため、CI/CDパイプラインのビルドステップで、各マイクロフロントエンドの `package.json` の依存関係を静的解析するLinterスクリプトを組み込んでおくと、人災によるバージョン競合を完全に防げます。

—

まとめ

今回は、Webpackの Module Federation におけるパッケージバージョンの競合戦略について、理論と実践的な設定コードを交えて解説しました。

  • `singleton: true` でブラウザ上のインスタンスを単一化し、HooksやContextの衝突を防ぐ。
  • `strictVersion: true` と `requiredVersion` で、組織的なバージョンの不一致をシステムレベルでブロックする。

この仕組みを理解し、適切にプロジェクトに組み込むことができれば、マイクロフロントエンドの管理コストは劇的に下がります。
「大規模化しても怖くないアーキテクチャ」を手に入れたあなたの毎日のコーディングが、より楽しく、自信に満ちたものになることを心から応援しています!

タイトルとURLをコピーしました