Next.jsでの開発にStorybookを導入する

## はじめに
この記事は、StorybookをNext.jsの開発に導入する流れと、簡単な使い方、そしてStorybookを用いたコンポーネントテストについてまとめた記事です。

・・・と思ったのですが、今回はStorybookをNext.jsの開発に導入する流れと、簡単な使い方まで書いた状態で投稿します。
Storybookを用いたコンポーネントテストについては、後日、別記事を作成したいと思います。

## Storybookとは？

Storybookとは、UIコンポーネントのカタログのようなもので、各componentのUIや、渡すpropsに応じた挙動の確認を行うことができます。

開発メンバー間の認識合わせや、同じようなコンポーネントを重複して作成してしまうことの防止などに繋がります。

## Storybook導入〜設定

### プロジェクト作成

まずは、プロジェクトを作成。
私は今回、Next.js + TypeScriptで開発しているので、以下のように作成しました。
 (yarnを用いて作成)

```
yarn create next-app --typescript [プロジェクト名]
```

### Storybookのインストール

Storybookのインストールは、プロジェクトのルートディレクトリの中で、Storybook CLIを使用して1つのコマンドで行います。

```
npx sb init
```
このコマンドを実行することで、Storybookを使用するのに必要なパッケージが自動的にインストールされ、package.jsonのscriptsにも自動で必要なコマンドが追加されます。
↓
(実行後のpackage.jsonのscripts)
<img width="400" alt="scripts" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1007943/9bf0c1d0-1b1a-9f39-5ce3-5a247b07b9a1.png">
(実行後のpackage.jsonのdevDependencies)
<img width="400" alt="package.json" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1007943/6745cc45-cdcc-e95e-c42f-9c1430d4d24a.png">

また、Storybookの設定用ファイルに加え、Storybook使用のサンプルとして、いくつかのコンポーネントとStorybook登録用の設定ファイルが追加されます。

### Storybook用リンター設定

Storybookをインストールしていると、 途中で"Do you want to run the 'eslintPlugin' fix on your project?"(eslintのプラグインを入れますか？)と聞かれます。
ESLintの静的解析を行いたい場合、"y"と入力することで”eslint-plugin-storybook”がインストールされます。

eslintrc.jsonに、以下のように追加します。

```eslintrc.json
"extends": [
        ....
    "plugin:storybook/recommended",  // 追加
```

### Storybookのmain.js設定
Storybookの全体的な設定は、"npx sb init"実行時に自動的に追加される、.storybook/main.jsの中で行います。

- **stories** ...storiesファイル(コンポーネント登録用ファイル)までのファイルパスを指定。
- **addons** ...導入したいアドオン(拡張機能)を指定

ちなみに私はTailwind CSSを利用しており、Storybook 起動時に PostCSS DeprecationWarningというものが発生しました。ライブラリが使用している PostCSS version と Storybook のPostCSS versionが異なることで発生するエラーのようです。

そのため、PostCSSのversionの整合性を取る為のアドオンをインストールして設定しています。

```.storybook/main.js
module.exports = {
  stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"], // storiesファイルまでのファイルパス
  addons: [
    "@storybook/addon-links",  // デフォルトで設定済み
    "@storybook/addon-essentials", // デフォルトで設定済み
    "@storybook/addon-postcss",   // ⇦ 追加
  ],
  framework: "@storybook/react",
};
```

## Storybookにコンポーネントを登録する
簡単な、コンポーネントを登録(ストーリーを定義というらしい)するまでの流れを記述します。

### コンポーネントを作成
まずは、Storybookに登録したいコンポーネントを作成します。
今回は、私が作成した汎用的なボタンを例に挙げたいと思います。Button.tsxファイルは以下のようになっています。

```src/components/Button.tsx
type Props = {
  label: string;
  backgroundColor?: string;
  color?: string;
  size?: "xs" | "sm" | "md" | "lg";
  onClick: MouseEventHandler<HTMLButtonElement>;
};

/**
 * 汎用的なボタンコンポーネント.
 */
export const Button: FC<Props> = memo((props) => {
  const {
    label,
    backgroundColor = "#f28728",
    color = "#fff",
    size,
    onClick,
  } = props;

  let scale = 1;
  if (size === "xs") scale = 0.3;
  if (size === "sm") scale = 0.75;
  if (size === "lg") scale = 1.5;

  const style = {
    backgroundColor,
    color,
    padding: `${scale * 0.3}rem ${scale * 1}rem`,
    width: `${scale * 10}rem`,
  };

  return (
    <button
      onClick={onClick}
      style={style}
      className="rounded-md shadow-md border-none font-bold hover:opacity-90"
    >
      {label}
    </button>
  );
});
 
```

ラベルやカラー、サイズなどをpropsで渡すことで、さまざまな様相をしたボタンになるように作成しています。
backgroundColorにはアプリケーションのテーマカラー、colorには白をデフォルト値として指定しています。

