この記事の要点
• マイクロフロントエンドはアプリケーションを独立してデプロイ可能な小さなアプリに分割する
• Module Federation、Single-SPA、Native Federationなど複数の統合パターンがある
• Custom Eventsによるアプリ間通信と共有UIコンポーネントが設計の鍵
マイクロフロントエンドとは
マイクロフロントエンドは、マイクロサービスの考え方をフロントエンドに適用したアーキテクチャパターンです。大規模なアプリケーションを、独立してデプロイ可能な小さなアプリケーションに分割します。
flowchart TB
subgraph Monolithic["モノリシックフロントエンド"]
subgraph Single["Single Application"]
M1["Product<br/>Module"]
M2["Cart<br/>Module"]
M3["Account<br/>Module"]
end
Note1["単一のデプロイ、単一のチーム"]
end
subgraph MicroFE["マイクロフロントエンド"]
subgraph Shell["Container App (Shell)"]
Header["Shared Header"]
subgraph Apps["独立アプリケーション"]
A1["Product App<br/>(Team A)"]
A2["Cart App<br/>(Team B)"]
A3["Account App<br/>(Team C)"]
end
end
Note2["独立デプロイ、独立チーム、技術選択の自由"]
end
導入を検討すべきケース
| ケース | 理由 |
|---|---|
| 大規模チーム(10+人) | チーム間の独立性を確保 |
| レガシーシステムの段階的移行 | 新旧技術の共存が可能 |
| 異なるリリースサイクル | 機能ごとに独立してデプロイ |
| ドメイン駆動組織 | ビジネスドメインでチーム分割 |
| 技術スタックの多様性 | React, Vue, Angular の共存 |
ポイント: マイクロフロントエンドの導入は、チーム規模が10人以上で独立したリリースサイクルが必要な場合に最も効果を発揮します。小規模チームでは過剰設計になりがちです。
統合パターン
1. Module Federation(Webpack 5)
flowchart TB
subgraph Host["Host App (Shell)"]
Config["webpack.config.js<br/>remotes: {<br/> products, cart, account<br/>}"]
end
Host --> Products
Host --> Cart
Host --> Account
subgraph Products["Products Remote"]
P1["exposes:<br/>- List<br/>- Detail"]
end
subgraph Cart["Cart Remote"]
C1["exposes:<br/>- Cart<br/>- Summary"]
end
subgraph Account["Account Remote"]
A1["exposes:<br/>- Profile<br/>- Settings"]
end
// host-app/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
products: 'products@http://localhost:3001/remoteEntry.js',
cart: 'cart@http://localhost:3002/remoteEntry.js',
account: 'account@http://localhost:3003/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
// products-app/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
// host-app/src/App.tsx
import React, { Suspense, lazy } from 'react';
// 動的インポートでリモートコンポーネントを読み込み
const ProductList = lazy(() => import('products/ProductList'));
const Cart = lazy(() => import('cart/Cart'));
const AccountSettings = lazy(() => import('account/Settings'));
function App() {
return (
<div>
<Header />
<Routes>
<Route
path="/products/*"
element={
<Suspense fallback={<Skeleton />}>
<ProductList />
</Suspense>
}
/>
<Route
path="/cart"
element={
<Suspense fallback={<Skeleton />}>
<Cart />
</Suspense>
}
/>
<Route
path="/account/*"
element={
<Suspense fallback={<Skeleton />}>
<AccountSettings />
</Suspense>
}
/>
</Routes>
</div>
);
}
2. Single-SPA
// root-config.ts
import { registerApplication, start } from 'single-spa';
// マイクロフロントエンドの登録
registerApplication({
name: '@myorg/products',
app: () => System.import('@myorg/products'),
activeWhen: ['/products'],
});
registerApplication({
name: '@myorg/cart',
app: () => System.import('@myorg/cart'),
activeWhen: ['/cart'],
});
registerApplication({
name: '@myorg/account',
app: () => System.import('@myorg/account'),
activeWhen: ['/account'],
});
// 共有コンポーネント(常にアクティブ)
registerApplication({
name: '@myorg/navbar',
app: () => System.import('@myorg/navbar'),
activeWhen: () => true,
});
start();
// products-app/src/myorg-products.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import App from './App';
const lifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
errorBoundary(err, info, props) {
return <div>Error in products app</div>;
},
});
export const { bootstrap, mount, unmount } = lifecycles;
3. Native Federation(Vite/Rspack対応)
// vite.config.ts
import { defineConfig } from 'vite';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
federation({
name: 'host-app',
remotes: {
products: 'http://localhost:3001/assets/remoteEntry.js',
cart: 'http://localhost:3002/assets/remoteEntry.js',
},
shared: ['react', 'react-dom'],
}),
],
});
注意: 共有ライブラリ(React等)のバージョン不一致はランタイムエラーの原因になります。singletonオプションとrequiredVersionを必ず設定してください。
状態管理と通信
flowchart TB
subgraph Events["1. Custom Events (推奨)"]
EA["App A"] -->|CustomEvent| EB["App B"]
end
subgraph SharedState["2. Shared State (複雑な場合)"]
SA["App A"] --> Store["Store<br/>(Global)"]
SC["App C"] --> Store
Store --> SB["App B"]
Store --> SD["App D"]
end
subgraph URL["3. URL/Query Params"]
URLNote["ナビゲーション情報の共有"]
end
Custom Events
// 共有ライブラリ
// @myorg/events
export const CartEvents = {
ITEM_ADDED: 'cart:item-added',
ITEM_REMOVED: 'cart:item-removed',
CART_UPDATED: 'cart:updated',
} as const;
export interface CartItemEvent {
productId: string;
quantity: number;
price: number;
}
export function dispatchCartEvent(
type: string,
detail: CartItemEvent
): void {
window.dispatchEvent(new CustomEvent(type, { detail }));
}
export function subscribeToCartEvent(
type: string,
handler: (event: CustomEvent<CartItemEvent>) => void
): () => void {
window.addEventListener(type, handler as EventListener);
return () => window.removeEventListener(type, handler as EventListener);
}
// products-app/src/ProductCard.tsx
import { CartEvents, dispatchCartEvent } from '@myorg/events';
function ProductCard({ product }) {
const handleAddToCart = () => {
dispatchCartEvent(CartEvents.ITEM_ADDED, {
productId: product.id,
quantity: 1,
price: product.price,
});
};
return (
<div>
<h3>{product.name}</h3>
<button onClick={handleAddToCart}>Add to Cart</button>
</div>
);
}
// cart-app/src/Cart.tsx
import { useEffect, useState } from 'react';
import { CartEvents, subscribeToCartEvent } from '@myorg/events';
function Cart() {
const [items, setItems] = useState([]);
useEffect(() => {
const unsubscribe = subscribeToCartEvent(CartEvents.ITEM_ADDED, (event) => {
setItems((prev) => [...prev, event.detail]);
});
return unsubscribe;
}, []);
return (
<div>
<h2>Cart ({items.length} items)</h2>
{/* ... */}
</div>
);
}
グローバル状態の共有
// @myorg/shared-store
import { create } from 'zustand';
interface User {
id: string;
name: string;
email: string;
}
interface AuthState {
user: User | null;
isAuthenticated: boolean;
login: (user: User) => void;
logout: () => void;
}
// グローバルに共有されるストア
export const useAuthStore = create<AuthState>((set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false }),
}));
// 各マイクロフロントエンドで使用
// header-app/src/Header.tsx
import { useAuthStore } from '@myorg/shared-store';
function Header() {
const { user, isAuthenticated, logout } = useAuthStore();
return (
<header>
{isAuthenticated ? (
<>
<span>Welcome, {user?.name}</span>
<button onClick={logout}>Logout</button>
</>
) : (
<a href="/login">Login</a>
)}
</header>
);
}
ルーティング戦略
// shell-app/src/routing.ts
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { lazy, Suspense } from 'react';
// 各マイクロフロントエンドのルートを動的読み込み
const ProductsRoutes = lazy(() => import('products/Routes'));
const CartRoutes = lazy(() => import('cart/Routes'));
const AccountRoutes = lazy(() => import('account/Routes'));
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{
path: 'products/*',
element: (
<Suspense fallback={<Loading />}>
<ProductsRoutes />
</Suspense>
),
},
{
path: 'cart/*',
element: (
<Suspense fallback={<Loading />}>
<CartRoutes />
</Suspense>
),
},
{
path: 'account/*',
element: (
<Suspense fallback={<Loading />}>
<AccountRoutes />
</Suspense>
),
},
],
},
]);
export function AppRouter() {
return <RouterProvider router={router} />;
}
実践メモ: マイクロフロントエンド間の通信はCustom Eventsが最もシンプルで疎結合です。グローバル状態の共有は認証情報など最小限に留めましょう。
デプロイ戦略
flowchart TB
subgraph CDN["CDN構成"]
Shell["shell/<br/>└── v1.2.3/"]
Products["products/<br/>└── v2.1.0/"]
Cart["cart/<br/>└── v1.8.5/"]
Account["account/<br/>└── v3.0.1/"]
end
subgraph Manifest["マニフェスト管理"]
JSON["products: cdn/products/v2.1.0/<br/>cart: cdn/cart/v1.8.5/<br/>account: cdn/account/v3.0.1/"]
end
CDN --> Manifest
Note["Blue-Green / Canary デプロイ対応可能"]
各マイクロフロントエンドは独立してデプロイ可能であることが前提です。
共通UIコンポーネント
// @myorg/ui-kit (共有デザインシステム)
export { Button } from './Button';
export { Input } from './Input';
export { Card } from './Card';
export { Modal } from './Modal';
export { theme } from './theme';
// 各マイクロフロントエンドで使用
// webpack.config.js
{
shared: {
'@myorg/ui-kit': {
singleton: true,
requiredVersion: '^1.0.0',
},
},
}
各マイクロフロントエンドは独立してテスト可能であることが重要です。
テスト戦略
| レベル | 対象 | ツール |
|---|---|---|
| ユニット | 個別コンポーネント | Vitest, Jest |
| 統合 | マイクロFE単体 | Testing Library |
| E2E | 全体統合 | Playwright, Cypress |
| コントラクト | API契約 | Pact |
| コントラクト | API契約 | Pact |
参考リンク
参考リソース
- Martin Fowler - Micro Frontends
- Webpack - Module Federation
- Single-SPA Documentation
- micro-frontends.org