📚

EventHubでのStorybook活用事例

2022/12/17に公開

これは EventHub  Advent Calendar 2022 の17日目の記事です。
昨日はインサイドセールスマネージャーの加藤さんの「 トークスクリプトってシニアなインサイドセールスに必要なの?」でした。こちらもぜひ!

こんにちは。
昨年の10月から株式会社EventHubでエンジニアをしている佐野と申します。
プライベートではもうすぐ2歳になる息子がおり、入社時には6時間の時短勤務でしたが、今年の5月からフルタイムになりました。

私はここ5年ほど複数の会社、複数のプロジェクトでReact/TypeScriptでのSPA開発に携わってきましたが、毎回Storybookを使用していて、開発に欠かせない存在になっています。今回の記事では、現在EventHubでどのようにStorybookを活用しているかを紹介したいと思います。

https://storybook.js.org/

Storybookを使うメリット

コンポーネントカタログを作成できるツールとしておなじみのStorybookですが、そのメリットは多岐に渡ります。

1.画面仕様が確認しやすい

まずはやはりコンポーネントカタログですので、画面仕様が確認しやすい、ということが一番のメリットかと思います。特に、特定の条件下でしか再現できないUIなどを確認する場合は、その状況を実際のアプリケーションで再現するのも大変なので、大幅に手間が減ります。
具体的にStorybookがあってよかった!と思う場面を以下に挙げてみました。

デザイナーがデザイン通りに実装されているかを確認しやすい

状態のパターンごとのStoryを用意しておけば、デザイナーがデザイン通りに実装されているかを確認する際にも確認がしやすくなります。

多言語UIの確認がしやすい

現在EventHubは日英の2言語に対応していますが、英語のUIの確認を怠ってしまい、英語の場合の画面崩れなどがQAで報告されることがあります。そのため、Storybook上で言語切り替えができるようなDecoratorを追加し、確認をしやすくしました。

日本語表示

英語表示

こちらを参考に、ツールバーに言語選択メニューを追加し、その値をDecoratorでstoryに反映しています。
https://storybook.js.org/docs/react/essentials/toolbars-and-globals

// .storybook/preview.js

export const globalTypes = {
  locale: {
    name: "言語",
    description: "言語選択",
    defaultValue: "ja",
    toolbar: {
      icon: "globe",
      items: [
        { value: "ja", right: "🇯🇵", title: "ja" },
        { value: "en", right: "🇺🇸", title: "en" },
      ],
    },
  },
};

addDecorator(LocaleDecorator);

// LocaleDecorator.tsx

export const LocaleDecorator = (StoryEl: Story, storyContext: StoryContext) => {
  void i18n.changeLanguage(storyContext.globals.locale);
  return <StoryEl />;
};

翻訳依頼の際に、文言の使用箇所のUIを明示できる

英語の文言を翻訳依頼する際には、文言だけでは文脈がわからず適切な訳がしづらいため、使用される箇所を共有する必要があります。その場合にも、該当箇所のStorybookのurlを併記することで、共有がしやすくなります。

2.アクセシビリティのチェックができる

Storybookには、アクセシビリティのチェックのためのaddonが用意されています。
https://storybook.js.org/docs/react/writing-tests/accessibility-testing
現在、新しく作るコンポーネントに対しては必ずstoryを用意して、アクセシビリティの機械的にチェック可能な項目を確認することにしています。それによって、アプリケーション全体で最低限のアクセシビリティを担保することを目指しています。
画面のaddonのパネルで警告が出ていても見落としやすいのですが、
testing-library
jest-axe
などと組み合わせてstoryをテストにも利用することで、アクセシビリティ警告に気づくようにしています。
https://storybook.js.org/blog/accessibility-testing-with-storybook/

