Storybook解説（Next.js, TailwindCSS環境）

# はじめに
最近実務でStorybookを触る機会があり、色々調べたものを備忘録として残しておきます。
なかなか面白かったので、興味のある方は覗いていってください！
Storybookに触れたことがない方・Storybookの導入を検討されている方に見ていただければ嬉しいです！
本記事を読めば、実際に動かしてみて、Storybookがどんなものなのかを理解することができます！

# Storybookとは
そもそもStorybookとは何かですが、「UIコンポーネントカタログ」の一種で、フロントエンド開発において、必要不可欠なサービスです。
React、Vue、Angularなど様々なフレームワークに対応していて、無料で利用することができます。

# UIコンポーネントカタログとは
Storybookを初めて聞いた方は、UIコンポーネントカタログと聞いてもイメージしづらいと思うので、その説明も併せて残しておきます。
UIコンポーネントカタログとは何かですが、Web開発で使用しているコンポーネントを一覧にするツールのことです。
ボタンのコンポーネントであれば、画面からボタンの色、ラベルの色、活性・非活性、サイズなどを変更して確認できます。

# Storybook（UIコンポーネントカタログ）を導入するメリット
いくつかあるので、箇条書きで下記にまとめました！
- コンポーネント単体でブラウザに表示可能なため、レイアウトや挙動を確認しながらコーディングができる
- 画面からUIの操作が可能なため、ソースコードが読めない方でも扱える
- UIがどのような動きをするかすぐに確認できる
- バックエンドとは切り離して作ることができるため、バックエンドの実装を待たずとも開発ができる

# 実践
まずは触ってみてStorybookとは何なのかの理解を深めていきましょう。
ソースコードは用意しているので、下記手順を行ってみてください。

※今回はホスト環境の違いで動かないということがないように、VSCodeからDockerコンテナに入り、動作確認をするので、問題なく動くかと思います。
#### プロジェクトのクローン
ターミナルを開き、下記手順を実行してください。
```bash
# ホームディレクトリに移動
cd

# プロジェクトをクローン
git clone https://github.com/75ks/next-storybook.git

# プロジェクトディレクトリに移動
cd next-storybook

# クローンしたプロジェクトをVSCodeで開く
code .
```

#### VSCodeからコンテナに接続
この手順は別記事で載せていますので、[こちら](https://qiita.com/75ks/items/b2961e8562c353f42d21#vscode%E3%81%8B%E3%82%89%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%AB%E6%8E%A5%E7%B6%9A)から参照してください。

今回はNext.jsのサーバーだけでなく、Storybookのサーバーも起動しているので、
[localhost:6006](localhost:6006)に接続できるかも確認してください！

#### Docker上でもホットリロードを有効にする
クローンしたプロジェクトはDocker上で動かすため、下記設定がないとホットリロードが無効となってしまいます。
下記設定はプッシュしているため、個別で設定する必要はありませんが、詰まりやすいポイントなので、是非覚えておいてください。
##### Docker上で動くNext.jsのホットリロードを有効にする
```diff_javascript:next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
+  // Webpackの開発モードの設定
+  webpackDevMiddleware: (config) => {
+    // 監視オプション
+    config.watchOptions = {
+      poll: 1000, //  変更チェックをする間隔（ミリ秒）
+      aggregateTimeout: 500, // 変更があってから再ビルドするまでの間隔（ミリ秒）
+      ignored: ["node_modules"], // 監視対象外ディレクトリ
+    }
+    return config;
+  },
}

module.exports = nextConfig

```

##### Docker上で動くStorybookのホットリロードを有効にする
```diff_typescript
import type { StorybookConfig } from "@storybook/nextjs";
import type { Configuration } from 'webpack';

const config: StorybookConfig = {
  stories: [
    "../stories/**/*.mdx",
    "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
    '../app/components/**/*.stories.ts',
  ],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-onboarding",
    "@storybook/addon-interactions",
  ],
  framework: {
    name: "@storybook/nextjs",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
+  // Webpackの設定
+  webpackFinal: async (config: Configuration) => {
+    // 監視オプション
+    config.watchOptions = {
+      poll: 1000, // 変更チェックをする間隔（ミリ秒）
+      aggregateTimeout: 500, // 変更があってから再ビルドするまでの間隔（ミリ秒）
+      ignored: ["node_modules"], // 監視対象外ディレクトリ
+    };
+    return config;
+  },
};
export default config;
```


# 解説
[localhost:6006](localhost:6006)で接続した画面がStorybookの画面になります。
ここで作成したコンポーネントの動作確認をすることができます。
画像赤枠部分のストーリーが作成したコンポーネントのものになります。
※赤枠外の上部の「EXAMPLE」配下のストーリーは、Storybook導入時にデフォルトで作成されたものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616933/da29e903-d122-8407-d315-f64887178765.png)

