StorybookでUIのカタログを作ろう #techtekt Advent Calendar 2021

トップ画像 techtekt アドベントカレンダー2021 の 6日目の記事です 🎄

5日目の記事はこちら

techtekt.persol-career.co.jp

こんにちは! テクノロジー本部 エンジニアリング統括部 サービス開発部でエンジニアをしている亀澤です。

チームでプロダクトを開発する際は、デザイナーがUIデザインを作成し、エンジニアがUIコンポーネントを作成、それを別のエンジニアがアプリケーションに組み込んで機能を実装する…といった具合で、複数のメンバーが関わることと思います。

メンバー同士の情報共有のためにドキュメンテーションは避けて通れません。特にリモートワークと非同期のコミュニケーションが馴染んできた昨今では、より確実でスピーディな手段が求められています。

やっていますか?UIのドキュメンテーション。

本記事では前述したようなプロダクト開発の中で、私の所属チームが普段から活用している Storybook というツールについて紹介いたします!

Storybook とは何か

一言で説明すると、ウェブアプリケーションのUIコンポーネントをカタログ化できるオープンソースツールです。作成したUIコンポーネントをアプリケーションに組み込むことなく、画面上でレイアウトや挙動を確認することができるようになります。

storybook.js.org

導入のメリット

  • Sandboxのような環境で開発できる

    コンポーネント単体でブラウザに表示できるので、レイアウトや挙動を逐一確認しながらコーディングを進めることができます。

  • 複数のUIパターンを列挙できる

    コンポーネントの活用方法に合わせて複数のレイアウト用ページを作成できるので、必要なパターンを網羅できているかチェックする際に役立ちます。

  • インタラクティブなドキュメントを作成できる

    コンポーネントに設定できるプロパティ等の情報をスキャンして一覧表示してくれたり、そのプロパティを直接操作した場合の変化をブラウザ上で確認できます。

    機能開発を始める前にStorybook上の動きをチームメンバーにレビューしてもらうことで、UI作成の手戻りを減らすことができるようになります。

活用例

以上のメリットから、私の担当プロダクトでは以下のような場面で活用しています。

  • 機能開発を始める前の伝達手段
  • フロントエンドエンジニア同士、デザイナーやプランナーとのコミュニケーション
  • コンポーネントのドキュメンテーション(型情報、イベント等)
  • UIのモック作成、動作確認、レビュー

いつ導入すべきか

できる限り早いほう、可能であれば最初からが良いと言えます。開発がある程度進捗してからの導入となると、Storybookに掲載するのが難しい複雑なコンポーネントが作られていたり、階層や依存性が整理されていなかったり、大量にある既存のドキュメントを移植する必要が出てきたりと、障壁は次第に高まっていきます。

とはいえ途中からの導入でも十分効果はあります。新規コンポーネントからStorybookを前提とした構造とし、既存コンポーネントもできるところから少しずつリファクタしていくことを試みてみましょう(実際、私の担当プロダクトがその手法でした)。プロダクト毎に適したStorybookの活用方法を模索することが大事です。

また、コンポーネントは再利用を想定した部品として作成することが多いので、ドキュメントが充実していると長期的に見た開発スピードは速くなります。

導入手順

今回は Vue 3 × JavaScript のコンポーネントを前提として、お手軽にStorybookを構成してみましょう。yarnコマンドの実行できる環境をあらかじめ用意してください。 (※ 他にもReactやAngular等の著名なUIフレームワークや、TypeScriptに対応しています)

まずは適当なディレクトリを作成します。

mkdir practice-storybook
cd practice-storybook

次に必須パッケージをインストールします。 ※執筆時点のバージョン:vue@3.2.23, storybook@6.4.4

yarn init
yarn add vue@next storybook

この状態で以下のコマンドを実行すると、Storybookを起動するために必要なパッケージのインストール、サンプルコード追加、コマンド追加まで自動的に構成してくれます。便利ですね。

yarn sb init

practice-storybookディレクトリ配下がこのようになっていればOKです。

.storybook/
node_modules/
stories/
package.json
yarn.lock

以下のコマンドでStorybookの開発サーバーがローカルで起動します(終了はcontrol + c)。

yarn storybook

f:id:shota-kamezawa:20211202114442p:plain

コンポーネントを作成してみる

storiesディレクトリ内のサンプルコードを見ても良いのですが、せっかくなので新規でコンポーネントを作ってみましょう。

以下は背景色を変更できるシンプルなボタンコンポーネントの例です。

<!-- stories/base-button.vue -->

<template>
  <button
    :style="{
      'background-color': color,
    }"
  >
    <slot />
  </button>
</template>

<script>
export default {
  name: 'base-button',

  props: {
    color: {
      type: String,
      required: false,
      default: 'lightgray',
    },
  },
};
</script>

