Storybook でドキュメントを自動生成する方法

コンポーネント開発において、ドキュメント作成は重要な作業ですが、手動での管理は想像以上に大変な作業です。コードが更新されるたびにドキュメントも更新する必要があり、気がつけば実装とドキュメントの内容が食い違っているという経験をお持ちの方も多いのではないでしょうか。
そんな課題を解決してくれるのが、Storybook のドキュメント自動生成機能です。TypeScript の型定義や JSDoc コメントから自動的にドキュメントを生成し、常に最新の情報を保持できる仕組みを構築できます。
今回は、Storybook を使ったドキュメント自動生成の具体的な方法について、実際の設定手順とともに詳しく解説していきます。
手動ドキュメント作成の課題
メンテナンスコストの高さ
従来の手動ドキュメント作成では、以下のような課題が発生しがちです。
課題 | 詳細 | 影響 |
---|---|---|
更新漏れ | コード変更時のドキュメント更新忘れ | 情報の不整合 |
重複作業 | 同じ情報を複数箇所で管理 | 工数の無駄 |
品質のばらつき | 作成者によって記述レベルが異なる | 利用者の混乱 |
実際の開発現場では、プロジェクトが進むにつれてドキュメントの更新が追いつかなくなり、結果的に「使えないドキュメント」になってしまうケースが多く見られます。
情報の不整合問題
手動管理で最も深刻なのが、コードとドキュメントの不整合です。
typescript// 実際のコンポーネント
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick: () => void;
}
// ドキュメントでは古い情報のまま
// variant: 'default' | 'primary' | 'secondary' ← 'danger' が抜けている
// size プロパティの記載なし
このような不整合は、開発者の混乱を招き、バグの原因にもなりかねません。
開発速度への影響
ドキュメント作成にかかる時間は、想像以上に開発速度に影響を与えます。
- コンポーネント実装:30 分
- ドキュメント作成:20 分
- 更新時のメンテナンス:10 分 × 更新回数
この時間を自動化できれば、より多くの時間を実装やテストに充てることができますね。
Storybook でできるドキュメント自動生成の種類
Storybook では、複数の方法でドキュメントを自動生成できます。それぞれの特徴を理解して、プロジェクトに最適な方法を選択しましょう。
自動生成機能の全体像
機能 | 生成内容 | データソース | 設定難易度 |
---|---|---|---|
Autodocs | 基本的なドキュメントページ | Story + 型定義 | 低 |
Props Table | プロパティ一覧表 | TypeScript 型 | 低 |
Controls | インタラクティブなプロパティ操作 | Story の args | 中 |
JSDoc 連携 | コメントからの説明文 | JSDoc コメント | 中 |
MDX カスタマイズ | 高度なドキュメント | MDX ファイル | 高 |
段階的な導入アプローチ
最初からすべての機能を使う必要はありません。以下の順序で段階的に導入することをお勧めします。
- 基本設定:Autodocs の有効化
- 型連携:Props Table の自動生成
- コメント活用:JSDoc との連携
- カスタマイズ:MDX による拡張
この順序で進めることで、無理なく高品質なドキュメントを構築できます。
Autodocs 機能の活用
Autodocs は、Storybook 6.5 以降で利用できる強力な自動ドキュメント生成機能です。最小限の設定で、美しいドキュメントページを自動生成できます。
基本設定
まず、.storybook/main.js
で Autodocs を有効化しましょう。
javascriptmodule.exports = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-docs',
],
docs: {
autodocs: 'tag', // 'tag' または true を指定
},
framework: {
name: '@storybook/nextjs',
options: {},
},
};
この設定により、タグ付きのストーリーに対して自動的にドキュメントページが生成されます。
ストーリーファイルでの設定
次に、コンポーネントのストーリーファイルで Autodocs を有効化します。
typescript// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'], // この行を追加
parameters: {
docs: {
description: {
component:
'プライマリボタンコンポーネントです。様々なバリエーションで使用できます。',
},
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary Button',
},
};
生成されるドキュメントの内容
Autodocs により、以下の内容が自動生成されます。
- コンポーネント概要:description で指定した説明文
- Props Table:TypeScript 型定義から自動生成
- Story 一覧:定義したすべてのストーリー
- Controls パネル:インタラクティブな操作 UI
これらの情報が、コードの変更に合わせて自動的に更新されるため、常に最新の状態を維持できます。
Props Table の自動生成
Props Table は、コンポーネントのプロパティ情報を表形式で表示する機能です。TypeScript の型定義から自動的に生成されるため、手動でのメンテナンスが不要になります。
TypeScript インターフェースの活用
まず、コンポーネントの Props を TypeScript で定義します。
typescript// Button.tsx
import React from 'react';
/**
* ボタンコンポーネントのプロパティ
*/
export interface ButtonProps {
/**
* ボタンのバリエーション
* @default 'primary'
*/
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
/**
* ボタンのサイズ
* @default 'md'
*/
size?: 'sm' | 'md' | 'lg';
/**
* 無効状態かどうか
* @default false
*/
disabled?: boolean;
/**
* ボタンの表示テキスト
*/
children: React.ReactNode;
/**
* クリック時のイベントハンドラー
*/
onClick?: () => void;
/**
* 追加のCSS クラス名
*/
className?: string;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
disabled = false,
children,
onClick,
className,
}) => {
// コンポーネントの実装
return (
<button
className={`btn btn-${variant} btn-${size} ${
className || ''
}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
};
JSDoc コメントの活用
上記のコードでは、JSDoc コメントを使用してプロパティの説明を記述しています。これらの情報は自動的に Props Table に反映されます。
JSDoc タグ | 用途 | 表示例 |
---|---|---|
/** */ | プロパティの説明 | Props Table の Description 列 |
@default | デフォルト値 | Props Table の Default 列 |
@deprecated | 非推奨マーク | 取り消し線付きで表示 |
高度な型定義の例
複雑な型定義も適切に Props Table に反映されます。
typescript// 高度な型定義の例
export interface AdvancedButtonProps {
/**
* ボタンのテーマ設定
*/
theme: {
primary: string;
secondary: string;
text: string;
};
/**
* アイコンの設定
*/
icon?: {
name: string;
position: 'left' | 'right';
size?: number;
};
/**
* イベントハンドラーの設定
*/
handlers: {
onClick?: (event: React.MouseEvent) => void;
onHover?: (event: React.MouseEvent) => void;
};
}
このような複雑な型定義も、Props Table で適切に表示され、開発者が理解しやすい形で情報を提供できます。
TypeScript 型定義からの自動生成
TypeScript の型システムを最大限活用することで、より詳細で正確なドキュメントを自動生成できます。
型定義の最適化
ドキュメント生成を意識した型定義を作成しましょう。
typescript// types/Button.types.ts
/**
* ボタンのバリエーション定義
*/
export type ButtonVariant =
| 'primary'
| 'secondary'
| 'danger'
| 'ghost';
/**
* ボタンのサイズ定義
*/
export type ButtonSize = 'sm' | 'md' | 'lg';
/**
* ボタンの状態定義
*/
export interface ButtonState {
/**
* ローディング状態
*/
loading: boolean;
/**
* 無効状態
*/
disabled: boolean;
/**
* アクティブ状態
*/
active: boolean;
}
/**
* ボタンコンポーネントの基本プロパティ
*/
export interface BaseButtonProps {
/**
* ボタンのバリエーション
* @default 'primary'
*/
variant?: ButtonVariant;
/**
* ボタンのサイズ
* @default 'md'
*/
size?: ButtonSize;
/**
* ボタンの状態
*/
state?: Partial<ButtonState>;
}
/**
* 最終的なボタンプロパティ
*/
export interface ButtonProps extends BaseButtonProps {
/**
* ボタンの表示内容
*/
children: React.ReactNode;
/**
* クリック時のイベントハンドラー
*/
onClick?: (
event: React.MouseEvent<HTMLButtonElement>
) => void;
}
Union Types の活用
Union Types を使用することで、選択可能な値を明確に示せます。
typescript// アイコンボタンの型定義例
export interface IconButtonProps {
/**
* アイコンの種類
* Font Awesome のアイコン名を指定
*/
icon:
| 'home'
| 'user'
| 'settings'
| 'search'
| 'menu'
| 'close';
/**
* アイコンのスタイル
*/
iconStyle?: 'solid' | 'regular' | 'light';
/**
* ボタンの形状
*/
shape?: 'square' | 'circle' | 'rounded';
}
条件付き型の活用
より高度な型定義により、プロパティ間の関係性も表現できます。
typescript// 条件付き型の例
export interface ConditionalButtonProps {
/**
* ボタンのタイプ
*/
type: 'button' | 'submit' | 'link';
/**
* type が 'link' の場合は href が必須
*/
href: string extends infer T
? T extends 'link'
? string
: never
: never;
/**
* type が 'submit' の場合のフォーム関連プロパティ
*/
form?: string extends infer T
? T extends 'submit'
? string
: never
: never;
}
このような高度な型定義により、Props Table でより詳細な情報を提供できます。
MDX によるドキュメント強化
MDX(Markdown + JSX)を使用することで、自動生成されたドキュメントをさらに充実させることができます。
基本的な MDX ファイルの作成
コンポーネントと同じディレクトリに .stories.mdx
ファイルを作成します。
mdx<!-- Button.stories.mdx -->
import {
Meta,
Story,
Canvas,
ArgsTable,
} from '@storybook/addon-docs';
import { Button } from './Button';
<Meta
title='Components/Button'
component={Button}
argTypes={{
variant: {
control: { type: 'select' },
options: ['primary', 'secondary', 'danger', 'ghost'],
description:
'ボタンの見た目のバリエーションを指定します',
},
size: {
control: { type: 'radio' },
options: ['sm', 'md', 'lg'],
description: 'ボタンのサイズを指定します',
},
}}
/>
# Button コンポーネント
ボタンコンポーネントは、ユーザーのアクションを促すための重要な UI 要素です。
様々なバリエーションとサイズを提供し、アクセシビリティにも配慮した実装となっています。
# 使用方法
基本的な使用方法は以下の通りです:
<Canvas>
<Story name='基本的な使用例'>
<Button
variant='primary'
onClick={() => alert('クリックされました!')}
>
クリックしてください
</Button>
</Story>
</Canvas>
# バリエーション
## Primary ボタン
最も重要なアクションに使用します。
<Canvas>
<Story name='Primary'>
<Button variant='primary'>Primary Button</Button>
</Story>
</Canvas>
## Secondary ボタン
補助的なアクションに使用します。
<Canvas>
<Story name='Secondary'>
<Button variant='secondary'>Secondary Button</Button>
</Story>
</Canvas>
# プロパティ一覧
<ArgsTable of={Button} />
# 使用上の注意点
## アクセシビリティ
- `onClick` ハンドラーが設定されていない場合、キーボードナビゲーションに影響する可能性があります
- `disabled` 状態の場合、適切な `aria-disabled` 属性が設定されます
## パフォーマンス
- 大量のボタンを表示する場合は、`React.memo` の使用を検討してください
- `onClick` ハンドラーは `useCallback` でメモ化することを推奨します
# 関連コンポーネント
- [IconButton](/docs/components-iconbutton--docs) - アイコン付きボタン
- [LinkButton](/docs/components-linkbutton--docs) - リンク機能付きボタン
インタラクティブな例の追加
MDX では、実際に動作するコンポーネントを埋め込むことができます。
mdx# インタラクティブな例
以下は、実際にクリックして動作を確認できる例です:
<Canvas>
<Story name='インタラクティブ例'>
{() => {
const [count, setCount] = React.useState(0);
return (
<div>
<p>クリック回数: {count}</p>
<Button
variant='primary'
onClick={() => setCount(count + 1)}
>
カウントアップ ({count})
</Button>
</div>
);
}}
</Story>
</Canvas>
コードスニペットの追加
実際の使用例をコードブロックで示すことも重要です。
mdx# 実装例
## React での基本的な使用方法
```tsx
import { Button } from '@/components/Button';
function MyComponent() {
const handleClick = () => {
console.log('ボタンがクリックされました');
};
return (
<Button
variant='primary'
size='md'
onClick={handleClick}
>
送信する
</Button>
);
}
```
Next.js での使用方法
tsximport { Button } from '@/components/Button';
import { useRouter } from 'next/router';
function NavigationButton() {
const router = useRouter();
const handleNavigate = () => {
router.push('/dashboard');
};
return (
<Button variant='secondary' onClick={handleNavigate}>
ダッシュボードへ
</Button>
);
}
graphql
# 実際の設定手順と具体例
ここまでの内容を踏まえて、実際のプロジェクトでドキュメント自動生成を導入する手順を説明します。
## プロジェクトの初期設定
まず、必要なパッケージをインストールします。
```bash
# Storybook の初期化(既存プロジェクトの場合)
yarn dlx storybook@latest init
# 追加のアドオンをインストール
yarn add --dev @storybook/addon-docs @storybook/addon-essentials
設定ファイルの更新
.storybook/main.js
を以下のように設定します。
javascriptmodule.exports = {
stories: [
'../src/**/*.stories.@(js|jsx|ts|tsx)',
'../src/**/*.stories.mdx',
],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-docs',
'@storybook/addon-controls',
'@storybook/addon-actions',
],
docs: {
autodocs: 'tag',
defaultName: 'ドキュメント', // デフォルトのドキュメントタブ名
},
framework: {
name: '@storybook/nextjs',
options: {},
},
typescript: {
check: false,
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) =>
prop.parent
? !/node_modules/.test(prop.parent.fileName)
: true,
},
},
};
TypeScript 設定の最適化
tsconfig.json
でドキュメント生成に必要な設定を追加します。
json{
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "dom.iterable", "ES6"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"declaration": true,
"declarationMap": true
},
"include": ["src", ".storybook"]
}
実践的なコンポーネント例
実際のプロジェクトで使用できるコンポーネントとストーリーの例を示します。
typescript// src/components/Card/Card.tsx
import React from 'react';
import './Card.css';
/**
* カードコンポーネントのプロパティ
*/
export interface CardProps {
/**
* カードのタイトル
*/
title: string;
/**
* カードの説明文
*/
description?: string;
/**
* カードの画像URL
*/
imageUrl?: string;
/**
* カードのサイズ
* @default 'medium'
*/
size?: 'small' | 'medium' | 'large';
/**
* 影の深さ
* @default 'medium'
*/
elevation?: 'none' | 'small' | 'medium' | 'large';
/**
* クリック可能かどうか
* @default false
*/
clickable?: boolean;
/**
* クリック時のイベントハンドラー
*/
onClick?: () => void;
/**
* 子要素
*/
children?: React.ReactNode;
}
/**
* カードコンポーネント
*
* 情報をカード形式で表示するためのコンポーネントです。
* 画像、タイトル、説明文を含めることができ、クリック可能な設定も可能です。
*/
export const Card: React.FC<CardProps> = ({
title,
description,
imageUrl,
size = 'medium',
elevation = 'medium',
clickable = false,
onClick,
children,
}) => {
const cardClasses = [
'card',
`card--${size}`,
`card--elevation-${elevation}`,
clickable ? 'card--clickable' : '',
]
.filter(Boolean)
.join(' ');
return (
<div
className={cardClasses}
onClick={clickable ? onClick : undefined}
role={clickable ? 'button' : undefined}
tabIndex={clickable ? 0 : undefined}
>
{imageUrl && (
<div className='card__image'>
<img src={imageUrl} alt={title} />
</div>
)}
<div className='card__content'>
<h3 className='card__title'>{title}</h3>
{description && (
<p className='card__description'>{description}</p>
)}
{children && (
<div className='card__children'>{children}</div>
)}
</div>
</div>
);
};
対応するストーリーファイル
typescript// src/components/Card/Card.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Card } from './Card';
const meta: Meta<typeof Card> = {
title: 'Components/Card',
component: Card,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'情報をカード形式で表示するコンポーネントです。画像、タイトル、説明文を含めることができます。',
},
},
},
argTypes: {
size: {
control: { type: 'select' },
options: ['small', 'medium', 'large'],
description: 'カードのサイズを指定します',
},
elevation: {
control: { type: 'select' },
options: ['none', 'small', 'medium', 'large'],
description: '影の深さを指定します',
},
clickable: {
control: { type: 'boolean' },
description: 'クリック可能にするかどうかを指定します',
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: 'サンプルカード',
description:
'これはサンプルのカードコンポーネントです。',
},
};
export const WithImage: Story = {
args: {
title: '画像付きカード',
description: '美しい画像と共に情報を表示します。',
imageUrl: 'https://via.placeholder.com/300x200',
},
};
export const Clickable: Story = {
args: {
title: 'クリック可能なカード',
description: 'このカードはクリックできます。',
clickable: true,
onClick: () => alert('カードがクリックされました!'),
},
};
export const Large: Story = {
args: {
title: '大きなカード',
description:
'サイズが大きく設定されたカードです。より多くの情報を表示できます。',
size: 'large',
elevation: 'large',
},
};
export const WithChildren: Story = {
args: {
title: 'カスタムコンテンツ',
description: '子要素を含むカードの例です。',
children: (
<div style={{ marginTop: '16px' }}>
<button style={{ marginRight: '8px' }}>
アクション1
</button>
<button>アクション2</button>
</div>
),
},
};
運用フローの確立
チーム開発での運用フローを確立しましょう。
フェーズ | 作業内容 | 担当者 | 自動化レベル |
---|---|---|---|
開発 | コンポーネント実装 + 型定義 | 開発者 | - |
ストーリー作成 | 基本ストーリーの作成 | 開発者 | 一部自動 |
ドキュメント確認 | 自動生成された内容の確認 | 開発者 | 完全自動 |
レビュー | ドキュメント品質のチェック | レビュアー | - |
デプロイ | Storybook のビルド・公開 | CI/CD | 完全自動 |
このフローにより、品質の高いドキュメントを効率的に維持できます。
まとめ
Storybook のドキュメント自動生成機能を活用することで、手動管理の課題を大幅に解決できます。
導入効果
- 工数削減:ドキュメント作成・更新時間を 70%削減
- 品質向上:コードとドキュメントの不整合を防止
- 開発効率:最新情報への即座なアクセス
成功のポイント
- 型定義の充実:TypeScript の型システムを最大限活用
- JSDoc の活用:適切なコメントでより詳細な情報を提供
- 段階的導入:基本機能から始めて徐々に高度な機能を追加
- チーム運用:明確なルールとフローの確立
今後の展望
ドキュメント自動生成は、今後さらに進化していく分野です。AI を活用した説明文の自動生成や、デザインツールとの連携強化など、より便利な機能が期待されています。
まずは基本的な Autodocs 機能から始めて、プロジェクトの成長とともに機能を拡張していくことをお勧めします。適切に設定されたドキュメント自動生成システムは、開発チームの生産性を大幅に向上させる強力なツールとなるでしょう。
関連リンク
- blog
ドキュメントは「悪」じゃない。アジャイル開発で「ちょうどいい」ドキュメントを見つけるための思考法
- blog
「アジャイルコーチ」って何する人?チームを最強にする影の立役者の役割と、あなたがコーチになるための道筋
- blog
ペアプロって本当に効果ある?メリットだけじゃない、現場で感じたリアルな課題と乗り越え方
- blog
TDDって結局何がいいの?コードに自信が持てる、テスト駆動開発のはじめの一歩
- blog
「昨日やったこと、今日やること」の報告会じゃない!デイリースクラムをチームのエンジンにするための3つの問いかけ
- blog
燃え尽きるのは誰だ?バーンダウンチャートでプロジェクトの「ヤバさ」をチームで共有する方法