### コンポーネント
これからコンポーネントとストーリーの紐付き方について説明していきますが、その前にまずボタンコンポーネントファイルを確認してみましょう。
```tsx:CustomButton.ts
interface Props {
  /** ボタンの文言 */
  label: string;
  /** 非活性にするか */
  disabled?: boolean;
  /** ボタンのサイズ */
  size?: "small" | "medium" | "large";
  /** ボタンの色 */
  color?: "blue" | "red" | "green" | "gray";
  /** クリックイベント */
  onClick: () => void;
}

export const CustomButton: React.FC<Props> = ({
    label,
    disabled = false,
    size = "medium",
    color = "blue",
    onClick
}: Props) => {
  const sizeClasses = {
    small: "px-2 py-1 text-sm",
    medium: "px-4 py-2",
    large: "px-6 py-3 text-lg",
  };
  const colorClasses = {
    blue: "enabled:bg-blue-400 enabled:hover:bg-blue-300",
    red: "enabled:bg-red-400 enabled:hover:bg-red-300",
    green: "enabled:bg-green-400 enabled:hover:bg-green-300",
    gray: "enabled:bg-gray-400 enabled:hover:bg-gray-300",
  };

  const sizeClass = sizeClasses[size];
  const colorClass = colorClasses[color];

  return (
    <button 
      className={`${sizeClass} ${colorClass} disabled:bg-gray-300 disabled:cursor-not-allowed text-white font-bold rounded`}
      disabled={disabled}
      onClick={onClick}
    >
      { label }
    </button>
  )
}
```
`Props`インターフェースを作成して、コンポーネントに渡す情報を定義しています。
ここでは`label`、`disabled`、`size`、`color`、`onClick`の5つの情報を持っており、`label`と`onClick`だけ必須としています。
必須以外は初期値を設定しています。

このボタンコンポーネントは`CustomButton`という名前でexportしています。

### ストーリー
#### Features
次に本命のボタンストーリーファイルを確認します。
まずは、`Features.stories.ts`です。
Featuresでは、コンポーネントのカタログを作成しています。
カタログなので、画面からの変更はできないようにしています。
主にボタンデザインの確認用に使用します。
```ts:Features.stories.ts
import type { Meta, StoryObj } from "@storybook/react";
import { CustomButton } from "./CustomButton";

const meta: Meta<typeof CustomButton> = {
	title: "components/Button/Features",
	component: CustomButton,
	parameters: {
		layout: "centered",
		controls: { disable: true },
	},
	argTypes: {
		disabled: { control: "boolean" },
		label: { control: "text" },
		size: { control: "inline-radio" },
		color: { control: "inline-radio" },
	},
};
export default meta;

type Story = StoryObj<typeof CustomButton>;

export const Enabled: Story = {
	args: {
		disabled: false,
		label: "Enabled Button",
	}
};

export const Disabled: Story = {
	args: {
		disabled: true,
		label: "Disabled Button",
	}
};

export const Small: Story = {
	args: {
		label: "Small Button",
		size: "small"
	}
};

export const Medium: Story = {
	args: {
		label: "Medium Button",
		size: "medium"
	}
};

export const Large: Story = {
	args: {
		label: "Large Button",
		size: "large"
	}
};

export const Blue: Story = {
	args: {
		label: "Blue Button",
		color: "blue",
	}
};

export const Red: Story = {
	args: {
		label: "Red Button",
		color: "red",
	}
};

export const Green: Story = {
	args: {
		label: "Green Button",
		color: "green",
	}
};

export const Gray: Story = {
	args: {
		label: "Gray Button",
		color: "gray",
	}
};
```
##### meta
`Story`のmetaデータを設定します。詳細は下記の通りです。
- title: Storybookの画面に表示される名前(/で階層を入れることが可能)　※画像赤枠
- component: 紐付けるコンポーネント
- parameters: ストーリー表示に関するオプション
  - layout: ストーリーをどこに配置するか
    - centered: 中央に表示
    - fullscreen: 幅と高さいっぱいに表示
    - padded: コンポーネントの周囲に余白を追加
  - controls: Controlsタブの表示
- argTypes: Propsを定義
  - Props.control: Storybook画面のControls > Controlの表示を設定

※argTypesの詳細は後で紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616933/df378109-f0c5-80a5-e028-d278bffc6567.png)

##### Story
`StoryObj`のジェネリクスにexportしたコンポーネントを指定することで、そのコンポーネントの`Story`のひな形を作成します。
作成した`Story`をオブジェクトの型に指定することで、そのオブジェクトはストーリーをして扱われ、Storybookの画面に表示されます。
オブジェクトの`args`には`Props`を設定し、Storybookに表示される初期値を設定します。
今回は`Enabled`、`Disabled`、`Small`、`Medium`、`Large`、`Blue`、`Red`、`Green`、`Gray`の9つのストーリーを作成しています。

