T-CREATOR

Storybook アーキテクチャ完全図解:Preview/Manager/Builder が噛み合う瞬間

Storybook アーキテクチャ完全図解:Preview/Manager/Builder が噛み合う瞬間

Storybook を使って開発していると、「なぜこんなにスムーズにコンポーネントが表示されるんだろう?」と感じたことはありませんか。実は、その背後ではPreviewManagerBuilderという 3 つのコア要素が絶妙な連携を見せています。

これらの要素がどのように協調し合い、私たちに快適な開発体験を提供しているのか。その仕組みを理解することで、Storybook のカスタマイズやトラブルシューティングが格段に楽になります。今回は、Storybook の内部アーキテクチャを図解で分かりやすく解説し、各要素の役割と相互関係を明らかにしていきましょう。

背景

Storybook の基本概念と役割

Storybook は、UI コンポーネントを独立した環境で開発・テスト・文書化できるツールです。アプリケーション全体を起動することなく、個々のコンポーネントに集中して開発できるのが最大の特徴です。

javascript// 基本的なStoryの例
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'Example/Button',
  component: Button,
  parameters: {
    layout: 'centered',
  },
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
  args: {
    primary: true,
    label: 'Button',
  },
};

従来の UI コンポーネント開発の課題

従来のコンポーネント開発では、以下のような課題がありました。

課題従来の問題点Storybook による解決
1アプリ全体を起動してコンポーネントを確認独立環境でのコンポーネント表示
2複雑な状態設定が必要プロップスを直接指定可能
3UI 変更の影響範囲が不明各ストーリーで独立してテスト
4デザインシステムの管理困難統一された文書化環境

Storybook はこれらの課題を、独自のアーキテクチャで解決しています。

Preview、Manager、Builder 各要素の基本定義

Storybook の内部では、役割の異なる 3 つの要素が連携して動作します。

mermaidflowchart TB
    Dev[開発者] -->|ストーリー作成| Builder[Builder<br/>ビルドシステム]
    Builder -->|コンパイル| Preview[Preview<br/>表示エンジン]
    Builder -->|UI生成| Manager[Manager<br/>管理インターフェース]

    Preview -->|表示内容| Manager
    Manager -->|操作指示| Preview

    Preview -->|コンポーネント描画| Browser[ブラウザ画面]
    Manager -->|UI操作画面| Browser

各要素の基本的な役割は次のとおりです。

  • Builder: ストーリーファイルをコンパイルし、Webpack や Vite などのビルドツールを抽象化
  • Preview: 実際のコンポーネントを描画・実行する iframe 環境
  • Manager: サイドバー、ツールバー、パネルなどの操作 UI を管理

図で理解できる要点:

  • 各要素が独立した責任を持ちながら連携
  • Builder が他の 2 要素の基盤を提供
  • Preview と Manager は双方向にデータをやり取り

課題

各要素がどう連携しているか分からない

多くの開発者が抱える最初の疑問は、「Storybook の中で何が起きているのか」ということです。表面的には単純に見える Storybook ですが、内部では複雑な要素間通信が行われています。

mermaidsequenceDiagram
    participant U as 開発者
    participant B as Builder
    participant M as Manager
    participant P as Preview

    U->>B: yarn storybook 実行
    B->>B: ストーリーファイル解析
    B->>M: Manager UI構築
    B->>P: Preview環境構築
    M->>P: ストーリー選択指示
    P->>M: 描画完了通知
    M->>U: Storybook画面表示

この連携プロセスが見えないことで、以下の問題が発生します。

  • カスタマイズ時にどのファイルを変更すべきか判断困難
  • エラーが発生した際の原因特定に時間がかかる
  • パフォーマンス問題の改善ポイントが分からない

ビルドプロセスが複雑で理解しにくい

Storybook のビルドプロセスは、通常の Web アプリケーションよりも複雑です。

typescript// .storybook/main.ts での設定例
import type { StorybookConfig } from '@storybook/react-webpack5';

