T-CREATOR

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

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 ファイル

段階的な導入アプローチ

最初からすべての機能を使う必要はありません。以下の順序で段階的に導入することをお勧めします。

  1. 基本設定:Autodocs の有効化
  2. 型連携:Props Table の自動生成
  3. コメント活用:JSDoc との連携
  4. カスタマイズ: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%削減
  • 品質向上:コードとドキュメントの不整合を防止
  • 開発効率:最新情報への即座なアクセス

成功のポイント

  1. 型定義の充実:TypeScript の型システムを最大限活用
  2. JSDoc の活用:適切なコメントでより詳細な情報を提供
  3. 段階的導入:基本機能から始めて徐々に高度な機能を追加
  4. チーム運用:明確なルールとフローの確立

今後の展望

ドキュメント自動生成は、今後さらに進化していく分野です。AI を活用した説明文の自動生成や、デザインツールとの連携強化など、より便利な機能が期待されています。

まずは基本的な Autodocs 機能から始めて、プロジェクトの成長とともに機能を拡張していくことをお勧めします。適切に設定されたドキュメント自動生成システムは、開発チームの生産性を大幅に向上させる強力なツールとなるでしょう。

関連リンク