T-CREATOR

Storybook を Monorepo に導入:Yarn Workspaces/Turborepo の最短レシピ

Storybook を Monorepo に導入:Yarn Workspaces/Turborepo の最短レシピ

Monorepo で複数のプロジェクトを管理していると、コンポーネントの可視化や開発効率の向上のために Storybook を導入したくなりますよね。しかし、Yarn Workspaces や Turborepo といった Monorepo ツールと Storybook を組み合わせる際、設定の複雑さや依存関係の解決に悩まされることも少なくありません。

本記事では、Yarn Workspaces と Turborepo を使った Monorepo 環境に Storybook を最短で導入する手順を、初心者の方でもわかりやすく解説します。実際に動作するサンプルコードとともに、つまずきやすいポイントも丁寧にご紹介しますので、ぜひ最後までお読みください。

背景

Monorepo とは

Monorepo(モノレポ)は、複数のプロジェクトやパッケージを 1 つのリポジトリで管理する開発手法です。

従来の Multi-repo では、各プロジェクトが独立したリポジトリを持つため、共通コードの管理やバージョン管理が煩雑になりがちでした。Monorepo を採用することで、コードの再利用性が向上し、依存関係の管理が容易になります。

以下の図は、Monorepo の基本構造を示しています。

mermaidflowchart TB
  root["Monorepo ルート"]
  root --> packages["packages/"]
  packages --> ui["ui パッケージ"]
  packages --> utils["utils パッケージ"]
  packages --> app["app パッケージ"]
  ui --> ui_components["React コンポーネント"]
  utils --> util_funcs["共通関数"]
  app --> app_code["アプリケーション<br/>コード"]
  app -.依存.-> ui
  app -.依存.-> utils

この構成により、app パッケージは uiutils を簡単に参照でき、コードの重複を避けられます。

Yarn Workspaces と Turborepo の役割

Yarn Workspaces は、Yarn が提供する Monorepo 管理機能です。複数のパッケージを 1 つの node_modules で管理し、依存関係の解決を効率化します。

Turborepo は、Monorepo 向けのビルドシステムで、タスクのキャッシュや並列実行により、ビルド時間を大幅に短縮できます。

以下の図は、両者の役割分担を示しています。

mermaidflowchart LR
  yarn["Yarn Workspaces"]
  turbo["Turborepo"]
  deps["依存関係管理"]
  build["ビルド最適化"]
  yarn --> deps
  turbo --> build
  deps --> node["node_modules<br/>共通化"]
  build --> cache["キャッシュ/<br/>並列実行"]

Yarn Workspaces がパッケージの依存を一元管理し、Turborepo がビルドプロセスを最適化することで、開発体験が大きく向上します。

Storybook の役割

Storybook は、UI コンポーネントを独立した環境で開発・テストするためのツールです。

コンポーネントカタログとして機能し、デザイナーや他の開発者との共有もスムーズに行えます。Monorepo 環境では、複数のパッケージにまたがるコンポーネントを一元管理できるため、Storybook の導入は特に有効です。

課題

Monorepo 環境での Storybook 導入の難しさ

Monorepo に Storybook を導入する際、以下のような課題に直面することがあります。

  • 依存関係の解決: Workspace 間のパッケージ参照が正しく認識されない
  • 設定ファイルの配置: Storybook の設定をどのパッケージに置くべきか判断が難しい
  • ビルドキャッシュの管理: Turborepo のキャッシュと Storybook のビルドが干渉する
  • パス解決の問題: TypeScript や Webpack のパス解決が複雑になる

これらの課題を解決しないと、Storybook が正しく起動しなかったり、コンポーネントが表示されなかったりします。

以下の図は、典型的な問題の発生フローを示しています。

mermaidflowchart TD
  start["Storybook 起動"]
  start --> check_deps["依存関係チェック"]
  check_deps -->|失敗| err1["Error: Module<br/>not found"]
  check_deps -->|成功| check_path["パス解決"]
  check_path -->|失敗| err2["Error: Cannot<br/>resolve path"]
  check_path -->|成功| check_build["ビルド実行"]
  check_build -->|失敗| err3["Error: Build<br/>failed"]
  check_build -->|成功| success["Storybook<br/>正常起動"]

これらのエラーを回避するために、適切な設定が必要となります。

解決策

基本方針

Monorepo に Storybook を導入する際の基本方針は以下の通りです。

  1. 専用パッケージとして Storybook を配置: packages​/​storybook など専用のパッケージを作成する
  2. Workspace の依存関係を明示: 他のパッケージを参照する場合は package.json に明記する
  3. Turborepo のタスクに統合: turbo.json に Storybook のビルドタスクを追加する
  4. パス解決を統一: TypeScript の tsconfig.json や Webpack の設定でパスエイリアスを統一する