const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
  addons: [
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/react-webpack5',
    options: {},
  },
};

export default config;

このシンプルな設定ファイルの背後では、以下の複雑な処理が実行されています。

  1. ストーリーファイルの自動検出とインポート
  2. アドオンの依存関係解析と読み込み
  3. Preview 用と Manager 用の別々の Webpack 設定生成
  4. Hot Module Replacement(HMR)の設定
  5. 静的ファイルの処理とコピー

カスタマイズ時にどこを変更すればいいか不明

Storybook をカスタマイズする際、変更対象が 3 つの要素のどれに該当するか判断に迷うことがあります。

カスタマイズ内容対象要素変更ファイル
ストーリーの表示方法変更Preview.storybook​/​preview.ts
サイドバーのテーマ変更Manager.storybook​/​manager.ts
ビルド設定の調整Builder.storybook​/​main.ts
カスタムアドオンの作成全要素複数ファイル

この判断ミスにより、期待した結果が得られず、開発効率が低下してしまいます。

解決策

Preview:コンポーネント表示エンジンの仕組み

Preview は、ストーリーで定義されたコンポーネントを実際に描画・実行する環境です。iframe 内で独立して動作し、アプリケーションの本番環境に近い状態でコンポーネントをテストできます。

typescript// .storybook/preview.ts での設定例
import type { Preview } from '@storybook/react';
import '../src/styles/globals.css';

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
  decorators: [
    (Story) => (
      <div style={{ margin: '3em' }}>
        <Story />
      </div>
    ),
  ],
};

export default preview;

Preview の主な機能:

ストーリーの描画処理

javascript// Preview内部で実行される描画ロジック(簡略版)
class PreviewRenderer {
  async renderStory(storyId) {
    const story = this.getStory(storyId);
    const Component = story.component;
    const args = story.args;

    // React要素として描画
    const element = React.createElement(Component, args);
    ReactDOM.render(element, this.rootElement);

    // Managerに描画完了を通知
    this.channel.emit('storyRendered', {
      storyId,
      success: true,
    });
  }
}

HMR によるリアルタイム更新

Preview 環境では、ストーリーファイルやコンポーネントファイルの変更を検知し、自動的にリロードします。

mermaidflowchart LR
    File[ファイル変更] -->|検知| Builder[Builder]
    Builder -->|更新通知| Preview[Preview]
    Preview -->|再描画| Component[コンポーネント]
    Component -->|表示更新| Browser[ブラウザ]

図で理解できる要点:

  • ファイル変更からブラウザ表示まで自動化されたフロー
  • Builder が変更検知と Preview 更新の橋渡し役
  • リアルタイム開発体験の実現

Manager:UI 管理・ナビゲーション層の役割

Manager は、Storybook の操作画面(サイドバー、ツールバー、アドオンパネル)を管理します。Preview とは別の Web アプリケーションとして動作し、専用の API を通じて Preview と通信します。

typescript// .storybook/manager.ts での設定例
import { addons } from '@storybook/manager-api';
import { create } from '@storybook/theming/create';

const theme = create({
  base: 'light',
  brandTitle: 'My Custom Storybook',
  brandUrl: 'https://example.com',
  brandImage: './logo.svg',
});

addons.setConfig({
  theme,
  panelPosition: 'bottom',
  selectedPanel: 'controls',
});

ストーリーナビゲーションの仕組み

javascript// Manager内部のナビゲーション管理(簡略版)
class ManagerNavigation {
  constructor() {
    this.stories = new Map();
    this.currentStory = null;
  }

  selectStory(storyId) {
    this.currentStory = storyId;

    // Previewに描画指示を送信
    this.channel.emit('setCurrentStory', { storyId });

    // URLを更新
    this.updateURL(storyId);

    // UIを更新
    this.updateSidebar(storyId);
  }
}

アドオンパネルの統合

Manager は、各アドオンが提供するパネルを統合し、タブ形式で表示します。

