流行語で終わらせないマイクロフロントエンド
Module Federationとsingle-spaの役割、独立したデプロイの代償、そしてシンプルなフロントエンドを選ぶべき場面。
目次
リモートモジュールが読み込めたら、最初の一歩としては順調です。ただし、それだけでは複数のチームが安全に一つの製品をつくれるかは分かりません。
私の本番環境での経験は、主にWebpack Module Federationを使った業務基盤です。ホストと、それぞれに担当がいるフロントエンドモジュールで構成されていました。難しかったのは、共有する依存関係、ナビゲーションの担当、認証情報の渡し方、本番障害に関わるバージョンの特定といった、境界に関する問いでした。
この記事では、その経験をsingle-spaの公式文書にあるアプリケーションモデルと比較します。single-spaを本番運用した経験として紹介するものではありません。二つのツールは同じ課題の異なる部分を扱うため、比較する意味があります。
チームの境界から考える
取引、請求、ユーザー管理、設定を含む業務アプリを考えてみます。最初は一つのフロントエンドと一つのリリース手順で十分かもしれません。一つのリポジトリ内でも、モジュールの境界が明確なら長く運用できます。
各領域を別々のチームが担当するようになると、リリースのたびに調整する負担が大きくなります。マイクロフロントエンドは、担当範囲が明確な小さな単位から製品を構成し、デプロイの仕組みが対応していれば、別々にリリースできるようにする方法です。
それでも利用者が期待するのは、一つのアプリです。移動、ログイン、エラーからの復帰のために、社内のチーム構成を理解する必要はありません。
ここにトレードオフがあります。リリース時のチーム間依存を減らす代わりに、独立して変化する部分の互換性を保つ仕事が増えます。リポジトリを分けるだけでは解決しません。共有状態、密結合したAPI、曖昧なリリース規則があれば、チーム間の依存は残ります。
Module Federationはコードを読み込み、single-spaはアプリを調整する
Module Federationでは、独立してビルドしたアプリ同士が、実行時にモジュールを提供・利用できます。ホストはリモートのエントリーを読み込み、自身のビルドに実装を含めずに、公開された部品やアプリを利用できます。単位はアプリ全体でも小さなウィジェットでも構いません。WebpackのModule Federation概要で説明されています。
ホストが適切なリモートを見つけられ、インターフェースの互換性が保たれていれば、別々のデプロイが可能になります。ただし、ルートの担当範囲や標準のマウント・アンマウント手順は定義しません。周囲の設計で決める必要があります。
single-spaは、読み込み関数と有効化条件を持つアプリを登録し、bootstrap、mount、unmountを調整します。URLに基づく有効化が一般的で、複数アプリを同時に表示することもできます。UIやリソースの後片付けは、各アプリのunmount実装が担当します。single-spaのライフサイクル文書が参考になります。
例えば、ナビゲーションを表示したまま、/billingで請求アプリを有効化できます。異なる経緯で育ったアプリや、移行中に複数フレームワークを共存させる場面で役立つモデルです。
二つの役割を組み合わせることもできます。
single-spaの有効化条件
→ 読み込み関数がフェデレーションのモジュールを取得
→ モジュールがアプリのライフサイクル関数を提供
→ single-spaがアプリをマウント
→ アンマウント時にアプリが後片付けただし、接続する実装は必要です。任意の部品を読み込むだけでsingle-spaアプリになるわけではありません。single-spaの構成ガイドでは、他の読み込み方法とともにModule Federationも説明されています。既存ホストがナビゲーションとライフサイクルを十分に管理していれば、調整用の仕組みを追加する必要はないかもしれません。
難しさその1:共有する依存関係は契約になる
ホストとリモートが両方Reactを使っていても、それだけでは互換性の契約として不十分です。
React 18のホストと、React 16の前提でつくられた古いリモートを考えます。ランタイムを一つに決めても、API、描画処理、ライブラリの前提が自動的に一致するわけではありません。別のランタイムを使う場合も境界の設計が必要で、部品やコンテキストをそのまま共有できるとは限りません。
Webpackのsingletonは、共有スコープ内で一つのバージョンを使うための設定です。requiredVersionは要求バージョンを表し、strictVersionは該当する構成で不正なバージョンを拒否するかを制御します。これらは解決方法の設定であり、非互換なコードを変換するものではありません。フォールバックやエラーの動作は構成に依存します。ModuleFederationPluginのリファレンスで確認できます。
リモートを増やす前に、互換性の方針を明確にしておきたいと思います。
- どの依存関係は同じランタイムを共有し、どれは個別に持てるか?
- どのホストとリモートのバージョンの組み合わせをサポートするか?
- 要求バージョンを満たせないとき、どうするか?
- プレビュー、移行、ロールバックをどう行うか?
認証情報、権限インターフェース、イベント、基盤SDKにも同じ考えが当てはまります。独立してデプロイできるのは、利用側と共有する契約を守れる範囲です。
私の基盤開発では、共通コードを各アプリにコピーすると契約の維持が難しくなりました。修正の届く時期がずれ、同期自体が仕事になります。バージョン付きの共通機能によって移行の道筋は明確になりましたが、移行作業がなくなったわけではありません。この経験はAIでひな形を同期し続けるのをやめ、ライブラリに切り出すで詳しく紹介しています。
難しさその2:ルーティングとライフサイクルには担当が必要
請求アプリには自身のルートの情報が必要です。しかし、URLのどの部分がシェルや他の製品領域のものかを、推測させるべきではありません。
シェルが最上位のナビゲーションを担当し、各アプリが割り当てられたパス配下を担当する、という合意が出発点になります。ただし、直接リンク、戻る・進む、リダイレクト、認証切れ、リモートの読み込み失敗を検証する必要があります。
ライフサイクルも重要です。別のルートへ移動した後に、購読、イベントリスナー、タイマー、DOMが残ってはいけません。調整側がunmountを呼んでも、後片付けをするのはアプリです。Federationがモジュールを届けても、それを開始・終了する仕組みはホストに必要です。
アプリ間通信も、狭く明確な境界で扱いたいところです。選択中のアカウントや言語の変更は複数アプリに関係しますが、すべての内部ストアを共有すると、リポジトリを分けても密結合が残ります。
適切な場面でのURLパラメーター、基盤API、担当とペイロードを定義したイベントなど、明示的なインターフェースを重視しています。認証と権限には一貫した契約が必要で、認可はフロントエンドの確認だけを信頼せず、バックエンドでも強制します。
実践上の問いは、別のリモートの内部状態を理解せずに、自分たちの実装を変更できるかです。
難しさその3:障害には十分な情報が必要
ホストとリモートが別々にリリースされる構成で、「ページが失敗した」という報告だけでは調査が難しくなります。
ホストのバージョン、リモート名とバージョン、ルート、リリース識別子、関連する機能フラグ、利用可能ならトレースIDが役立ちます。アカウントやテナントの情報は、診断に必要で適切な範囲に限定します。
Module Federationを使った仕事では、可観測性も基盤の契約の一部になりました。最初にエラーを表示したリポジトリの問題だと決めつけるより、フロントエンドの計測情報からバックエンドのスパンまで追うことが役立ちました。
独立したデプロイは、障害の隔離を自動的に実現しません。同じページのコードは、グローバル状態、スタイル、メインスレッドの重い処理を通じて影響し合います。Reactのエラーバウンダリは一部の描画エラーに役立ちますが、リモートのすべての動作を隔離するものではありません。
読み込み時の代替表示、エラー処理、バージョンの把握、復旧経路を意図して設計する必要があります。結合時の確認も必要です。リモート単体のテストが通っても、実際のホスト内では失敗することがあります。
対応するホストとリモートをプレビューし、重要な契約を検証し、主要経路のE2Eテストを行うほうが、コンパイル成功だけで互換性を判断するより有益です。
チームが使える基盤にする
開発者の手元でも機能する設計が必要です。小さな変更のたびに多くのリポジトリ、認証サービス、バックエンドをそろえるなら、リリースの調整負担をローカル開発に移しただけかもしれません。
ひな形生成、設定の文書化、ローカルモック、プレビュー環境、ホストが読み込むリモートのバージョンを選ぶ仕組みが役立ちます。共通のUI基本要素と言語対応の規約も、各チームが基礎を再実装せず、一貫した製品をつくる助けになります。
互換性の方針、リリース手順、障害時の調整にも担当が必要です。リモートを所有する責任はデプロイ後まで続きます。この点は肩書きより先に、オーナーシップをで、自身の経験として書いています。
シンプルなフロントエンドを選ぶ場面
小さなチームですでに素早くリリースでき、アプリが理解しやすい場合や、領域の境界が曖昧な場合には、導入を慎重に考えます。実行時の合成や個別のデプロイ基盤がなくても、モジュール化したアプリで明確な内部境界をつくれます。
判断は、解決したい制約から始めます。
- 実行時のモジュール配信と依存関係の共有が必要: Module Federationを検討する。
- アプリの有効化とライフサイクル調整が必要: single-spaを検討する。
- 両方が実際の課題: 接続するコストも含めて併用を評価する。
- どちらも開発の妨げではない: シンプルなアプリを維持する。
明確な担当と別々のリリースによる利点が、運用上の負担に見合うかが判断基準です。私にとって大きな学びは、コードを読み込むことは始まりにすぎないという点でした。互換性、ナビゲーション、診断、サポートこそが、チームが独立して動きながら一つの製品を届けられるかを決めます。