### storiesファイル作成(基本)
上で作成したボタンコンポーネントをStorybookに登録するために、storiesファイルを作成します。
今回私は、プロジェクト直下にsrcディレクトリを作成して開発しているのですが、その直下にstoriesディレクトリを作って、その中にstoriesファイルを置くことにしました。

src/componentsの中、コンポーネント作成ファイルと同階層にstoriesファイルを置くことも多いようですが、それではcomponentsディレクトリの中のファイル数が多くなり、見にくくなると思ったからです。

実際に開発コードの中で使うコンポーネントのファイルとは分離させたいと考えました。

Storybookによるストーリーの定義は、**.stories.jsx(tsx)** という拡張子で定義します。

(全体像)

```src/stories/Button.stories.tsx
import type { ComponentStory, ComponentMeta } from "@storybook/react";
import { Button } from "../components/Button/Button";

// storyのmetadataをdefault export
export default {
  title: "Button",   // コンポーネントのタイトル(任意)
  component: Button, // 実際に使用するコンポーネント（上でimportしたもの）
} as ComponentMeta<typeof Button>;

/// 1. Storybookで描画するためのコンポーネントの雛形を用意しておく
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;

// 2. bindを用いて雛形を元にしたコピーを作成
// 名前付きエクスポートはデフォルトでストーリーオブジェクトを表す
export const Default: ComponentStory<typeof Button> = Template.bind({});
// 3. Propsに値を設定しない
Default.args = {
  label: "Default",
  onClick: () => {
    alert("クリック");
  },
};
Default.storyName = "デフォルト";

export const SubButton: ComponentStory<typeof Button> = Template.bind({});
SubButton.args = {
  label: "SubButton",
  backgroundColor: "#f6f0ea",
  color: "#622d18",
};
SubButton.storyName = "サブボタン";

export const Small: ComponentStory<typeof Button> = Template.bind({});
Small.args = {
  label: "Small",
  size: "sm",
};
Small.storyName = "小さいボタン";

export const Large: ComponentStory<typeof Button> = Template.bind({});
Large.args = {
  label: "Large",
  size: "lg",
};
Large.storyName = "大きいボタン";

export const SubSmall: ComponentStory<typeof Button> = Template.bind({});
SubSmall.args = {
  label: "SubSmall",
  backgroundColor: "#f6f0ea",
  color: "#622d18",
  size: "sm",
};
SubSmall.storyName = "小さいサブボタン";

```
- storyのmetadataを定義してdefault export

```src/stories/Button.stories.tsx
export default {
  title: "Button",   // タイトル(任意)。　コンポーネント名と同じにするなら省略してもよさそう。
  component: Button, // 実際に使用するコンポーネント（上でimportしたもの）
} as ComponentMeta<typeof Button>;
```

- 1) Storybookで描画するためのコンポーネントの雛形を用意しておく

argsには、各ストーリーで後々設定するpropsが入ってきます。

```src/stories/Button.stories.tsx
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
```
- 2) bindを用いて雛形を元にしたコピーを作成

storiesファイルで名前付きエクスポートを行うと、デフォルトでストーリーオブジェクトであると認識されます。

```src/stories/Button.stories.tsx
export const Default: ComponentStory<typeof Button> = Template.bind({});
```

- 3) 作成したコピーにそれぞれの引数を設定することで、様々なpropsに応じたコンポーネント(今回はボタン)を作り出すことができます。
以下はデフォルトのボタンを作り出したいので、カラーやサイズを渡していません。

```src/stories/Button.stories.tsx
Default.args = {    // 引数(props)を設定　 
  label: "Default",
  onClick: () => {
    alert("クリック");
  },
};
Default.storyName = "デフォルト"; 
```

 .storyNameでそのストーリーのタイトルをつけることができます。

- サブボタンのストーリーも定義してみる

```src/stories/Button.stories.tsx
export const SubButton: ComponentStory<typeof Button> = Template.bind({});
SubButton.args = {
  label: "SubButton",
  backgroundColor: "#f6f0ea", // サブで作成したいボタンのカラーを設定
  color: "#622d18",
};
SubButton.storyName = "サブボタン";
```

### Storyの型定義について
型定義では**ComponentMeta**, **ComponentStory**を使います。

```src/stories/Button.stories.tsx
import type { ComponentStory, ComponentMeta } from "@storybook/react";
```

```src/stories/Button.stories.tsx
export default {
  title: "Button",   
  component: Button, 
} as ComponentMeta<typeof Button>;
```
```src/stories/Button.stories.tsx
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
```

ちょっと前まで(実際に開発でStorybookをいじっていた1ヶ月くらい前)はMeta、Storyというものを使っていましたが、こっちがデフォに変わったらしい。(記事を書くときに知ってコードを直しました。)
公式ドキュメントでもこちらで書かれています。