typescript// カスタムアドオンパネルの登録例
import { addons, types } from '@storybook/manager-api';

addons.register('my-addon', () => {
  addons.add('my-addon/panel', {
    title: 'My Panel',
    type: types.PANEL,
    render: ({ active, key }) => (
      <div style={{ padding: 20 }}>
        <h2>カスタムパネル</h2>
        <p>ここに独自の機能を実装</p>
      </div>
    ),
  });
});

Builder:ビルドシステムの構造と処理フロー

Builder は Webpack や Vite などのビルドツールを抽象化し、Preview と Manager それぞれに最適化されたビルド環境を提供します。

typescript// Builderの基本構造(概念図)
interface StorybookBuilder {
  // 開発サーバーの起動
  start(options: BuilderOptions): Promise<void>;

  // プロダクションビルド
  build(options: BuilderOptions): Promise<void>;

  // Preview用設定の生成
  getPreviewConfig(): WebpackConfig;

  // Manager用設定の生成
  getManagerConfig(): WebpackConfig;
}

Webpack Builder の内部構造

javascript// Webpack Builderの設定生成ロジック(簡略版)
class WebpackBuilder {
  async getPreviewConfig() {
    const baseConfig = {
      entry: {
        preview: './preview-entry.js',
      },
      module: {
        rules: [
          {
            test: /\.(js|jsx|ts|tsx)$/,
            use: ['babel-loader'],
          },
          {
            test: /\.css$/,
            use: ['style-loader', 'css-loader'],
          },
        ],
      },
      plugins: [
        new HtmlWebpackPlugin({
          template: 'preview.html',
        }),
        new DefinePlugin({
          'process.env.NODE_ENV':
            JSON.stringify('development'),
        }),
      ],
    };

    // ユーザー設定をマージ
    return mergeWebpackConfig(baseConfig, this.userConfig);
  }
}

Vite Builder との違い

Vite Builder を使用する場合、高速な開発体験が得られます。

typescript// .storybook/main.ts でVite Builderを指定
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  framework: {
    name: '@storybook/react-vite', // Vite Builderを使用
    options: {},
  },
  viteFinal: async (config) => {
    // Vite固有の設定をカスタマイズ
    config.define = {
      ...config.define,
      global: 'globalThis',
    };
    return config;
  },
};

export default config;

3 要素の相互作用とデータフローの詳細解説

3 つの要素がどのように連携して Storybook を動作させているか、詳細なデータフローを解説します。

mermaidsequenceDiagram
    participant Dev as 開発者
    participant B as Builder
    participant M as Manager
    participant P as Preview
    participant Add as Addon

    Note over Dev,Add: Storybook起動フロー
    Dev->>B: yarn storybook
    B->>B: main.ts読み込み
    B->>B: ストーリーファイル検索

    par Manager構築
        B->>M: Manager HTML生成
        B->>M: manager.ts読み込み
        M->>Add: アドオンパネル登録
    and Preview構築
        B->>P: Preview HTML生成
        B->>P: preview.ts読み込み
        P->>P: ストーリーメタデータ生成
    end

    M->>P: 初期ストーリー選択
    P->>M: 描画完了通知

    Note over Dev,Add: ユーザー操作フロー
    Dev->>M: ストーリー選択
    M->>P: ストーリー変更指示
    P->>P: コンポーネント描画
    P->>Add: 描画データ送信
    Add->>M: パネル更新

このフローにより、以下の連携が実現されています。

チャンネル通信による要素間データ交換

javascript// チャンネル通信の実装例
import { addons } from '@storybook/preview-api';

// Previewからのデータ送信
const channel = addons.getChannel();
channel.emit('storybook/controls/change', {
  name: 'backgroundColor',
  value: '#ff0000',
});

// Managerでのデータ受信
channel.on('storybook/controls/change', (data) => {
  console.log('Controls changed:', data);
  updateControlsPanel(data);
});

ストーリー情報の同期