この方針に従うことで、Monorepo 環境でもスムーズに Storybook を動作させられます。

ディレクトリ構成

以下のようなディレクトリ構成を想定します。

csharpmonorepo-root/
├─ package.json          # ルートの package.json
├─ turbo.json            # Turborepo の設定
├─ yarn.lock
└─ packages/
   ├─ ui/                # UI コンポーネントパッケージ
   │  ├─ package.json
   │  ├─ src/
   │  │  └─ Button.tsx
   │  └─ tsconfig.json
   ├─ utils/             # ユーティリティパッケージ
   │  ├─ package.json
   │  └─ src/
   │     └─ format.ts
   └─ storybook/         # Storybook 専用パッケージ
      ├─ package.json
      ├─ .storybook/
      │  ├─ main.ts
      │  └─ preview.ts
      └─ stories/
         └─ Button.stories.tsx

この構成により、Storybook を独立したパッケージとして管理でき、他のパッケージへの影響を最小限に抑えられます。

設計のポイント

以下の図は、Storybook パッケージと他パッケージの関係を示しています。

mermaidflowchart LR
  sb["storybook<br/>パッケージ"]
  ui["ui<br/>パッケージ"]
  utils["utils<br/>パッケージ"]
  sb -.参照.-> ui
  sb -.参照.-> utils
  ui -.参照.-> utils

Storybook パッケージは、UI コンポーネントやユーティリティを参照するだけで、逆方向の依存は発生しません。これにより、依存関係がシンプルになります。

具体例

ステップ 1: Monorepo の初期設定

まず、Yarn Workspaces を有効にした Monorepo を作成します。

ルートディレクトリに package.json を作成し、以下のように設定してください。

json{
  "name": "monorepo-example",
  "private": true,
  "workspaces": ["packages/*"],
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "storybook": "yarn workspace storybook storybook"
  },
  "devDependencies": {
    "turbo": "^1.10.0"
  }
}

ここでは、workspaces フィールドで packages​/​* 配下のすべてのディレクトリを Workspace として登録しています。

また、scripts には Turborepo を使ったビルドコマンドと、後ほど作成する Storybook の起動コマンドを定義しています。

ステップ 2: Turborepo の設定

次に、Turborepo の設定ファイル turbo.json をルートに作成します。

json{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "dev": {
      "cache": false
    },
    "storybook": {
      "cache": false,
      "dependsOn": ["^build"]
    }
  }
}

この設定により、build タスクは依存パッケージのビルドが完了してから実行されます。

storybook タスクは、キャッシュを無効化し、依存パッケージのビルド完了後に実行されるように設定しています。これにより、常に最新のコンポーネントが Storybook に反映されます。

ステップ 3: UI パッケージの作成

UI コンポーネントを管理する packages​/​ui を作成します。

まず、packages​/​ui​/​package.json を作成してください。

json{
  "name": "ui",
  "version": "1.0.0",
  "main": "./src/index.ts",
  "scripts": {
    "build": "tsc"
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "react": "^18.2.0",
    "typescript": "^5.0.0"
  },
  "peerDependencies": {
    "react": "^18.2.0"
  }
}

このパッケージでは、React コンポーネントを TypeScript で開発し、tsc でビルドします。

次に、packages​/​ui​/​tsconfig.json を作成します。

json{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "jsx": "react-jsx",
    "declaration": true,
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

これにより、TypeScript のコンパイル設定が完了します。

次に、サンプルのボタンコンポーネントを作成しましょう。packages​/​ui​/​src​/​Button.tsx を作成してください。

typescriptimport React from 'react';

// ボタンコンポーネントのプロパティ型定義
export interface ButtonProps {
  label: string; // ボタンに表示するテキスト
  onClick?: () => void; // クリック時のコールバック
  variant?: 'primary' | 'secondary'; // ボタンのスタイルバリエーション
}

ここでは、ボタンのプロパティを定義しています。variant プロパティで、ボタンのスタイルを切り替えられるようにしています。

次に、ボタンコンポーネント本体を実装します。

typescript// ボタンコンポーネントの実装
export const Button: React.FC<ButtonProps> = ({
  label,
  onClick,
  variant = 'primary',
}) => {
  // スタイルをバリエーションに応じて切り替え
  const style: React.CSSProperties = {
    padding: '10px 20px',
    border: 'none',
    borderRadius: '4px',
    cursor: 'pointer',
    backgroundColor:
      variant === 'primary' ? '#007bff' : '#6c757d',
    color: '#fff',
    fontSize: '14px',
  };

  return (
    <button style={style} onClick={onClick}>
      {label}
    </button>
  );
};

シンプルなボタンコンポーネントですが、variant によってスタイルが変わることがわかります。

最後に、packages​/​ui​/​src​/​index.ts でエクスポートします。

typescript// ui パッケージのエントリーポイント
export { Button } from './Button';
export type { ButtonProps } from './Button';

これで、UI パッケージの準備が整いました。

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

次に、Storybook 専用のパッケージ packages​/​storybook を作成します。

まず、packages​/​storybook​/​package.json を作成してください。

json{
  "name": "storybook",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build"
  },
  "dependencies": {
    "ui": "workspace:*"
  },
  "devDependencies": {
    "@storybook/react": "^7.6.0",
    "@storybook/react-vite": "^7.6.0",
    "@storybook/addon-essentials": "^7.6.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "storybook": "^7.6.0",
    "vite": "^5.0.0"
  }
}