describe("Select", () => {
  it("should have no violation.", async () => {
    const { container } = render(<Default />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});

今まではstoryごとにこのようなテストを書いていたのですが、こちらの仕組みを使うと、まとめてチェックできるのでさらに良さそうです!↓
https://storybook.js.org/blog/automate-accessibility-tests-with-storybook/

3.レビューの負荷が軽減される

先にUI部分だけを実装するようなPRの場合、そのアウトプットを確認できるStorybookがあることで、レビューや動作確認がしやすくなります。また、条件によって表示パターンが複数ある場合などは、そのパターンごとのstoryを用意することで、それが画面仕様として可視化されるので、どういう表示パターンがあるのかをコードだけで見るよりもレビュワーが理解しやすくなります。
また、PRごとにStorybookが確認環境にdeployされるようになっているため、UIの確認であれば、localにcheckoutしなくてもブラウザ上からそのPRで変更されたstoryを動作確認できることも、レビューのしやすさにつながっています。
さらに、そのようなUIだけのPRがしやすくなることで、PRのスコープが分割されるという側面もあると思います。一度のPRにUI作成もAPIとの繋ぎ込みも全部盛りこむよりも、スコープを小さくしたPRの方が、レビューの負荷は下がります。

運用面の工夫や悩みポイントなど

storyを書きやすくするためのコンポーネント構成

コンポーネントは基本的にContainerコンポーネント/Presentationalコンポーネントの2種類に分け、Presentationalコンポーネントに対してstoryを作るようにしています。

https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
この分け方はReactのコンポーネント設計としては割と古いもので、hooksが出来てからはこの分け方にこだわる必要がないということも上記の記事に追記されていますが、ロジックとviewの部分を分離するのはわかりやすいですし、storyが作りやすくなるので、この構成にしています。

  • Containerコンポーネント
    • APIアクセスやContextからのデータ取得を行い、必要なデータをpropsとしてPresentationalコンポーネントに渡す
    • ファイル名はindex.tsxとする
  • Presentationalコンポーネント
    • propsを用いてUIをrenderする
    • storyはここに対して作成する
    • ファイル名はtemplate.tsxとする(同じ階層にContainerコンポーネントがない場合はindex.tsx)

コンポーネントのサイズによっては、見通しをよくするために、いくつかの部分的なコンポーネントに分割することもあります。
例えばHeaderコンポーネントにMenuとSettingが含まれているとして、MenuもSettingもそれぞれ別のAPIアクセスが必要だとします。その場合はこのようなディレクトリ構成になります。

Header/index.tsx
Header/parts/Menu/index.tsx
Header/parts/Menu/template.tsx
Header/parts/Menu/template.stories.tsx
Header/parts/Setting/index.tsx
Header/parts/Setting/template.tsx
Header/parts/Setting/template.stories.tsx

storyを作る粒度について

目で見て確認しやすいコンポーネントの粒度と、実装上のコンポーネント分割は必ずしも同じではありません。
上記のHeaderコンポーネントの例では、PresentationalコンポーネントはMenuとSettingですが、それぞれの単体のstoryだけではHeaderの全体感が把握しづらいので、画面の確認のしやすさという観点では、Headerのstoryが欲しくなってきます。

そうすると
Presentationalコンポーネント → Containerコンポーネント → Presentationalコンポーネント
という入れ子構造になってしまい、storyが作りづらくなってしまいます。

そういった場合には、

  • 一番上のレイヤーだけをContainerコンポーネントにして、partsはPresentationalコンポーネントのみにする

という方法も考えられますが、各partsコンポーネントのロジックがすべて一番上のレイヤーに集約されるため、見通しが悪くなり、propsのバケツリレーになってしまうというデメリットがあります。

別のアプローチとしては、

  • Containerコンポーネントに含まれるAPIリクエストの処理などをモックする

という方法があります。
PresentationalコンポーネントとContainerコンポーネントが何層か入れ子になっているコンポーネントの構造を変えずに、まとまった単位のコンポーネントのstoryを作りたい場合は、モックを利用したstoryが便利です。
https://storybook.js.org/addons/msw-storybook-addon

今はこのようなケースがそこまで多くはないので、モックは部分的に使用していますが、再利用性の高い共通コンポーネント以外は、逆に

  • すべてモックを前提として、Container/Presentationalという設計から見直す

という方法もあるかもしれません。この辺は今後も模索していきたいと思っています。

表示パターンを網羅することが大事

1つのstoryで、Controlsパネルでvalueを書き換えながら確認できるのは便利な点ですが、propsが多い場合には、どういう表示パターンがあるのか実装を知っていないとわからないので、仕様の見落としにつながります。また、アクセシビリティチェックも抜けてしまいます。
なので、例えばtypeで色が変わるようなラベルであれば、すべてのタイプを並べてrenderしておいたり、propsの組み合わせによって表示要素が変わる場合は、パターンごとにstoryを分けて用意するように意識しています。Controlパネルをいじらなくても確認すべきパターンが網羅されていて、文言などを書き換えて確認したい場合のみControlパネルを使うのが理想的かなと思います。

Bad

悪い例
Defaultのストーリーしかなく、0件表示やLoading状態の表示を確認するにはControlsパネルの値をいじる必要があり、その表示パターンに気づきにくい

Good

良い例
Storyを表示パターンごとに分けて作ることで、確認すべきパターンが明示的になっている

まとめ

Storybookはコンポーネントカタログとして色々な場面で画面仕様の確認や共有に役立つだけでなく、アクセシビリティチェックなどにも活用でき、アプリケーションの質や開発体験を向上させてくれるツールだと思います。ここで挙げたこと以外にも、Visual Regression Testingなど、さまざまな活用ができそうです。
今後もStorybookのよりよい使い方を模索していきたいです!

お知らせ

EventHubでは、シリーズAの資金調達を受けて、採用活動を強化しています。
https://www.notion.so/EventHub-a35d5e4b2ccd4b2784f2e4483356d90f

次の18日目の記事は坂田さんです。
お楽しみに!

Discussion