#### Playground
次に`Playground.stories.ts`です。
Playgroundでは、コンポーネントのサンドボックスを作成しています。
サンドボックスなので、画面から変更できるようにしています。
主にボタンの動作確認用に使用します。
```ts:Playground.stories.ts
import type { Meta, StoryObj } from "@storybook/react";
import { CustomButton } from "./CustomButton";

const meta: Meta<typeof CustomButton> = {
	title: "components/Button/Playground",
	component: CustomButton,
	parameters: {
		layout: "centered",
	},
	tags: ["autodocs"],
	argTypes: {
		disabled: { control: "boolean" },
		label: { control: "text" },
		size: { control: "inline-radio" },
		color: { control: "inline-radio" },
	},
};
export default meta;

type Story = StoryObj<typeof CustomButton>;

export const Playground: Story = {
	args: {
		label: "Playground Button",
	}
};
```

Featuresと異なる点は2つあります。

1つ目は、metaの`tags: ["autodocs"]`がある点です。
これがあることによって、ドキュメントが自動生成され、ドキュメントからも変更が可能となります。
ドキュメントでは、今の状態をコードに変換してくれる機能も備わっています。
※`Docs`がドキュメントになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616933/f7085cc3-8e34-341f-d942-92c692a79238.png)

2つ目は、metaの`parameters.controls`がない点です。
Featuresでは、`disable: true`を設定していたため、Storybook画面のControls > Controlが表示されていませんでしたが、Playgroundでは表示されるため、ドキュメント以外の画面からも変更が可能となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616933/d46cbaf5-54f2-50a3-339a-bad9b49fd3e6.png)
metaの`argTypes.Props.control`の種類はいくつかあり、`Props`の型によって設定できるcontrolに制限があります。
```ts:Playground.stories.ts
...
//(省略)
	argTypes: {
		disabled: { control: "boolean" },
		label: { control: "text" },
		size: { control: "inline-radio" },
		color: { control: "inline-radio" },
	},
//(省略)
```
| 型 | control | 説明 |
| ---- | ---- | ---- |
| array | 'object' | 配列の値を扱うJSONベースのエディタを提供する。また、rawモードでの編集も可能。 <br>{ control: 'object' }|
| boolean | 'boolean' | 可能な状態を切り替えるためのトグルを提供する。 <br>{ control: 'boolean' }|
| enum | 'check' | 複数のオプションを選択するための、積み重ねられたチェックボックスのセットを提供する。 <br>{ control: 'check', options: ['email', 'phone', 'mail'] }|
|  | 'inline-check' | 複数のオプションを選択するための一連のインラインチェックボックスを提供する。<br>{ control: 'inline-check', options: ['email', 'phone', 'mail'] } |
|  | 'radio' | 利用可能なオプションに基づいて、積み重ねられたラジオボタンのセットを提供する。<br>{ control: 'radio', options: ['email', 'phone', 'mail'] } |
|  | 'inline-radio' | 利用可能なオプションに基づいて、インライン化されたラジオボタンのセットを提供<br>{ control: 'inline-radio', options: ['email', 'phone', 'mail'] } |
|  | 'select' | オプションから一つの値を選択するセレクトを提供する。<br>{ control: 'select', options: [20, 30, 40, 50] } |
|  | 'multi-select' | オプションから複数の値を選択するセレクトを提供する。<br>{ control: 'multi-select', options: ['USA', 'Canada', 'Mexico'] } |
| number | 'number' | すべての可能な値の範囲を含む数値入力を提供する。<br>{ control: { type: 'number', min:1, max:30, step: 2 } } |
|  | 'range' | すべての可能な値を含む範囲スライダーを提供する。<br>{ control: { type: 'range', min: 1, max: 30, step: 3 } } |
| object | 'file' | URLの配列を返すファイル入力を提供する。特定のファイルタイプを受け付けるように、さらにカスタマイズすることもできる。<br>{ control: { type: 'file', accept: '.png' } } |
|  | 'object' | オブジェクトの値を扱うJSONベースのエディタを提供する。また、rawモードでの編集も可能。<br>{ control: 'object' } |
| string | 'color' | 色の値を選択するためのカラーピッカーを提供する。さらに、カラープリセットのセットを含むように設定できる。<br>{ control: { type: 'color', presetColors: ['red', 'green']} } |
|  | 'date' | 日付を選択するためのデートピッカーを提供する。<br>{ control: 'date' } |
|  | 'text' | 自由形式のテキスト入力を提供する。<br>{ control: 'text' } |

# さいごに
これだけでStorybookの大枠を理解し、作成することができます。
難しい設定などはないので、慣れてしまえばすぐに扱えるようになると思います！
今回は実際に動かしてみて、Storybookがどんなものなのかを理解することが目的だったので、興味のある方は本記事を参考に、1から是非作ってみると良いと思います！