Storybook を腐らせずに運用しよう

副業で [Findy](https://findy-code.io/) の開発に参加している keik です。

先日そちらの開発で Storybook を導入しましたが、ただ導入するだけでは何かと腐りがちな Storybook なので、そうならないためにちょこちょこと工夫をしています。それらの内容を紹介します。

# TL;DR

* UI コンポーネントのカタログ化ツール Storybook は、設計・実装・テストの各プロセスに良い効果をもたらす
* しかし、漫然と導入してもメンテナンスされず腐りやすい
* 腐らせないために以下の工夫をするのがオススメ

1. StoryShots による自動スナップショットテストを導入する
2. Story は UI コンポーネント実装ファイル一つ一つに対して `.stories.js` というファイル名で実装する
3. Story 名はファイルパスから自動生成する

サンプル https://github.com/keik/maintainable-storybook-example

# Storybook 概要

[Storybook](https://github.com/storybooks/storybook) は UI コンポーネントのカタログ化ツールです。
React などを用いて実装した UI コンポーネントのレンダリング結果やインタラクションを、Storybook の起動画面内で確認することができるようになります。

Storybook では UI コンポーネントのカタログのことを **Story** と呼びます。
追加した Story は Storybook の画面内にリストされ、選択することで表示内容を確認できます。
実物は公式サンプルを見てください。https://storybooks-official.netlify.com

Story の追加方法の例として、 `MyUI` という UI コンポーネントを Story に追加するには以下のようにします。

```jsx:MyUI.stories.js
import { storiesOf } from '@storybook/react'
import MyUI from './MyUI'

storiesOf('Story名（任意の文字列）', module).add('default', () => (
  <MyUI>Hello Storybook</MyUI>
));
```

Story のファイル名は `*.stories.js` と命名し、それらのファイル名をパターン一致で収集することで Story に自動追加するように設定するのが一般的です。

## Storybook 導入のメリット

Storybook を導入するとどういうメリットがあるでしょうか。
ざっと箇条書きしてみます。

- UI コンポーネントをカタログから探せるので **再利用が捗る**
- Storybook は開発中アプリを起動することなくそれ単独で簡単に起動できるので **デザイナとの共同作業のチャンネルにしやすい**
- Story の実装がそのまま UI コンポーネントの **使用方法のドキュメントになる**
- Story の実装時に UI コンポーネントの使い勝手を検証できるので **インタフェース設計を推敲できる**
- 副作用（マウント時のデータ fetch など）があると Story が追加しにくいため **副作用を分離した設計が促される**
- HMR (Hot Module Replacement) に対応しており **コードの変更が即時に画面に反映され実装作業中が素早く行える**
- 追加済みの Story に対し **自動的にスナップショットテストを構成できる**

良い事づくしです。

# しかし Storybook は腐りやすい

そんな Storybook でも、漫然と導入すればメンテナンスされずに腐ってしまいがちです。

なぜなら、Story を適当に管理していると、カタログの一覧性・検索性が失われたり、新たな Story 追加時のメンテナンスコストが増えたり、スタイルの再現性が不完全で信頼性が失われたりするからです。

コストがメリットを上回ったとき、それは腐り始めます。
そこで、メンテナンスコストを下げつつ、よりメリットを享受するために、以下の工夫をするのがオススメです。


## 1. StoryShots による自動スナップショットテストを導入する

Storybook に追加した Story に対して、自動的にスナップショットテストを実施することができる [StoryShots](https://github.com/storybooks/storybook/tree/master/addons/storyshots/storyshots-core) という Storybook アドオンがあります。

これを導入しておくと、UI コンポーネントの変更に対して Story のメンテナンスが追従せず気がついたら Story が表示できなくなっているということが防げます。

また **Story の追加 = カタログの追加 + テストの追加** になるので、Story 追加の効果が高まります。スナップショットテストでもカバレッジは計測されるので、その辺を見ながら Story を追加していくと気分も高まるはず。

## 2. Story は UI コンポーネント実装ファイルのそれぞれに対して `.stories.js` というファイル名で実装する

Story の追加方法やファイル名に制限はありませんが、UI コンポーネントの実装ファイルと同階層に同名の `.stories.js` ファイルで配置することをオススメします。

こうすると、どの UI コンポーネントに対して Story およびスナップテストが存在しているのかがファイルレイアウト上から一瞬で分かるようになります。逆にこうしないと、追加済み Story の中から目的のコンポーネントを探しにくくなったり、重複で追加してしまったり、といったことが起きやすくなります。

こんな感じにするとよいです。

```
.
├── companies
│   └── components
│       ├── Company.js
│       └── Company.stories.js
└── users
    └── components
        ├── User
        │   ├── Profile.js  <------ Story 無い
        │   ├── Settings.js
        │   └── Settings.stories.js
        ├── User.js  <------------- Story 無い
        ├── Users.js
        └── Users.stories.js
```

## 3. Story 名には自動的にファイルパスを使用する

Storybook 上の Story 一覧表示部分は、Story 名に含まれる `/` の文字によって自動的に階層構造化されます。

<img width="204" alt="スクリーンショット 2019-03-13 2.43.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/34233/ae1145e2-0690-34c2-0baf-cd9cadaa8b46.png">

この階層構造は、UI コンポーネントを探しやすくするためにも、先述したファイルレイアウトと一致させておくべきです。

その際、ファイルレイアウトと一致するように Story 名を手動で設定していくのはメンテナンスが面倒なので、Story 実装ファイルのパスから自動生成するようにしておきましょう。

この方法は公式のドキュメントで紹介されています。
https://storybook.js.org/docs/basics/writing-stories/#generating-nesting-path-based-on-__dirname
これを参考に、ファイルパスを Story 名に使用するには以下のようにするとよいです。

```jsx
import { storiesOf } from "@storybook/react";
import { base, filename } from "paths.macro"

storiesOf(`${base}/${filename.replace("stories", "")}`, module).add(...)
```

# サンプル

上記の一式を設定した Storybook のサンプルを作成したのでよければ参考にしてください。
 https://github.com/keik/maintainable-storybook-example