typescript// ストーリーメタデータの共有構造
interface StoryMetadata {
  id: string;
  title: string;
  name: string;
  parameters: Record<string, any>;
  args: Record<string, any>;
  argTypes: Record<string, ArgType>;
}

// PreviewからManagerへの同期
class StorySync {
  syncToManager(stories: StoryMetadata[]) {
    this.channel.emit('storiesConfigured', {
      stories: stories.map((story) => ({
        id: story.id,
        title: story.title,
        name: story.name,
        kind: story.title, // 後方互換性のため
      })),
    });
  }
}

図で理解できる要点:

  • チャンネル通信により要素間でリアルタイムデータ交換
  • 各要素が独立性を保ちながら協調動作
  • アドオンも同様の仕組みで統合される

具体例

実際の Storybook 起動時の処理フロー

yarn storybook コマンドを実行したときの詳細な処理フローを追ってみましょう。

bash# Storybookの起動
yarn storybook

フェーズ 1:初期化とファイル検索

javascript// Builder内部の初期化処理
class StorybookStarter {
  async start() {
    console.log('Starting Storybook...');

    // 1. 設定ファイル読み込み
    const mainConfig = await this.loadMainConfig(
      '.storybook/main.ts'
    );
    const previewConfig = await this.loadPreviewConfig(
      '.storybook/preview.ts'
    );

    // 2. ストーリーファイル検索
    const storyPaths = await this.findStories(
      mainConfig.stories
    );
    console.log(`Found ${storyPaths.length} story files`);

    // 3. アドオン解析
    const addons = await this.resolveAddons(
      mainConfig.addons
    );
    console.log(`Loaded ${addons.length} addons`);

    return {
      mainConfig,
      previewConfig,
      storyPaths,
      addons,
    };
  }
}

フェーズ 2:ビルド設定生成

javascript// Webpack設定の生成
async function generateConfigs() {
  const managerConfig = {
    entry: {
      manager: path.resolve(__dirname, 'manager-entry.js'),
    },
    output: {
      path: path.resolve('.storybook-static'),
      filename: 'manager.[hash].js',
    },
    plugins: [
      new HtmlWebpackPlugin({
        template: 'manager.html',
        filename: 'index.html',
      }),
    ],
  };

  const previewConfig = {
    entry: {
      preview: path.resolve(__dirname, 'preview-entry.js'),
    },
    output: {
      path: path.resolve('.storybook-static'),
      filename: 'preview.[hash].js',
    },
    plugins: [
      new HtmlWebpackPlugin({
        template: 'preview.html',
        filename: 'iframe.html',
      }),
    ],
  };

  return { managerConfig, previewConfig };
}

フェーズ 3:開発サーバー起動

javascript// 開発サーバーの起動
class DevServer {
  async start(managerConfig, previewConfig) {
    // 2つの異なるWebpackコンパイラーを起動
    const managerCompiler = webpack(managerConfig);
    const previewCompiler = webpack(previewConfig);

    // Express サーバー設定
    const app = express();

    // Manager用ミドルウェア
    app.use(
      '/manager',
      webpackDevMiddleware(managerCompiler)
    );

    // Preview用ミドルウェア
    app.use(
      '/preview',
      webpackDevMiddleware(previewCompiler)
    );

    // 静的ファイル配信
    app.use('/static', express.static('public'));

    // サーバー起動
    const server = app.listen(6006, () => {
      console.log(
        'Storybook started at http://localhost:6006'
      );
    });

    return server;
  }
}

起動フローの全体像:

mermaidflowchart TD
    Start[yarn storybook] --> Config[設定ファイル読み込み]
    Config --> Stories[ストーリーファイル検索]
    Stories --> Addons[アドオン解析]
    Addons --> WebpackM[Manager Webpack設定生成]
    WebpackM --> WebpackP[Preview Webpack設定生成]
    WebpackP --> Server[開発サーバー起動]
    Server --> Browser[ブラウザでアクセス可能]

    Browser --> ManagerLoad[Manager読み込み]
    Browser --> PreviewLoad[Preview読み込み]
    ManagerLoad --> UI[Storybook UI表示]
    PreviewLoad --> Stories2[ストーリー描画準備完了]

