T-CREATOR

Next.js × pnpm/Turborepo 初期構築:ワークスペース・共有パッケージ・CI 最適化

Next.js × pnpm/Turborepo 初期構築:ワークスペース・共有パッケージ・CI 最適化

Next.js プロジェクトが大きくなるにつれ、複数のアプリケーション(Web フロント、管理画面、LP など)や共有ライブラリを効率的に管理する必要性が高まります。本記事では、pnpm のワークスペース機能Turborepo を組み合わせて、モノレポ構成を最初から正しく構築する方法を解説します。

特に以下のテーマに焦点を当てています。

  • pnpm ワークスペースによる依存関係の一元管理
  • Turborepo によるビルド・テストの高速化
  • 共有パッケージ(UI コンポーネント、共通ロジック)の効率的な運用
  • CI/CD パイプラインの最適化(キャッシュ戦略・並列実行)

これから Next.js でモノレポを始める方、既存プロジェクトをスケールさせたい方に役立つ実践的な内容となっています。

背景

モノレポが選ばれる理由

従来、複数のプロジェクトを別々のリポジトリで管理するマルチリポ構成が一般的でした。しかし、プロジェクトが増えると以下の課題が顕在化します。

  • 共通コンポーネントやユーティリティを各リポジトリにコピー&ペーストする必要がある
  • 依存関係のバージョンが統一されず、互換性問題が発生しやすい
  • CI/CD パイプラインをリポジトリごとに設定・管理しなければならない

こうした問題を解決するため、複数のプロジェクトを 1 つのリポジトリで管理するモノレポ構成が注目されています。

pnpm と Turborepo の役割

モノレポを実現するツールには、Yarn Workspaces、Lerna、Nx、Turborepo などがありますが、本記事では pnpm + Turborepo の組み合わせを採用します。

以下の図は、pnpm と Turborepo それぞれの役割を示しています。

mermaidflowchart TB
  subgraph pnpm ["pnpm Workspace"]
    direction TB
    pkg["依存関係の一元管理<br/>(シンボリックリンク)"]
    install["高速なインストール<br/>(ストレージ最適化)"]
  end

  subgraph turbo ["Turborepo"]
    direction TB
    cache["タスクキャッシュ<br/>(リモート/ローカル)"]
    parallel["並列実行<br/>(依存関係グラフ)"]
  end

  pnpm -->|パッケージ管理| turbo
  turbo -->|ビルド最適化| result["高速なビルド・CI"]

pnpm は依存関係のインストールと管理を効率化し、Turborepo はビルドやテストなどのタスク実行を高速化します。両者を組み合わせることで、開発速度と CI パフォーマンスが劇的に向上します。

技術選定のポイント

#ツール役割選定理由
1pnpmパッケージマネージャーインストールが高速で、ディスク容量を節約できる
2Turborepoビルドツールキャッシュ機能が強力で、CI 時間を大幅に短縮
3Next.jsフロントエンドフレームワークSSR/SSG に対応し、モノレポとの相性が良い

課題

モノレポ導入時の典型的な問題

モノレポ構成を採用する際、以下のような課題に直面することがあります。

1. 依存関係の複雑化

複数のパッケージが相互に依存すると、バージョン管理が煩雑になります。特に、共有パッケージを更新した際に、依存する全アプリケーションで動作確認が必要になります。

2. ビルド時間の増大

モノレポ内のパッケージ数が増えると、全体のビルド時間が長くなります。変更のないパッケージまで毎回ビルドされると、開発効率が低下します。

3. CI/CD の非効率性

従来の CI 設定では、コードの一部変更でもリポジトリ全体をビルド・テストする必要があり、CI 実行時間が増大します。

以下の図は、モノレポにおける課題の全体像を示しています。

mermaidflowchart TD
  mono["モノレポ"] --> issue1["依存関係の複雑化"]
  mono --> issue2["ビルド時間の増大"]
  mono --> issue3["CI/CD の非効率性"]

  issue1 --> problem1["バージョン管理が煩雑"]
  issue2 --> problem2["変更なしでも全ビルド"]
  issue3 --> problem3["全体テストで時間増"]