propsの型も、コンポーネントファイルでexportしてここでimportして、、、ということをしてましたが、上記のように「typeof [コンポーネント名]」で良くなったみたい。


### Storybookを起動して確認
上のpackage.jsonのscriptsにもあるように、”yarn storybook”(npmであれば"npm run storybook") とすることで、Storybookの画面を立ち上げることができます。

- Canvas

ターミナルに出てくるURLをクリックすると、以下のような画面が出てきます。

<img width="５00" alt="package.json" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1007943/a9643402-3ad2-66a1-5df3-0368e0aefd54.png">

左側に登録したコンポーネントの一覧が並び、それを選択することで、コンポーネントのUIを確認することができます。
下のControlsのところをいじると、その場でpropsを変更して、propsでの違いを見ることもできます。

- Docs

また、上のDocsメニューを押すと、以下のようなコンポーネントごとのドキュメントを見ることができます。

<img width="５00" alt="package.json" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1007943/3dc59ab7-6caa-db8d-e2ef-80da21b25f01.png">

下の方へ行くと、定義したストーリーが並んでいます。
"Show code"で、その見た目にするにはどのようにpropsを渡せば良いかが一目瞭然なのでとても便利です！

<img width="５00" alt="package.json" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1007943/99105bb3-667e-863a-0996-418863bc5e75.png">

## CSF3.0が正式に使えるように？！

ここまで書いたのは、CSF2.0の話。

### CSFとは？？
**CSF(Component Story Format)**　: Storybook5.2から推奨の書き方となった、Storybookのフォーマット。それまでstoriesOf APIというものを利用していた、らしい。CSFで書くことで、Jest や Cypress など、他ツールとも連携できるようになったとのこと。

### CSF3.0へ
2021年11月末、**Storybook6.4**が正式にリリースされました。それに伴って、CSFの最新の形式である**CSF3.0**が利用可能となったようです。

CSF3.0になることで...

1. ストーリー定義がより簡潔に記述できるように👏
2. intercative storyを記述できるように(play関数の登場)👏

ここでは一つ目、「ストーリー定義がより簡潔に記述できるように」という観点で、どのように変わったかを検証したいと思います。

上で、CSF2.0を用いて記述したボタンコンポーネントのストーリーを、CSF3.0に書き換えてみます。

Storybookのバージョンが6.4以上であれば、問題なくCSF3.0を利用することができます。
(私も2.0で書いていましたが、実は6.4以上だったのでCSF3.0で書ける状態でした。)

```src/stories/Button.stories.tsx
export default {
  component: Button,
} as ComponentMeta<typeof Button>;

export const Default: ComponentStoryObj<typeof Button> = {
  args: {
    label: "Default",
    onClick: action("clicked!"),
  },
  storyName: "デフォルト",
};

export const SubButton: ComponentStoryObj<typeof Button> = {
  args: {
    label: "SubButton",
    backgroundColor: "#f6f0ea",
    color: "#622d18",
  },
  storyName: "サブボタン",
};

   .
   .
   .
```
### 変更点

- **ストーリーが関数からオブジェクトへ**

CSF2.0ではコンポーネントの雛形をTemplateとして作成して、各ストーリーでbindを用いてコピー...ということをやっていました。が、3.0ではその冗長な記述が不要になります。

オブジェクト形式で、argsやstoryNameなど、各ストーリーに渡したいものを渡します。

```src/stories/Button.stories.tsx
// CSF2.0 ...関数
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;

export const Default: ComponentStory<typeof Button> = Template.bind({});

Default.args = {    
  label: "Default",
  onClick: () => {
    alert("クリック");
  },
};
Default.storyName = "デフォルト"; 

// CSF3.0 ...オブジェクト
export const Default: ComponentStoryObj<typeof Button> = {
  args: {            // propsの定義
    label: "Default",
    onClick: action("clicked!"),
  },
  storyName: "デフォルト", // ストーリーのタイトルを指定
};
```

- 型の追加 **ComponentStoryObj**

ストーリーが関数からオブジェクトに変わったことに伴い、ストーリの型定義として、**ComponentStoryObj**が追加されました。
2.0で**ComponentStory**を使っていたところが、**ComponentStoryObj**に変わるだけです。

### play関数の登場

二つ目の「intercative storyを記述できるように(play関数の登場)」については、Storybookでテストを行う際に便利、ということで、別途Storybookでのテストに関する記事の中でまとめたいと思います。

## 参考文献
- Storybook公式ドキュメント

https://storybook.js.org/docs/react/writing-stories/introduction

- Next.jsへの導入方法

https://tech-broccoli.life/articles/engineer/add-storybook-for-nextjs/

- CSF3.0について

https://storybook.js.org/blog/component-story-format-3-0/

https://zuma-lab.com/posts/next-story-book-csf3