フロントエンドプロジェクトのコントリビューターが20人を超え、モジュールが50個を超えると、「動けばいい」というエンジニアリング構造は急速に崩壊する。保守性は美徳ではなく、生存戦略だ。本稿は実際のエンジニアリング意思決定から出発し、MonorepoとMultirepoの真のトレードオフ、依存関係ガバナンスの核心的な矛盾、そして自動化パイプラインで複雑度の膨張を制御する方法を議論する。
Monorepo vs Multirepo:技術選択ではなく組織の決断
Monorepoの真の恩恵
Monorepoの核心的な優位性は「便利さ」ではなく、強制的な統一性だ:
monorepo/
├── packages/
│ ├── ui-components/ # 共有UIライブラリ
│ ├── data-fetcher/ # データ層の抽象化
│ ├── app-admin/ # 管理ダッシュボード
│ └── app-portal/ # ユーザーポータル
├── tooling/
│ ├── eslint-config/
│ ├── tsconfig-base/
│ └── build-scripts/
├── turbo.json
└── pnpm-workspace.yaml
この構造がもたらす重要な能力:
- アトミックな変更:1つのPRで下位ライブラリと上位アプリを同時に変更でき、CIが即座に互換性を検証する
- 統一ツールチェーン:ESLint・TypeScript・ビルド設定を1か所で管理し、
extendsで配布 - 安全なクロスパッケージリファクタ:IDEの「グローバルリネーム」が本当に機能する。リポジトリをまたぐ検索の盲点がない
Monorepoの真のコスト
実際の運用では、Monorepoが引き起こす問題も同様に厄介だ:
CI時間の膨張 ─ リポジトリに200以上のパッケージが含まれると、Turborepoのリモートキャッシュを使っても、コールドスタートのCIで依存関係のインストールに10〜15分かかる。変更検出を導入する:
# turbo.json ─ 依存グラフを正確に定義
{
"pipeline":
{
"build":
{
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"],
"outputs": ["dist/**"],
},
"test": { "dependsOn": ["build"], "inputs": ["src/**", "__tests__/**"] },
},
}
権限境界の曖昧化 ─ 誰でも何でも変更できる。CODEOWNERSとprotected branch rulesを組み合わせる:
# .github/CODEOWNERS
/packages/ui-components/ @frontend-platform-team
/packages/app-admin/ @admin-team
/tooling/ @dx-team
Multirepoを選ぶべき時
以下の条件を満たす場合、Multirepoが適切かもしれない:
- サブシステムのリリースサイクルが完全に独立している(月1回 vs 1日3回)
- チーム間にコード共有ニーズがない
- 組織が高度に分散していて統一DXチームがない
依存関係ガバナンス:バージョンドリフトは最大の隠れた技術的負債
問題の本質
プロジェクトが2年以上稼働すると、最も一般的なエンジニアリング問題は「コードが汚い」ではなく依存関係のバージョン断片化だ。
ガバナンス戦略
戦略1:統一バージョンポリシー(Monorepo用)
{
"pnpm": {
"overrides": {
"react": "18.3.1",
"react-dom": "18.3.1",
"typescript": "5.5.4",
},
},
}
戦略2:自動アップグレード + 人間によるセーフティネット
{
"packageRules": [
{ "matchUpdateTypes": ["patch", "minor"], "automerge": true },
{
"matchUpdateTypes": ["major"],
"automerge": false,
"labels": ["breaking-change"]
}
]
}
自動化パイプライン:Lint + Gitフック + CIの階層設計
3層防衛モデル
┌─────────────────────────────────────────────────────┐
│ Layer 3: CIパイプライン(ゲート) │
│ → フルビルド + フルテスト + セキュリティスキャン │
├─────────────────────────────────────────────────────┤
│ Layer 2: Pre-pushフック │
│ → TypeScript型チェック + 影響を受けるパッケージの単体テスト │
├─────────────────────────────────────────────────────┤
│ Layer 1: Pre-commitフック(高速) │
│ → ESLint + Prettier(ステージングされたファイルのみ) │
└─────────────────────────────────────────────────────┘
核心原則:下位層ほど高速に、上位層ほど完全に。Pre-commitは3秒以内に終わらなければ、開発者に迂回される。
// lint-staged.config.js
export default {
"*.{ts,tsx,vue}": ["eslint --fix --max-warnings 0", "prettier --write"],
"*.css": ["stylelint --fix", "prettier --write"],
"*.json": ["prettier --write"],
};
まとめ
保守性は一度設計したら終わりではない。統一ツールチェーン、自動化された依存関係アップグレード、階層的な品質ゲート、明確なオーナーシップ境界への継続的な投資が必要だ。見返りは複利で増えていく ─ 整備されたコードベースに参加した新しいエンジニアは、環境と戦うのに数週間ではなく、数時間で生産的になれる。