ストーリーを作成してみる

コンポーネントをStorybookで閲覧できるようにするにはストーリーの登録が必要です。ストーリーは Component Story Format (CSF) という記法で作成されたファイルに記載することで登録できます。

ひとまず簡単なストーリーを一個作ってみましょう。以下は先程のボタンコンポーネントに対してPrimaryというストーリーを登録する例です。

/* stories/base-button.stories.js */

import BaseButton from './base-button.vue';

// メタデータ
export default {
  component: BaseButton,

  title: 'Components/Buttons/BaseButton',
};

// ストーリーテンプレート
const Template = (args) => ({
  components: {
    BaseButton,
  },

  setup() {
    return {
      args,
    };
  },

  template: /* html */ `
    <base-button v-bind="args">
      Click me!
    </base-button>
  `,
});

// この export された変数がストーリーになります
export const Primary = Template.bind({});

Templateをご覧の通り、ストーリーは通常のコンポーネントと同じ感覚で作成することができます(Vue.js の場合)。ドキュメントとして充実させるには他にも細かい設定を要しますが、まずはこのようにブラウザで閲覧可能にするところから始めてみると良いでしょう。

ストーリーを確認してみる

それでは、早速ストーリーをブラウザで確認してみましょう。yarn storybookコマンドで開発サーバーを起動しておいてください。

Canvasタブにコンポーネントが表示され、Controlsタブでコンポーネントのpropsに渡す値を変更することができます。

f:id:shota-kamezawa:20211202114449p:plain

デフォルトの設定では、colorで終わる名称のpropsをカラーピッカーで操作できるようになっています。

f:id:shota-kamezawa:20211202114456p:plain

また、サイドメニューをご覧頂いた通り、ストーリーメタデータのtitleで簡単に階層分けすることができます。私の担当プロダクトではAtomic Designを採用しているため、最上位にAtoms, Molecules, Organisms, Templatesといった階層を設定しています。

Atomic Designについてはtechtektの過去記事がありますので、そちらも是非ご覧ください。

techtekt.persol-career.co.jp

ストーリーを充実させてみる

さて、このボタンコンポーネントはcolorを指定することで背景色を変えることができましたね。もちろんControlsで操作することはできますが、コンポーネントの見た目を変えたストーリーをあらかじめ用意しておけば使い方をイメージしやすくなることでしょう。

早速ストーリーを更新してみます。以下はボタンをマゼンタ色にしたストーリーを追加する例です。

/* stories/base-button.stories.js */

/* 省略 */

// この export された変数がストーリーになります
export const Primary = Template.bind({});
Primary.args = {
  color: undefined,
};

// マゼンタ色にしたボタンのストーリー
export const Magenta = Template.bind({});
Magenta.args = {
  color: '#E4007F',
};

追加のストーリーをブラウザで見てみます。

f:id:shota-kamezawa:20211202114453p:plain

ドキュメンテーションを強化してみる

propsやslotsの詳細を表示させてみましょう。データ型やデフォルト値、説明文があると便利ですよね。

メタデータを以下のように更新してみます。

/* stories/base-button.stories.js */

// メタデータ
export default {
  /* 省略 */

  // props のデータ型、デフォルト値を表示
  parameters: {
    controls: {
      expanded: true,
    },
  },

  argTypes: {
    // props の説明文を追加
    color: {
      description: '背景色を設定します。',
    },

    // slots の説明文を追加
    default: {
      description: 'デフォルトの Vue slot',
    },
  },
};

f:id:shota-kamezawa:20211202114501p:plain

イベントを検出してログ出力してみる

実際のコンポーネントは様々なイベントを親コンポーネントへ通知できるように作成するかと思います。ボタンコンポーネントで発生するイベントをログ出力してみましょう。

以下はクリックとマウスオーバーを検出してログ出力する例です。

/* stories/base-button.stories.js */

// メタデータ
export default {
  /* 省略 */

  argTypes: {
    /* 省略 */

    // ※デフォルト設定では onXxx というパターンの名称で定義すると xxx イベントを検出できます

    // click イベントをログ出力
    onClick: {},

    // mouseover イベントをログ出力
    onMouseover: {},
  },
};

ログはActionsタブに出力されます。このようにイベントは必要に応じてログ出力し、デバッグしやすいストーリーを作るよう心がけましょう。

f:id:shota-kamezawa:20211202114505p:plain

応用編:非同期処理を行うコンポーネントのストーリー作成

コールバックの実行結果によって表示内容が変化するような複雑なコンポーネントについても、ストーリーを工夫することで簡単にモック化でき、ブラウザ上でパターンを確認できます。