これらの課題を解決するために、pnpm のワークスペース機能と Turborepo のキャッシュ機構を活用します。

解決策

pnpm Workspace による依存関係の一元管理

pnpm のワークスペース機能を使うことで、複数のパッケージを 1 つのリポジトリで効率的に管理できます。

ワークスペース構成のメリット

  • 依存関係がルートの node_modules に集約され、重複インストールを回避
  • シンボリックリンクにより、ローカルパッケージを直接参照可能
  • バージョン管理が一元化され、整合性が保たれる

ディレクトリ構成

以下は、推奨するモノレポのディレクトリ構造です。

bashmonorepo/
├── apps/
│   ├── web/          # Next.js メインアプリ
│   └── admin/        # Next.js 管理画面
├── packages/
│   ├── ui/           # 共有 UI コンポーネント
│   ├── utils/        # 共通ユーティリティ
│   └── config/       # 共通設定(ESLint, TypeScript など)
├── package.json
├── pnpm-workspace.yaml
└── turbo.json

以下の図は、pnpm ワークスペースにおけるパッケージ間の依存関係を示しています。

mermaidflowchart LR
  web["apps/web"] -->|依存| ui["packages/ui"]
  admin["apps/admin"] -->|依存| ui
  web -->|依存| utils["packages/utils"]
  admin -->|依存| utils
  ui -->|依存| utils

apps​/​webapps​/​admin は、共有パッケージである packages​/​uipackages​/​utils に依存しています。pnpm はこれらをシンボリックリンクで効率的に管理します。

Turborepo によるビルド・テストの高速化

Turborepo は、タスクの依存関係を解析し、キャッシュと並列実行により高速化を実現します。

Turborepo の主要機能

#機能説明
1タスクキャッシュ一度実行したタスクの結果をキャッシュし、再実行を省略
2並列実行依存関係のないタスクを並列に実行
3リモートキャッシュCI 環境でキャッシュを共有し、ビルド時間を短縮

以下の図は、Turborepo のタスク実行フローを示しています。

mermaidflowchart TB
  start["タスク開始"] --> check["キャッシュ確認"]
  check -->|キャッシュあり| skip["実行スキップ"]
  check -->|キャッシュなし| exec["タスク実行"]
  exec --> save["結果をキャッシュ"]
  save --> done["完了"]
  skip --> done

キャッシュが存在する場合、タスクの実行をスキップし、大幅に時間を短縮できます。

CI/CD パイプラインの最適化

Turborepo のリモートキャッシュ機能を活用することで、CI 環境でも高速なビルドが可能になります。

CI 最適化のポイント

  • 変更のあったパッケージのみビルド・テスト
  • リモートキャッシュを利用し、過去のビルド結果を再利用
  • 並列実行により、複数のタスクを同時に処理

以下の図は、CI パイプラインにおけるキャッシュ戦略を示しています。

mermaidsequenceDiagram
  participant CI as CI 環境
  participant Cache as リモートキャッシュ
  participant Build as ビルドタスク

  CI->>Cache: キャッシュ確認
  alt キャッシュあり
    Cache-->>CI: キャッシュデータ返却
    CI->>Build: ビルドスキップ
  else キャッシュなし
    CI->>Build: ビルド実行
    Build-->>CI: ビルド結果
    CI->>Cache: 結果をキャッシュ保存
  end

リモートキャッシュにより、チーム全体で CI の高速化を共有できます。

具体例

ここでは、pnpm + Turborepo を使った Next.js モノレポの構築手順を、段階的に解説します。

ステップ 1: プロジェクトの初期化

まず、プロジェクトのルートディレクトリを作成し、pnpm を初期化します。

bashmkdir my-monorepo
cd my-monorepo
pnpm init

package.json が生成されます。次に、pnpm ワークスペースの設定を行います。

ステップ 2: pnpm ワークスペースの設定

