Storybookのアーキテクチャ:Channelでつながる三つの世界
前提知識
- ›Web開発の基礎知識(HTML・CSS・JavaScript)
- ›Node.jsおよびnpm/yarnの基本的な理解
- ›モノレポの概念に関する基礎知識
- ›iframeとクロスフレーム通信の理解
Storybookのアーキテクチャ:Channelでつながる三つの世界
StorybookはUIコンポーネントを独立した環境で開発するためのツールとして、業界で最も広く使われています。しかし、一見すると「サイドバー+プレビューウィンドウ」というシンプルな構成に見えながら、その内部は驚くほど深く設計されています。実行時にStorybookは三つの独立した環境として動作します。Node.jsサーバー、ブラウザベースのManager UI、そしてサンドボックス化されたPreview iframeです。これら3つはイベント駆動のchannel抽象層を通じて通信します。かつては多数の@storybook/*パッケージに分散していたコードベースは、現在では40以上のサブパスエクスポートを持つ単一のstorybookパッケージに統合され、整理されたビルドシステムのもとで管理されています。
この記事では、そのアーキテクチャを端から端まで追っていきます。ターミナルでstorybook devと入力した瞬間から、プリセット解決・サーバー起動・ブラウザ起動に至るまでの流れを完全にトレースします。読み終えるころには、Storybookの各ピースがどのようにつながっているか、明確なメンタルモデルが手に入るはずです。
統合されたコアパッケージ
以前のStorybookを使うには、@storybook/core-server・@storybook/channels・@storybook/preview-api・@storybook/manager-apiなど、数多くのパッケージを個別にインストールする必要がありました。Storybookチームはこれらすべてをstorybookという単一パッケージに統合し、code/coreから公開するようにしました。
package.jsonはサブパスエクスポートを通じてAPIの全公開面を提供しています。
code/core/package.json#L48-L220
各エクスポートは一貫したパターンに従っています。型定義ファイルを指すtypes条件、ソースコードを指すcode条件(ツール向け)、そしてコンパイル済み成果物を指すdefault条件です。storybook/preview-apiやstorybook/manager-apiなどの公開APIはトップレベルに置かれ、内部モジュールはstorybook/internal/*以下に配置されます。
| エクスポートパス | 用途 | 実行環境 |
|---|---|---|
storybook/preview-api |
ストーリーのレンダリング、decorator、args | ブラウザ(iframe) |
storybook/manager-api |
Managerの状態管理、addonフック | ブラウザ(親フレーム) |
storybook/test |
テストユーティリティ(expect、fn) |
ブラウザ(iframe) |
storybook/internal/core-server |
dev/buildサーバーのオーケストレーション | Node.js |
storybook/internal/channels |
環境間通信 | 両方 |
storybook/internal/core-events |
イベント名の定数定義 | 両方 |
ヒント: Storybookのソースコードを読む際、
storybook/internal/*からのインポートはマイナーバージョン間で変更される可能性のある内部APIです。internalが付かない公開APIが、安定した契約として機能します。
三つの環境:Server・Manager・Preview
Storybookの三環境モデルは設計上の気まぐれではありません。コンポーネントのレンダリングを開発ツールから完全に分離しなければならないという根本的な制約から生まれた必然の構造です。
flowchart TB
subgraph Node["Node.js Server"]
CS[Core Server]
SIG[StoryIndexGenerator]
PS[Preset System]
end
subgraph Browser["Browser Window"]
subgraph Manager["Manager (Parent Frame)"]
Sidebar[Sidebar]
Toolbar[Toolbar]
Addons[Addon Panels]
end
subgraph Preview["Preview (iframe)"]
Story[Rendered Story]
Decorators[Decorators]
PlayFn[Play Functions]
end
end
Node -->|"WebSocket (dev only)"| Manager
Node -->|"WebSocket (dev only)"| Preview
Manager <-->|"PostMessage"| Preview
Server(Node.js)はファイルシステムへのアクセス、ストーリーのインデックス生成、静的ファイルの配信、ビルドパイプラインを担います。HTTPサーバーとしてPolkaを使用し、リアルタイム通信用のWebSocketエンドポイントを提供します。
Managerは、サイドバー・ツールバー・addonパネルをレンダリングするフルのReactアプリケーションです。親ブラウザフレームに存在し、esbuildでビルドされます。
Previewはサンドボックス化されたiframeの中に存在します。実際のコンポーネントがレンダリングされる場所です。フレームワークの選択に応じて、Viteまたはwebpackでビルドされます。iframeによる分離によって、コンポーネントのスタイル・グローバル変数・副作用がStorybookのUIに漏れることを防ぎ、逆方向の影響も遮断します。
モノレポのレイアウト
Storybookリポジトリはcode/ディレクトリ配下のモノレポとして整理されています。主要な構造は次のとおりです。
| ディレクトリ | 用途 | 例 |
|---|---|---|
code/core/ |
統合されたstorybookパッケージ — すべてここから公開される |
channels、preview-api、manager-api、core-server |
code/frameworks/ |
builderとrendererを結合するフレームワーク固有のプリセット | react-vite、angular、svelte-vite |
code/builders/ |
ビルドツールの統合 | builder-vite、builder-webpack5 |
code/renderers/ |
フレームワーク固有のレンダリング実装 | react、vue3、svelte |
code/addons/ |
公式addonパッケージ | docs、a11y、themes、vitest |
code/lib/ |
スタンドアロンのユーティリティライブラリ | cli-storybook、create-storybook、codemod |
scripts/ |
ビルド・チェック・サンドボックスのオーケストレーション | build-package.ts、タスクランナー |
重要なのは、frameworkはプリセットの薄いラッパーに過ぎないという点です。たとえばreact-viteは、使用するbuilderとrendererを宣言するだけです。
code/frameworks/react-vite/src/preset.ts#L5-L8
export const core: PresetProperty<'core'> = {
builder: import.meta.resolve('@storybook/builder-vite'),
renderer: import.meta.resolve('@storybook/react/preset'),
};
ビルド設定・devサーバーのセットアップ・ストーリーのインデックス生成といった残りの処理はすべてcoreが担い、プリセットシステムを通じて組み合わされます。
CLIディスパッチャー
storybook devを実行すると、エントリーポイントとなるのがcode/core/src/bin/dispatcher.tsのディスパッチャーです。シンプルさが際立つ実装で、各コマンドをどこへ送るかを決める小さなルーティング関数です。
code/core/src/bin/dispatcher.ts#L36-L87
flowchart LR
CLI["storybook <command>"] --> Check{Command?}
Check -->|"dev / build / index"| Core["core.js (internal)"]
Check -->|"init"| Create["create-storybook (npx)"]
Check -->|"upgrade / doctor / ..."| SBCli["@storybook/cli (npx)"]
ルーティングパスは三つあります。
- コアコマンド(
dev、build、index)は、動的なimport()でコンパイル済みのコアバイナリから直接ロードされます。 - **
init**はcreate-storybookパッケージへルーティングされます。バージョンが一致するローカルインストールがあればそちらを使い、なければnpx経由で実行します。 - その他すべて(
upgrade、doctor、automigrate)は@storybook/cliへ送られ、同様にバージョン一致チェックの後、必要に応じてnpxにフォールバックします。
ルーティングが始まる前に、ディスパッチャーはNode.jsのバージョンを検証します。StorybookはNode 20.19以上またはNode 22.12以上を必要とし、24行目でハードゲートとして機能します。警告ではなく、完全なブロックです。
storybook devをエンドツーエンドで追う
storybook devを実行してからブラウザにストーリーが表示されるまでの全工程を追っていきましょう。
sequenceDiagram
participant CLI as CLI Dispatcher
participant BDS as buildDevStandalone
participant Load as loadAllPresets
participant Server as Polka Server
participant MB as Manager Builder (esbuild)
participant PB as Preview Builder (Vite)
participant Browser as Browser
CLI->>BDS: import core.js → buildDevStandalone()
BDS->>BDS: Resolve port, configDir, outputDir
BDS->>Load: First pass (determine builder)
Load-->>BDS: Builder info + core config
BDS->>BDS: Create WebSocket channel
BDS->>Load: Second pass (all presets)
Load-->>BDS: Full options with presets
BDS->>Server: storybookDevServer(options, server)
Server->>MB: managerBuilder.start()
Server->>PB: previewBuilder.start()
Server->>Server: app.listen(port)
Server->>Browser: openInBrowser(address)
旅の始まりはbuildDevStandalone()です。
code/core/src/core-server/build-dev.ts#L44-L51
この関数がdev体験全体をオーケストレーションします。ポートの解決(指定ポートが使用中の場合はプロンプト表示)、メイン設定の読み込み、フレームワークの検証を行った後、重要な二段階プリセットロードを実行します。一段階目でbuilderを特定し、二段階目でbuilderのオーバーライドプリセットを含む全プリセットをロードします。
実際のHTTPサーバーとして使われるのは、Expressの軽量代替であるPolkaです。
code/core/src/core-server/dev-server.ts#L28-L34
devサーバーはmiddleware(圧縮・ホスト検証・アクセス制御・キャッシュ)をセットアップし、ストーリーインデックスを配信するindex.jsonルートを登録した後、両方のbuilderを並列で起動します。Manager builderはesbuildを使用し、Preview builderはフレームワークに応じてViteまたはwebpackを使用します。
Node・Browser・Runtimeのビルド分類
coreパッケージのbuild-config.tsは、すべてのエントリーポイントを四つのカテゴリーに分類しています。
code/core/build-config.ts#L22-L210
flowchart TD
BC[build-config.ts] --> Node[node entries]
BC --> Browser[browser entries]
BC --> Runtime[runtime entries]
BC --> GR[globalizedRuntime entries]
Node --> N1[core-server]
Node --> N2[node-logger]
Node --> N3[telemetry]
Node --> N4[csf-tools]
Node --> N5[common utilities]
Browser --> B1[preview-api]
Browser --> B2[manager-api]
Browser --> B3[channels]
Browser --> B4[theming]
Browser --> B5[components]
Runtime --> R1[preview/runtime]
Runtime --> R2[manager/globals-runtime]
Runtime --> R3[mocker-runtime]
GR --> G1[manager/runtime.tsx]
Node entriesはサーバーサイドのコードで、fsやpathなどのNode APIを使用できます。Node.jsをターゲットプラットフォームとしてバンドルされます。
Browser entriesはブラウザで動作するクライアントサイドのコードです。ブラウザ互換のターゲット(Chrome 100以上、Safari 15以上、Firefox 91以上)でバンドルされます。
Runtime entriesは特殊なカテゴリーです。Preview iframeにスタンドアロンスクリプトとして注入されるため、コード分割なしでバンドルしなければならないブラウザコードです。
Globalized runtime entries(Manager runtimeのみ)は、addonの相互運用性を確保するために、エクスポートをwindowオブジェクトのグローバルとして公開するようラップされます。
この分類はビルドの最適化にとどまりません。安全性の境界線でもあります。browserコードからnode entryをインポートしたり(あるいはその逆)、実行時に破綻します。ビルドシステムは各カテゴリーを異なるesbuild設定でビルドすることで、この分離を強制します。
ヒント: Storybookにコントリビュートして新しいモジュールを追加するとき、どのカテゴリーに属するかを決めることが最初のアーキテクチャ上の判断のひとつになります。「このコードはファイルシステムが必要か?iframeの中で動くか?グローバルとして公開する必要があるか?」と自問してみましょう。
次のステップ
ここまでで高レベルなアーキテクチャの全体像を掴みました。3つの環境、分類されたビルドエントリーを持つ統合パッケージ、そして二段階プリセットロードシステムにディスパッチするCLIです。しかし、実際の設定がどのように機能するかについては、まだ表面を引っ掻いたに過ぎません。次の記事では、プリセットシステムを深掘りします。数十のソースから設定をfunctional reduceチェーンで合成するStorybookの設定カーネルです。