ここでは、dependencies"ui": "workspace:*" を指定することで、Workspace 内の ui パッケージを参照しています。

また、Storybook 7 系と Vite を使った構成を採用しています。Vite はビルドが高速で、開発体験が向上します。

次に、Storybook の設定ファイル packages​/​storybook​/​.storybook​/​main.ts を作成します。

typescriptimport type { StorybookConfig } from '@storybook/react-vite';

// Storybook のメイン設定
const config: StorybookConfig = {
  // ストーリーファイルの場所を指定
  stories: ['../stories/**/*.stories.@(ts|tsx|js|jsx)'],

  // 使用するアドオンを指定
  addons: [
    '@storybook/addon-essentials', // 基本的なアドオン群
  ],

  // 使用するフレームワークを指定
  framework: {
    name: '@storybook/react-vite', // React + Vite を使用
    options: {},
  },

  // ドキュメント生成の設定
  docs: {
    autodocs: 'tag', // @docs タグがあるストーリーは自動でドキュメント生成
  },
};

export default config;

この設定により、stories ディレクトリ配下のストーリーファイルが自動で読み込まれます。

次に、プレビュー設定 packages​/​storybook​/​.storybook​/​preview.ts を作成します。

typescriptimport type { Preview } from '@storybook/react';

// Storybook プレビュー画面の設定
const preview: Preview = {
  parameters: {
    // アクション(イベントログ)の設定
    actions: { argTypesRegex: '^on[A-Z].*' },

    // コントロールパネルの設定
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
};

export default preview;

この設定により、onClick などのイベントハンドラが自動的にアクションパネルに表示されます。

ステップ 5: ストーリーファイルの作成

次に、ボタンコンポーネント用のストーリーファイル packages​/​storybook​/​stories​/​Button.stories.tsx を作成します。

まず、インポート部分を記述します。

typescriptimport type { Meta, StoryObj } from '@storybook/react';
import { Button } from 'ui';

// Button コンポーネントのメタデータ設定
const meta: Meta<typeof Button> = {
  title: 'Components/Button', // Storybook 上での表示カテゴリ
  component: Button, // 対象のコンポーネント
  tags: ['autodocs'], // 自動ドキュメント生成を有効化
};

export default meta;

ここでは、ui パッケージから Button をインポートし、Storybook のメタデータを設定しています。

次に、具体的なストーリーを定義します。

typescript// Story の型定義
type Story = StoryObj<typeof Button>;

// Primary バリエーションのストーリー
export const Primary: Story = {
  args: {
    label: 'Primary Button',
    variant: 'primary',
  },
};

// Secondary バリエーションのストーリー
export const Secondary: Story = {
  args: {
    label: 'Secondary Button',
    variant: 'secondary',
  },
};

これにより、Storybook 上で PrimarySecondary の 2 つのバリエーションが表示されます。

さらに、クリックイベント付きのストーリーも作成しましょう。

typescript// クリックイベント付きのストーリー
export const WithClick: Story = {
  args: {
    label: 'Click Me',
    variant: 'primary',
    onClick: () => alert('Button clicked!'),
  },
};

このストーリーでは、ボタンをクリックするとアラートが表示されます。Storybook のアクションパネルでイベントの発火も確認できます。

ステップ 6: Storybook の起動

すべての設定が完了したら、Storybook を起動してみましょう。

ルートディレクトリで以下のコマンドを実行してください。

bashyarn install

これにより、すべての Workspace の依存関係がインストールされます。

次に、UI パッケージをビルドします。

bashyarn workspace ui build

このコマンドで、TypeScript のコンパイルが実行され、packages​/​ui​/​dist にビルド結果が出力されます。

最後に、Storybook を起動します。

bashyarn storybook

ブラウザで http:​/​​/​localhost:6006 にアクセスすると、Storybook が表示されます。

左側のサイドバーに Components​/​Button が表示され、PrimarySecondaryWithClick の各ストーリーを切り替えて確認できるはずです。

ステップ 7: Turborepo との統合

Turborepo を使って、ビルドと Storybook の起動を一括管理しましょう。

ルートの package.json に以下のスクリプトを追加します(すでに追加済みの場合はスキップしてください)。

json{
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "storybook": "yarn workspace storybook storybook"
  }
}