図で理解できる要点:

  • 並列で Manager と Preview 環境を構築
  • 各フェーズが独立しており、効率的な起動プロセス
  • 設定の柔軟性とパフォーマンスの両立

カスタムアドオン作成時の各要素との連携

カスタムアドオンを作成する際、3 つの要素それぞれで異なる処理を実装する必要があります。

Builder 側での登録

javascript// my-addon/src/preset.js - Builder用設定
module.exports = {
  managerEntries: (entry = []) => [
    ...entry,
    require.resolve('./manager'),
  ],
  previewEntries: (entry = []) => [
    ...entry,
    require.resolve('./preview'),
  ],
};

Manager 側での実装

typescript// my-addon/src/manager.tsx - Manager用コンポーネント
import React, { useState } from 'react';
import { addons, types } from '@storybook/manager-api';
import { useChannel } from '@storybook/manager-api';

const MyPanel = () => {
  const [data, setData] = useState(null);

  // Previewからのデータ受信
  useChannel({
    'my-addon/data-updated': (newData) => {
      setData(newData);
    },
  });

  return (
    <div style={{ padding: 20 }}>
      <h3>カスタムアドオンパネル</h3>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

// パネル登録
addons.register('my-addon', () => {
  addons.add('my-addon/panel', {
    title: 'My Addon',
    type: types.PANEL,
    render: MyPanel,
  });
});

Preview 側での実装

typescript// my-addon/src/preview.tsx - Preview用デコレーター
import type { Decorator } from '@storybook/react';
import { useEffect } from '@storybook/preview-api';

export const withMyAddon: Decorator = (
  StoryFn,
  context
) => {
  useEffect(() => {
    // ストーリーの情報を収集
    const storyData = {
      id: context.id,
      title: context.title,
      name: context.name,
      args: context.args,
    };

    // Managerに情報を送信
    const channel = addons.getChannel();
    channel.emit('my-addon/data-updated', storyData);
  }, [context]);

  return StoryFn();
};

// 全ストーリーにデコレーターを適用
export const decorators = [withMyAddon];

3 要素連携の実装パターン

mermaidsequenceDiagram
    participant U as ユーザー
    participant M as Manager<br/>(Panel)
    participant Ch as Channel
    participant P as Preview<br/>(Decorator)
    participant S as Story

    Note over U,S: アドオン動作フロー
    U->>M: ストーリー選択
    M->>Ch: ストーリー変更通知
    Ch->>P: 描画指示受信
    P->>S: ストーリー描画
    S->>P: 描画完了
    P->>Ch: データ収集・送信
    Ch->>M: データ受信
    M->>M: パネル更新
    M->>U: 情報表示

図で理解できる要点:

  • アドオンは 3 要素すべてに対してコードを提供
  • チャンネル通信でリアルタイムデータ交換
  • 各要素の責任分担が明確化

Webpack/Vite ビルダーの切り替え時の内部変化

プロジェクトの要件に応じて、Webpack から Vite へ、またはその逆に Builder を切り替えることがあります。この切り替え時の内部変化を詳しく見てみましょう。

Webpack Builder 使用時

typescript// .storybook/main.ts - Webpack版
import type { StorybookConfig } from '@storybook/react-webpack5';

const config: StorybookConfig = {
  framework: {
    name: '@storybook/react-webpack5',
    options: {
      builder: {
        useSWC: true, // SWCコンパイラーを使用
      },
    },
  },
  webpackFinal: async (config) => {
    // Webpack固有のカスタマイズ
    config.module?.rules?.push({
      test: /\.scss$/,
      use: ['style-loader', 'css-loader', 'sass-loader'],
    });

    return config;
  },
};

export default config;

Vite Builder 使用時

typescript// .storybook/main.ts - Vite版
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  viteFinal: async (config) => {
    // Vite固有のカスタマイズ
    config.plugins
      ?.push
      // カスタムViteプラグインの追加
      ();

    config.define = {
      ...config.define,
      __DEV__: true,
    };

    return config;
  },
};

export default config;

パフォーマンス比較

項目Webpack BuilderVite Builder
初回起動時間20-30 秒3-5 秒
HMR 速度1-3 秒50-200ms
ビルドサイズ大きめ最適化されたサイズ
設定の柔軟性非常に高い高い(制限あり)
エコシステム成熟急成長中

切り替え時の内部処理変化

javascript// Builder切り替え時の内部差異
class BuilderComparison {
  // Webpack Builder
  webpackFlow() {
    return {
      compilation: 'バンドル形式',
      hmr: 'webpack-dev-middleware',
      optimization: 'chunk splitting',
      devServer: 'webpack-dev-server',
    };
  }

  // Vite Builder
  viteFlow() {
    return {
      compilation: 'ESM + esbuild',
      hmr: 'native ES modules',
      optimization: 'tree-shaking + rollup',
      devServer: 'vite dev server',
    };
  }
}

切り替え時の考慮点:

mermaidflowchart LR
    Decision{Builder選択}

    Decision -->|高速開発| Vite[Vite Builder]
    Decision -->|柔軟設定| Webpack[Webpack Builder]

    Vite --> ViteFlow[ESM形式でのインクリメンタルビルド]
    Webpack --> WebpackFlow[バンドル形式での最適化ビルド]

    ViteFlow --> ViteBenefit[高速HMR<br/>軽量バンドル]
    WebpackFlow --> WebpackBenefit[詳細なカスタマイズ<br/>豊富なプラグイン]

図で理解できる要点:

  • Builder 変更は開発体験に大きく影響
  • 各 Builder の特性を理解した選択が重要
  • 設定ファイルの変更だけで切り替え可能

まとめ

3 つの要素が協調動作する全体像の総括

Storybook のPreviewManagerBuilderという 3 つのコア要素は、それぞれが明確な責任を持ちながら、絶妙な連携によって私たちに快適な開発体験を提供してくれています。

Builderは縁の下の力持ちとして、Webpack や Vite などのビルドツールを抽象化し、開発者が複雑な設定に悩むことなく Storybook を使えるようにしています。Previewは実際のコンポーネントが動く舞台として、本番環境に近い条件でのテストを可能にします。そしてManagerは指揮者として、直感的な UI でストーリーの選択や各種設定を管理します。

この 3 要素の協調により、ファイル保存から画面更新まで数百ミリ秒という驚異的な HMR 体験や、アドオンによる機能拡張の柔軟性が実現されているのです。

アーキテクチャ理解によるカスタマイズ・デバッグの効率化

今回解説したアーキテクチャの知識は、日々の開発で以下のような場面で威力を発揮します。

カスタマイズ時の迷いの解消

  • UI 表示の変更 → Preview 設定
  • サイドバーのテーマ変更 → Manager 設定
  • ビルド処理の最適化 → Builder 設定

効率的なトラブルシューティング

  • 表示エラー → Preview 側のログを確認
  • 操作 UI の問題 → Manager 側の設定を確認
  • ビルドエラー → Builder 側の設定を確認

パフォーマンス最適化の指針

  • 高速な HMR が必要 → Vite Builder の採用検討
  • 複雑なビルド要件 → Webpack Builder での詳細設定
  • アドオン開発 → 要素間通信の効率的な実装

Storybook は単なるツールではなく、コンポーネント駆動開発を支える総合的なプラットフォームです。その内部アーキテクチャを理解することで、より効果的に Storybook を活用し、チーム全体の開発効率を向上させることができるでしょう。

3 つの要素が奏でるハーモニーを理解したあなたは、もう Storybook マスターです。

関連リンク