ルートディレクトリに pnpm-workspace.yaml を作成します。

yamlpackages:
  - 'apps/*'
  - 'packages/*'

このファイルにより、apps​/​packages​/​ 配下のディレクトリがワークスペースとして認識されます。

ステップ 3: Next.js アプリケーションの作成

apps​/​ 配下に Next.js アプリケーションを作成します。

bashmkdir -p apps/web
cd apps/web
pnpm create next-app@latest . --typescript --tailwind --app --src-dir --import-alias "@/*"

同様に、管理画面用のアプリケーションも作成します。

bashmkdir -p apps/admin
cd apps/admin
pnpm create next-app@latest . --typescript --tailwind --app --src-dir --import-alias "@/*"

ステップ 4: 共有パッケージの作成

共有 UI コンポーネント用のパッケージを作成します。

bashmkdir -p packages/ui
cd packages/ui
pnpm init

packages​/​ui​/​package.json を以下のように設定します。

json{
  "name": "@my-monorepo/ui",
  "version": "0.0.1",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "scripts": {
    "lint": "eslint .",
    "type-check": "tsc --noEmit"
  },
  "dependencies": {
    "react": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "typescript": "^5.0.0"
  }
}

name フィールドには、スコープ付きのパッケージ名(例: @my-monorepo​/​ui)を指定します。これにより、他のパッケージから @my-monorepo​/​ui として参照できます。

ステップ 5: 共有パッケージの実装

packages​/​ui​/​src​/​index.ts にシンプルなボタンコンポーネントを作成します。

typescript// packages/ui/src/index.ts
export { Button } from './Button';
typescript// packages/ui/src/Button.tsx
import React from 'react';

interface ButtonProps {
  children: React.ReactNode;
  onClick?: () => void;
}

/**
 * 共通ボタンコンポーネント
 * モノレポ内の全アプリで利用可能
 */
export const Button: React.FC<ButtonProps> = ({
  children,
  onClick,
}) => {
  return (
    <button
      onClick={onClick}
      className='px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600'
    >
      {children}
    </button>
  );
};

このコンポーネントは、モノレポ内の全アプリケーションで共有されます。

ステップ 6: アプリケーションから共有パッケージを利用

apps​/​web から @my-monorepo​/​ui を依存関係に追加します。

bashcd apps/web
pnpm add @my-monorepo/ui@workspace:*

workspace:* を指定することで、ワークスペース内のローカルパッケージを参照します。

次に、apps​/​web​/​src​/​app​/​page.tsx でボタンコンポーネントを使用します。

typescript// apps/web/src/app/page.tsx
import { Button } from '@my-monorepo/ui';

/**
 * メインページ
 * 共有 UI コンポーネントを利用
 */
export default function Home() {
  return (
    <main className='flex min-h-screen flex-col items-center justify-center'>
      <h1 className='text-4xl font-bold mb-8'>
        Welcome to Next.js Monorepo
      </h1>
      <Button onClick={() => alert('Clicked!')}>
        Click Me
      </Button>
    </main>
  );
}

これにより、共有パッケージのコンポーネントをアプリケーション内で利用できます。

ステップ 7: Turborepo のセットアップ

ルートディレクトリに戻り、Turborepo をインストールします。

bashcd ../..
pnpm add -Dw turbo

-Dw オプションは、ワークスペースのルートに devDependency として追加することを意味します。

ステップ 8: Turborepo の設定

ルートディレクトリに turbo.json を作成し、タスクの設定を行います。

json{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "lint": {
      "outputs": []
    },
    "type-check": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

この設定により、以下のタスクが最適化されます。

#タスク説明
1buildビルド結果を .next​/​** にキャッシュ(.next​/​cache を除く)
2lintESLint による静的解析を実行
3type-checkTypeScript の型チェックを実行
4dev開発サーバーを起動(キャッシュ無効)

dependsOn フィールドには、タスクの依存関係を指定します。^build は「依存パッケージの build タスクが完了してから実行」を意味します。

ステップ 9: ルートの package.json にスクリプトを追加

ルートの package.json に Turborepo を使ったスクリプトを追加します。

json{
  "name": "my-monorepo",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "lint": "turbo run lint",
    "type-check": "turbo run type-check"
  },
  "devDependencies": {
    "turbo": "^1.10.0"
  }
}