これにより、yarn build で全パッケージのビルドが、yarn storybook で Storybook の起動が行えます。

また、turbo.json の設定により、依存関係が自動的に解決されるため、UI パッケージのビルドを忘れる心配もありません。

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

mermaidflowchart TD
  start["yarn storybook"]
  start --> turbo["Turborepo タスク解決"]
  turbo --> dep_check["依存関係チェック"]
  dep_check --> build_ui["ui パッケージ<br/>ビルド"]
  build_ui --> run_sb["Storybook 起動"]
  run_sb --> done["http://localhost:6006<br/>で閲覧可能"]

このフローにより、常に最新のコンポーネントが Storybook に反映されます。

よくあるエラーと対処法

Monorepo 環境で Storybook を導入する際、以下のようなエラーが発生することがあります。

Error: Cannot find module 'ui'

エラーコード: Error: Cannot find module 'ui'

発生条件: Workspace の依存関係が正しく解決されていない場合に発生します。

解決方法:

  1. packages​/​storybook​/​package.jsondependencies"ui": "workspace:*" が記載されているか確認する
  2. ルートディレクトリで yarn install を再実行する
  3. yarn.lock を削除して、再度 yarn install を実行する

これにより、Workspace 間の参照が正しく解決されます。

Error: Build failed - TypeScript compilation errors

エラーコード: Error: Build failed

エラーメッセージ:

luaerror TS2307: Cannot find module 'ui' or its corresponding type declarations.

発生条件: UI パッケージのビルドが完了していない、または型定義が生成されていない場合に発生します。

解決方法:

  1. yarn workspace ui build を実行して、UI パッケージをビルドする
  2. packages​/​ui​/​tsconfig.jsondeclarationtrue になっているか確認する
  3. packages​/​ui​/​dist.d.ts ファイルが生成されているか確認する

型定義が正しく生成されていれば、エラーは解消されます。

Storybook が起動するが、コンポーネントが表示されない

発生条件: ストーリーファイルのパスが正しく設定されていない場合に発生します。

解決方法:

  1. packages​/​storybook​/​.storybook​/​main.tsstories フィールドを確認する
  2. ストーリーファイルが packages​/​storybook​/​stories 配下に配置されているか確認する
  3. ファイル名が *.stories.tsx または *.stories.ts の形式になっているか確認する

パスとファイル名が正しければ、Storybook が自動的にストーリーを検出します。

さらなる改善ポイント

以下の表は、基本構成からさらに改善できるポイントをまとめたものです。

#改善項目内容効果
1CSS-in-JS の導入Emotion や styled-components を導入スタイル管理の一元化
2Addon の追加a11y、viewport、interactions などのアドオンテスト・アクセシビリティ向上
3テーマの共通化デザイントークンを別パッケージで管理デザインの一貫性向上
4ビルド最適化Vite のキャッシュ設定を調整ビルド時間の短縮
5CI/CD 統合GitHub Actions で Storybook を自動デプロイチーム共有の効率化

これらの改善により、Storybook の活用範囲がさらに広がります。

まとめ

本記事では、Yarn Workspaces と Turborepo を使った Monorepo 環境に Storybook を導入する手順を、具体的なコード例とともに解説しました。

Monorepo 環境では、依存関係の管理やビルドプロセスの複雑さがネックになりがちですが、適切な設定を行うことで、Storybook をスムーズに統合できます。特に、Workspace の依存関係を明示し、Turborepo のタスク管理を活用することで、開発体験を大きく向上させられるでしょう。

また、エラー発生時の対処法も併せてご紹介しましたので、つまずいた際にはぜひ参考にしてください。Storybook を活用することで、コンポーネントの可視化やチーム間の共有がスムーズになり、開発効率が飛躍的に向上するはずです。

今回ご紹介した構成をベースに、ぜひ皆さんのプロジェクトに合わせてカスタマイズしてみてくださいね。

関連リンク