Storybookを使おう！

## はじめに

モダンなフロントエンド開発において、UIコンポーネントの開発と管理は重要な課題の一つです。今回は、その課題を解決するツール「Storybook」について、その特徴と実践的な使い方を紹介します。

## Storybookとは

[Storybook](https://storybook.js.org/)は、UIコンポーネントの開発環境として機能するオープンソースツールです。
以下のような特徴があります。

- コンポーネントを独立した環境で開発・テストできる
- インタラクティブなドキュメントとしても機能する
- 様々なフレームワーク（React、Vue、Angular等）に対応
- 豊富なアドオンによる拡張性

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348593/3b0993c9-bdce-90da-5bec-eb6ba251bd63.png)

## どんな操作性なの？

Amebaのデザインシステム「Spindle」の[コンポーネントカタログ（Storybook）](https://ameba-spindle.web.app/)が公開されており、利用規約の範囲内で手軽にコンポーネントを試すことができます。


## セットアップ方法

### 1. プロジェクトへの導入

```bash
# 既存プロジェクトに Storybook を追加
npx storybook init
```

### 2. 基本的な構成

Storybookをインストールすると、以下のようなファイル構造が生成されます。

```
.storybook/
  ├── main.js  # Storybook の設定ファイル
  └── preview.js  # Story のグローバル設定
src/
  └── stories/  # Story ファイル用ディレクトリ
```
![full-mui-demo-1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348593/dbe061f5-780b-12f1-ec78-32433db59a57.gif)

## Storyファイルの作成単位

Storyファイルの作成対象単位を表にまとめてみましょう。

| 作成対象 | 説明 |
|----------|------|
| コンポーネント<BR>（Component） | 単一のUIコンポーネント<BR>（Button, Input, Cardなど） |
| 複合コンポーネント<BR>（Composite） | 複数のコンポーネントを組み合わせた機能<BR>（Form, Dialog, Tableなど） |
| レイアウト<BR>（Layout） | ページ構造を定義するコンポーネント<BR>（Header, Sidebar, Gridなど） |
| ページ<BR>（Page） | 完全なページ単位のコンポーネント<BR>（Login, Dashboard, Settingsなど） |
| テンプレート<BR>（Template） | 再利用可能なページの雛形<BR>（ArticleTemplate, ProductTemplate など） |
| フック<BR>（Hook） | カスタムフックの使用例<BR>（useForm, useAuth などのカスタムフック） |
| ユーティリティ<BR>（Utility） | 共通の機能やヘルパー<BR>（DataFormatter, ThemeProvider など） |
| パターン<BR>（Pattern） | 特定の UI パターン<BR>（LoadingState, ErrorBoundary など） |

これらの作成対象は、プロジェクトの規模や要件に応じて適切に選択します。
必ずしもすべての対象にStoryを作成する必要はありません。




## Storyの書き方

Storyはコンポーネントの使用例を示すためのファイルです。以下は基本的な作成例です。

### 1. コンポーネント（Component）
```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'],
};

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

export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'ボタン',
  },
};
```

### 2. 複合コンポーネント（Composite）
```typescript
// LoginForm.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { LoginForm } from './LoginForm';

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

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

export const Default: Story = {
  args: {
    onSubmit: (data) => console.log('Form submitted:', data),
  },
};

export const WithError: Story = {
  args: {
    error: 'Invalid credentials',
    onSubmit: (data) => console.log('Form submitted:', data),
  },
};
```

### 3. レイアウト（Layout）
```typescript
// Sidebar.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Sidebar } from './Sidebar';

const meta: Meta<typeof Sidebar> = {
  title: 'Layout/Sidebar',
  component: Sidebar,
  parameters: {
    layout: 'fullscreen',
  },
};

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

export const Default: Story = {
  args: {
    items: [
      { label: 'Dashboard', icon: 'home' },
      { label: 'Settings', icon: 'settings' },
    ],
  },
};
```

### 4. ページ（Page）
```typescript
// DashboardPage.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { DashboardPage } from './DashboardPage';

const meta: Meta<typeof DashboardPage> = {
  title: 'Pages/Dashboard',
  component: DashboardPage,
  parameters: {
    layout: 'fullscreen',
    backgrounds: {
      default: 'light',
    },
  },
};

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

export const Default: Story = {
  args: {
    username: 'John Doe',
    stats: {
      totalUsers: 1234,
      activeUsers: 789,
    },
  },
};
```

### 5. テンプレート（Template）
```typescript
// ArticleTemplate.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { ArticleTemplate } from './ArticleTemplate';

const meta: Meta<typeof ArticleTemplate> = {
  title: 'Templates/Article',
  component: ArticleTemplate,
  parameters: {
    layout: 'fullscreen',
  },
};

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

export const Default: Story = {
  args: {
    title: '記事タイトル',
    content: '記事の本文...',
    author: {
      name: '著者名',
      avatar: '/avatar.jpg',
    },
  },
};
```

### 6. フック（Hook）
```typescript
// useCounter.stories.tsx
import { useCounter } from './useCounter';
import { Meta, StoryObj } from '@storybook/react';

// フックを表示するためのラッパーコンポーネント
const CounterDemo = () => {
  const { count, increment, decrement } = useCounter(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
};

const meta: Meta<typeof CounterDemo> = {
  title: 'Hooks/useCounter',
  component: CounterDemo,
};

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

export const Default: Story = {};
```

### 7. ユーティリティ（Utility）
```typescript
// ThemeProvider.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { ThemeProvider } from './ThemeProvider';
import { Button } from '../components/Button';

const meta: Meta<typeof ThemeProvider> = {
  title: 'Utilities/ThemeProvider',
  component: ThemeProvider,
};

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

export const Default: Story = {
  render: () => (
    <ThemeProvider theme="light">
      <Button>Themed Button</Button>
    </ThemeProvider>
  ),
};
```

### 8. パターン（Pattern）
```typescript
// LoadingState.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { LoadingState } from './LoadingState';

const meta: Meta<typeof LoadingState> = {
  title: 'Patterns/LoadingState',
  component: LoadingState,
};

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

export const Default: Story = {
  args: {
    isLoading: true,
    children: <div>コンテンツ</div>,
  },
};

export const WithCustomSpinner: Story = {
  args: {
    isLoading: true,
    spinner: <div>カスタムローディング...</div>,
    children: <div>コンテンツ</div>,
  },
};
```

## アドオンの活用

Storybookの機能は、アドオンによって拡張できます。特に有用なアドオンを紹介します。

| アドオン名 | 説明 | 
|------------|------|
| [Controls](https://storybook.js.org/addons/@storybook/addon-controls) | コンポーネントのプロパティをGUI上で動的に変更可能 | `npm install @storybook/addon-controls` |
| [Accessibility](https://storybook.js.org/addons/@storybook/addon-a11y) | アクセシビリティチェックを自動実行 | `npm install @storybook/addon-a11y` |
| [Viewport](https://storybook.js.org/addons/@storybook/addon-viewport) | 様々な画面サイズでのレイアウト確認 | `npm install @storybook/addon-viewport` |
| [Actions](https://storybook.js.org/addons/@storybook/addon-actions) | ユーザーイベントのモニタリング | `npm install @storybook/addon-actions` |
| [Docs](https://storybook.js.org/addons/@storybook/addon-docs) | 自動ドキュメント生成 | `npm install @storybook/addon-docs` |
| [Interactions](https://storybook.js.org/addons/@storybook/addon-interactions) | インタラクティブなテストの作成と実行 | `npm install @storybook/addon-interactions` |

## CI/CDへの統合

Storybookは、CIプロセスに組み込むことができます。
以下に主要なCIプラットフォームでの設定例を示します。

### GitHub Actions

```yaml
# .github/workflows/storybook.yml
name: Storybook Tests
on: push
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
      - run: npm ci
      - run: npm run build-storybook
      - run: npm run test-storybook
```

### GitLab CI

```yaml
# .gitlab-ci.yml
storybook:
  image: node:latest
  stage: test
  script:
    - npm ci
    - npm run build-storybook
    - npm run test-storybook
  artifacts:
    paths:
      - storybook-static/
```

## まとめ

Storybookを導入することで、以下のメリットが得られます。
Storybookを使おう！

- コンポーネントの開発効率の向上

- 品質管理の効率化
- ドキュメント作成の自動化

## 参考リンク

- [Storybook 公式ドキュメント](https://storybook.js.org/)
- [Storybook GitHub](https://github.com/storybookjs/storybook)