これにより、pnpm dev でワークスペース全体の開発サーバーが起動し、pnpm build で全パッケージをビルドできます。

ステップ 10: ビルドとキャッシュの確認

最初のビルドを実行します。

bashpnpm build

初回は全パッケージがビルドされますが、2 回目以降は変更がない限りキャッシュが使われます。

bashpnpm build

2 回目の実行では、以下のようなログが表示され、キャッシュが利用されたことが確認できます。

makefile• Packages in scope: @my-monorepo/ui, web, admin
• Running build in 3 packages
• Remote caching disabled

@my-monorepo/ui:build: cache hit, replaying output
web:build: cache hit, replaying output
admin:build: cache hit, replaying output

Tasks:    3 successful, 3 total
Cached:   3 cached, 3 total
Time:     0.5s >>> FULL TURBO

FULL TURBO と表示されれば、全タスクがキャッシュから復元されています。

ステップ 11: CI でのリモートキャッシュ設定

Turborepo のリモートキャッシュを利用すると、CI 環境でもキャッシュを共有できます。Vercel が提供する無料のリモートキャッシュを使う場合、以下の手順で設定します。

まず、Turborepo のアカウントにログインします。

bashnpx turbo login

次に、リモートキャッシュをリンクします。

bashnpx turbo link

これにより、.turbo ディレクトリにトークンが保存され、CI 環境でもキャッシュを利用できます。

ステップ 12: GitHub Actions での CI 設定

.github​/​workflows​/​ci.yml を作成し、CI パイプラインを設定します。

yamlname: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Setup pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run lint
        run: pnpm lint

      - name: Run type-check
        run: pnpm type-check

      - name: Run build
        run: pnpm build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

TURBO_TOKENTURBO_TEAM は、GitHub のリポジトリシークレットに登録します。これにより、CI 環境でリモートキャッシュが利用されます。

以下の図は、CI パイプラインの実行フローを示しています。

mermaidflowchart TB
  checkout["コードチェックアウト"] --> setup["Node.js & pnpm セットアップ"]
  setup --> install["依存関係インストール"]
  install --> lint["Lint 実行"]
  install --> typecheck["型チェック実行"]
  lint --> build["ビルド実行"]
  typecheck --> build
  build --> cache["リモートキャッシュ保存"]
  cache --> finish["CI 完了"]

Lint と型チェックは並列に実行され、その後ビルドが実行されます。

図で理解できる要点

  • pnpm ワークスペースにより、依存関係がシンボリックリンクで効率的に管理される
  • Turborepo のキャッシュ機構により、変更のないタスクは実行がスキップされる
  • CI 環境でリモートキャッシュを利用することで、ビルド時間が大幅に短縮される

まとめ

本記事では、Next.js プロジェクトにおいて pnpm と Turborepo を組み合わせたモノレポ構成を、初期構築から CI 最適化まで段階的に解説しました。

主なポイントは以下の通りです。

  • pnpm ワークスペースにより、複数のパッケージを効率的に管理し、依存関係の重複を回避できます
  • Turborepo のタスクキャッシュと並列実行により、ビルド・テスト時間が劇的に短縮されます
  • 共有パッケージ(UI コンポーネント、ユーティリティ)を作成することで、コードの再利用性が向上します
  • リモートキャッシュを CI 環境で活用することで、チーム全体の開発効率が向上します

モノレポ構成は、初期設定こそ手間がかかりますが、長期的にはコード管理やビルド時間の面で大きなメリットがあります。特に、複数のアプリケーションや共通ライブラリを抱えるプロジェクトでは、導入を検討する価値があるでしょう。

今回紹介した構成を基盤として、プロジェクトの成長に合わせて拡張していくことができます。

関連リンク