非同期処理のコールバックをpropsで受け取るコンポーネントを考えてみましょう。 以下はボタンを押すとAPIコール等の非同期処理を行い、実行結果を表示するボタンコンポーネントの例です。

stories/async-button.vue (クリックして展開)

<!-- stories/async-button.vue -->

<template>
  <button @click="onClick">
    {{ message }}
  </button>
</template>

<script>
import { ref } from 'vue';

export default {
  name: 'async-button',

  props: {
    asyncCallback: {
      type: Function,
      required: true,
    },
  },

  setup(props) {
    const message = ref('Ready');

    const onClick = async () => {
      message.value = 'Loading...';

      try {
        await props.asyncCallback();
        message.value = 'Success!';

      } catch (error) {
        message.value = 'Failure!';
      }
    };

    return {
      message,
      onClick,
    };
  },
};
</script>

ここではprops.asyncCallbackをモック化したストーリーを作成します。

非同期処理の成否についてControlsタブで指定できるようにargsを設定します。このとき、コンポーネントのpropsと衝突しないようにユニークな名称にしておきましょう(この場合mockAsyncCallbackSuccess)。 また、コールバック実行最中のレイアウトも確認できるように、mockAsyncCallbackDelayで結果を遅らせるようにします。

stories/async-button.stories.js (クリックして展開)

/* stories/async-button.stories.js */

import AsyncButton from './async-button.vue';

// メタデータ
export default {
  component: AsyncButton,

  title: 'Components/Buttons/AsyncButton',

  args: {
    mockAsyncCallbackSuccess: false,
    mockAsyncCallbackDelay: 1000,
  },
};

// ストーリーテンプレート
const Template = (args) => ({
  components: {
    AsyncButton,
  },

  setup() {
    const mockAsyncCallback = async () => {
      // 実際のAPIコールに似せるため結果を遅らせる
      await new Promise((resolve) => setTimeout(resolve, args.mockAsyncCallbackDelay));

      // mockコールバックを失敗させたい場合はエラーを投げる
      if (!args.mockAsyncCallbackSuccess) {
        throw new Error();
      }
    };

    return {
      args,
      mockAsyncCallback,
    };
  },

  template: /* html */ `
    <async-button
      :async-callback="mockAsyncCallback"
    />
  `,
});

// この export された変数がストーリーになります
export const Primary = Template.bind({});

f:id:shota-kamezawa:20211202114508p:plain

それでは、ControlsのmockAsyncCallbackSuccessを切り替えながらボタンを操作してみたいと思います。

f:id:shota-kamezawa:20211202114512p:plain

ボタン押下後の1秒間は Loading... が表示され、その後は mockAsyncCallbackSuccesstrueのとき Success!falseのとき Failure! と表示されるようになりました。

このように正常系・異常系の確認といった単体テストの面でもStorybookが役立ちます。

カスタムドキュメントを作成してみる

コンポーネントが描画されたCanvasタブの隣にDocsタブがあることにお気付きでしょうか。

f:id:shota-kamezawa:20211202114515p:plain

デフォルトではCanvas + Controlsとあまり代わり映えしないページですが、ここには必要に応じて独自のドキュメントを配置することができます。

私の担当プロダクトではコンポーネントの説明をMarkdownで記述しています。ストーリーだけでは表現しきれない付加情報はこのように管理してみましょう。

<!-- stories/base-button.mdx -->

# base-button

本プロダクトにおける全ボタンのベースとなるコンポーネントです。

## Usage

````html
<base-button color="red">
  Click me!
</base-button>
````
// stories/base-button.stories.js

import mdx from './base-button.mdx';

// メタデータ
export default {
  /* 省略 */

  parameters: {
    docs: {
      page: mdx,
    },
  },
};

f:id:shota-kamezawa:20211202114522p:plain

おわりに

ここまでStorybookの良さについて記載してきましたが、ストーリーやドキュメントを作成する分の時間的コストは当然かかります。短期間に自分一人で完成させなければならないようなアプリケーションに対してはデメリットが大きくなることを否定できません。

しかし、プロダクト開発の多くはチームプレイです。コンポーネントを再利用するとき、仕様を変更するとき、新しいメンバーが加わるとき等、Storybookを活用すれば理解にかかるコストを下げることができると思います。長期的に見れば費やした時間に対してお釣りが出るくらいのリターンを見込めるでしょう。

皆さんも是非、Storybookの導入と充実したストーリーの作成を検討してみてください!


次回のアドベントカレンダーは松屋さんの “Google UX Design Certificateやってみた件” です。 明日もお楽しみに! 👋

alt

kamezawaさんのプロフィール

エンジニアリング統括部 サービス開発部 第1グループ エンジニア
好きな食べ物はアイスクリーム

※2021年12月現在の情報です。

▶エンジニアリング統括部の求人ページはこちら