Storybookを触ってみたらとても良かった

# 概要
Atomic Designを用いたフロントエンドの開発を行う中で、
<b>「このコンポーネントって既にあるんだっけ？」</b>と思い、プロジェクトの中を探し回ることがありました。
また、それっぽいファイルを見つけても、このコンポーネントはブラウザ上でどのように表示されるのかはぱっと見わかりません。

現状では、[Vue.js devtools](https://chrome.google.com/webstore/detail/vuejs-devtools/ljjemllljcmogpfapbkkighbhhppjdbg?hl=ja)などを使ってコンポーネント名を特定して、
実際の.vueまたは.tsxファイル名を照らし合わせる必要があります。

そんな中、[Storybook](https://storybook.js.org/)がいいという噂を見かけたので、早速試してみました。


# 作業環境

- Vue.js・・・3.2.45
- storybook/vue3・・・6.5.15

etc..

# Storybookとは
UIコンポーネントとページを分離して構築するためのフロントエンドワークショップです。
今までは、コンポーネントの挙動を確認するには、アプリ全体を起動、ページを移動、UIを正しい状態にする必要がありました。これには時間がかかるため、開発速度の低下につながります。

しかし、Storybookを使用することでこの問題を解決できます。具体的なメリットは以下に記載します。

## メリット
- コンポーネント単位でUI上での挙動を確認できる
- コンポーネント(ここではatomなど小さな単位)とページを分離してユースケースを検証できる
- UI上で到達しにくいエッジケースを検証できる
- UIテストが容易である
- コンポーネントやその状態をドキュメント化できる
- UIの動作を共有できる

## 特に注目している点

私は特に、最後の２つのメリットに注目しています。
- コンポーネントやその状態をドキュメント化できる
- UIの動作を共有できる

理由は、これによってデザイナーとの認識齟齬の発生を防ぐことができたり、同一のコンポーネントを再び実装してしまうことを防ぐことができるからです。

私のチームでは[Figma](https://www.figma.com/ja/)でデザインを行なっており、
Figmaでもコンポーネントを作成して、それらを組み合わせてページをデザインします。
その中で、デザインを確認しながらフロントで実装したコンポーネントの状態がデザイナーの意図に沿っているかどうかなど、相談ごとがどうしても増えてしまいます。
相手が忙しい場合も多いため、ここのコミュニケーションコストをできる限り下げる必要があります。


そこで、Storybookを利用することでコンポーネントやその状態をドキュメント化し共有することで、
アプリを操作して該当の画面に遷移する等の操作なく、コンポーネントを確認してもらうことが可能になります。
これによって、相談により発生するコミュニケーションコストが大幅に低下することが期待できます。

[静的サイトとしてデプロイ](https://storybook.js.org/tutorials/intro-to-storybook/react/ja/deploy/)が可能なようなので、S3等にアップロードしておけば
誰でもいつでも確認できるドキュメントとして利用できそうです。

# 実際に使ってみる
### 導入
ここを参考にします。
[Introduction to Storybook for Vue](https://storybook.js.org/docs/vue/get-started/introduction)

ちなみに、以下のようにReactやSvelteなど各ライブラリに応じたチュートリアルが紹介されています。
親切すぎる・・・
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/51663be8-7594-97a3-bc1f-8cd731407a30.png" width="600px" />

まずはViteでプロジェクトを生成します。
Storybookは空のプロジェクトでは動かないため、既にフレームワークがセットアップされている必要があります。

```
$ yarn create vite
✔ Project name: … sample
✔ Select a framework: › Vue
✔ Select a variant: › TypeScript

$ cd sample
$ yarn
$ yarn dev
$ vite

  VITE v4.0.4  ready in 406 ms

  ➜  Local:   http://127.0.0.1:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help
✨  Done in 100.32s.
```

次に、用意したプロジェクトのルートディレクトリで以下のコマンドを実行し、Storybookをインストールします。

```
$ npx storybook init

中略

yarn install v1.22.19
[1/4] 🔍  Resolving packages...
success Already up-to-date.
✨  Done in 0.54s.
. ✓
🔎 checking possible migrations..

✅ migration check successfully ran


To run your Storybook, type:

   yarn storybook 
```
コマンド実行により以下のような変更が加えられます。

- Storybookの実行とビルドに必要なスクリプトをセットアップする。
- デフォルトのStorybookの設定を追加する。
- 開始するためのデフォルトのストーリーを追加する。

プロジェクトにはstoriesディレクトリが追加されました。

<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/1d024bf3-b2d9-314c-dd41-33062580510d.png">

package.jsonのscriptには`storybook`と`build-storybook`が追加されています。

```package.json
{
  "name": "sample",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook"
  },
  "dependencies": {
    "vue": "^3.2.45"
  },
  "devDependencies": {
    "@babel/core": "^7.20.12",
    "@storybook/addon-actions": "^6.5.15",
    "@storybook/addon-essentials": "^6.5.15",
    "@storybook/addon-interactions": "^6.5.15",
    "@storybook/addon-links": "^6.5.15",
    "@storybook/builder-vite": "^0.3.0",
    "@storybook/testing-library": "^0.0.13",
    "@storybook/vue3": "^6.5.15",
    "@vitejs/plugin-vue": "^4.0.0",
    "babel-loader": "^8.3.0",
    "typescript": "^4.9.3",
    "vite": "^4.0.0",
    "vue-loader": "^16.8.3",
    "vue-tsc": "^1.0.11"
  }
}

```
次に、以下のコマンドを実行してStorybookが正常に動作するかを確認します。

```
$ yarn storybook

yarn run v1.22.19
$ start-storybook -p 6006
info @storybook/vue3 v6.5.15
info 
info => Loading presets

info => Ignoring cached manager due to change in manager config
ℹ ｢wdm｣: wait until bundle finished: 
ℹ ｢wdm｣: Hash: 70239335d1b0f74c3dc2
Version: webpack 4.46.0

中略

Child HtmlWebpackCompiler:
                          Asset      Size               Chunks  Chunk Names
    __child-HtmlWebpackPlugin_0  6.32 KiB  HtmlWebpackPlugin_0  HtmlWebpackPlugin_0
    Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
    [./node_modules/html-webpack-plugin/lib/loader.js!./node_modules/@storybook/core-common/templates/index.ejs] 2.04 KiB {HtmlWebpackPlugin_0} [built]
ℹ ｢wdm｣: Compiled successfully.
╭──────────────────────────────────────────────────────╮
│                                                      │
│   Storybook 6.5.15 for Vue3 started                  │
│   9.08 s for manager and 1.07 s for preview          │
│                                                      │
│    Local:            http://localhost:6006/          │
│    On your network:  http://xxx.xxx.xxx.xxx:6006/    │
│                                                      │
╰──────────────────────────────────────────────────────╯
```

このように表示されたら成功しています。
ブラウザで`http://localhost:6006/`を開くと、以下の画面が表示されます。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/a92e1196-39db-216d-e67e-450a8a40df99.png" width="600px" />

### Storyとは

ストーリーは、UIコンポーネントのレンダリング状態をキャプチャするために使用する、そのコンポーネントをどのようにレンダリングするかを記述した関数のことです。
コンポーネントごとに複数のストーリーを記述し、コンポーネントがサポートできるすべての状態を定義しておきます。

Storybookのインストール時に、CLIによってボタン、ヘッダー、ページのサンプルコンポーネントが作成されています。

各サンプルコンポーネントには、サポートする状態を示す一連のストーリーがあります。
UIでストーリーを参照し、`.stories.js`または`.stories.ts`で終わるファイルでその背後にあるコードを見ることができます。ストーリーは、コンポーネントのサンプルを記述するためのES6モジュールベースの標準である[Component Story Format (CSF)](https://storybook.js.org/docs/vue/api/csf)で記述されています。

# Storyを書いてみる
自動生成されたボタンコンポーネントのストーリーを書いてみます。
既に`Button.stories.js`が生成されているので、mdx形式を試します。

ちなみに[mdx](https://storybook.js.org/docs/vue/writing-docs/mdx)とは、markdownの中に、Reactで用いられるjsxを埋め込むことができるフォーマットです。


```Button.vue
<template>
  <button type="button" :class="classes" @click="onClick" :style="style">{{ label }}</button>
</template>

<script>
import './button.css';
import { reactive, computed } from 'vue';

export default {
  name: 'my-button',

  props: {
    label: {
      type: String,
      required: true,
    },
    primary: {
      type: Boolean,
      default: false,
    },
    size: {
      type: String,
      validator: function (value) {
        return ['small', 'medium', 'large'].indexOf(value) !== -1;
      },
    },
    backgroundColor: {
      type: String,
    },
  },

  emits: ['click'],

  setup(props, { emit }) {
    props = reactive(props);
    return {
      classes: computed(() => ({
        'storybook-button': true,
        'storybook-button--primary': props.primary,
        'storybook-button--secondary': !props.primary,
        [`storybook-button--${props.size || 'medium'}`]: true,
      })),
      style: computed(() => ({
        backgroundColor: props.backgroundColor,
      })),
      onClick() {
        emit('click');
      }
    }
  },
};
</script>

```
以下のように記述します。

```Button.stories.mdx
import { Meta, Story } from '@storybook/addon-docs';
import Button from './Button.vue';

<Meta title="Button" component={Button} />

# Button

<Story name="Primary">
  {() => {
    return {
      components: { Button },
      template: `<Button primary label="Button" />`,
    };
  }}
</Story>
```

上記のStoryを追加することで、ブラウザでドキュメント化され確認することができます。
この時点ではボタンを押下しても何も起こりません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/e27cda64-3fbc-05fe-db1c-77135da62b7d.png" width="600px" />

次にargsを使用して、ボタンに対して引数を渡します。
ここで、
`import { action } from "@storybook/addon-actions";`
したうえでargsに`onClick: action("clicked")`のようにイベントに対するアクションを設定しています。

```Button.stories.mdx
import { Meta, Story } from '@storybook/addon-docs';
import { action } from "@storybook/addon-actions";
import Button from './Button.vue';

<Meta title="Button" component={Button} />

# Button
export const Template = (args) => ({
  components: { Button },
  setup() {
    return { args };
  },
  template: '<Button v-bind="args" />',
});

<Story
  name="Primary"
  args={{
    primary: true,
    label: 'Button',
    onClick: action("clicked")
  }}>
  {Template.bind({})}
</Story>

```

argsを設定することで、以下のようにControlsタブが追加され、ボタンのがどのような状態を持つのかを画面上で確認できるようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/10510c7b-2924-284b-6e92-558ab6b856d4.png" width="600px" />

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/485339b9-90aa-09ce-d101-aee7c34d3bc7.png" width="600px" />


また、クリック時にActionsタブで詳細を確認することができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475547/7f085af6-077d-bb5a-661a-12e0c610f3dd.png" width="600px" />


# まとめ

Storybook、非常に良さそうですね。
特に、いちいちアプリを操作せずにコンポーネント単位で確認できるので、エンジニアもデザイナーも幸せに慣れそうな気がします。
他にも多くの機能があるので今後も触っていきたいです。
それでは！