Figma ✕ React Storybook で作るプロトタイピング & スタイルガイド

アプリを開発する前に予め、開発の全体像を把握するためにプロトタイプを作成します。
プロトタイピングツールで作成したUIは、実際のアプリ開発にも流用できるため効率的です。

今回は簡単な`写真共有Webブラウザアプリ`を制作すると仮定して、

1. プロトタイピングツール`Figma`でプロトタイプ作成
2. `Reactコンポーネント`への落とし込み 
3. `Storybook`での管理 

という一連の流れを実際に実装しながら`Figma`や`Storybook`を導入することでどういったメリットがあるのか・どういったことができるのかの検証を行います。

# <font color="LightSeaGreen">作成したプロトタイプの全体像</font>
最初にどんなアプリなのかをスクリーンショットで提示してから、詳細な解説を進めていきます。
基本的なアプリに最低限必要な`認証`・`一覧表示`・`登録`・`編集` 機能をプロトタイピングで再現しました。
マテリアルデザイン（Material-ui）をベースとしています。

## ログイン画面
- ユーザーサインイン
- ユーザー新規登録
- パスワードリセット

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/146c7fb2-1066-6554-da8d-eb3e177f0917.gif" width="800">

## アルバム一覧・投稿画面
- アルバム一覧表示
- アルバム投稿
- アルバム編集
- アルバム削除

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/f403e284-9d6e-96be-4fa0-75d23bf65e21.gif" width="800">

***

# <font color="LightSeaGreen">なぜFigmaを使うのか</font>
> Figma公式サイト
> https://www.figma.com/

プロトタイピングツールには`Sketch`・`AdobeXD`など様々な種類がありますが、今回`Figma`を選択したのは以下の理由からです。

- ブラウザからアクセスしサインインするだけで、ほとんどの機能を無料で使える
- コンポーネント・スタイルの管理が楽そう
- 機能面でSketchやXDと遜色なさそう
- リアルタイムコラボレーションを試してみたかった
- 話題の`web assembly`で記述されているwebアプリの操作性を実際に体験してみたかった

# <font color="LightSeaGreen">Figmaで作るスタイルガイド</font>
若干我流かもしれませんが`Atomic Design`を意識して、スタイルガイドを作成してみました。後ほど、`Material-ui`でコードを組む予定だったので、フォント・カラーなどの命名・パターンは極力Material-uiデフォルトのものを使用し齟齬が出ないようにしました。
Figmaではカラー・フォントはローカルスタイルとして定義することができ、定義されたスタイルをボタンやフォームなどに適用すれば、同一のスタイルを一括で変更したりと変更に強いデータを作成することが可能です。

## フォント
Material-uiにもともと定義されているタイポグラフィ+タイトルのフォントを定義。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/91f30694-09c9-82b8-c588-fdfb623b218a.png" width="800">

## カラー
- ベースカラー → Material-uiのオリジナルカラー`grey`のカラーパターン
- メインカラー → 独自のネイビー系の配色`dark`・`main`・`light`の３パターン
- アクセントカラー → 独自のオレンジ系の配色`dark`・`main`・`light`の３パターン

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/abc79ca3-1d7d-b1c6-ba81-aebab9889377.png" width="800">

## アイコン・ボタン・フォーム
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/5b2b7b9e-9e3d-d00b-8e23-8318d55f5e78.png" width="100%">


# <font color="LightSeaGreen">Figma便利機能</font>
Figmaは奥が深く、いろいろと便利機能があるのですが、私が利用するなかで特に便利だと思った機能について解説を行います。

## コンポーネント
図形やテキストなどのパーツをまとめたものをコンポーネントとして定義することができます。
画面上部メニューにある菱形アイコンをクリックするか、パーツを全て選択右クリック`Create Component`で簡単にコンポーネントを作成できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/57d56b12-f324-d6f2-5c4a-56a7e8d0298c.png" width="800">
コンポーネントはコピーすることで、インスタンスを生成することができ、コピー元コンポーネント（マスターコンポーネント）の変更がリアルタイムで反映されるようになります。
生成されたインスタンスはパーツの位置のみは固定で、以下項目についてはオーバーライドして独自のコンポーネントを作成することが可能です。

- カラー
- 画像
- テキストの内容
- コンポーネントのサイズ

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/2658977e-e263-f50c-4e13-8c7175b67f15.gif" width="800">

## コンストレイント
上記のコンポーネント機能で、インスタンスを作成しコンポーネントのサイズ感を変更したい。しかし、コンポーネントのサイズを変更すると図形の比率や配置が変更されてしまった。そんなときに使えるのがコンストレイント機能です。コンストレイント機能を使うと、「左上」「右上」「左下」「右下」「中央」の中から、位置固定の基準位置を設定することができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/517faf7f-45f1-2a81-29f1-6e76db197e94.jpeg" width="500">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/11d46878-f484-424d-f9a7-a9239713d909.gif" width="800">

## コンポーネントの整列
プロトタイプを作成していると、複数のコンポーネント間を一発で調整したい時があるはずです。Figmaなら整列ボタンを使わなくても直感的にドラックアンドドロップで調整できるので作業効率が上がります。Illustratorやphotoshopではこんな機能なかったのですごく便利に感じました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/762e8cd4-41cd-37ca-92e3-c7a181fe1e17.gif" width="800">
適当に配置した、オブジェクトをいい感じに整列してくれる「Tidy Up」もそこそこ便利です。整列ボタンから選択できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/63ae6ea7-5f70-73e0-2b0d-760d521743dd.gif" width="600">

## 画像の一括配置
画像も複数画像を選択してサクサク配置することができます。アプリのイメージを変えたいなと思った時も10秒とかからず変更することができます。ただ、Sketchのようにテキストの一括流し込みができないのは残念です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/7a3e8f90-98c1-d7cf-1da7-d4b10233cea7.gif" width="800">

## プロトタイピング
Figmaでは画面のレイアウトを作成したら、そのままプロトタイピングも実装できます。サイドバーメニューから`Prototype`タブを選択し、あとはトリガーとなるボタンまたはフォームから遷移先のフレームに以下画像のように動線を結べば「クリックしたら画面遷移」や「ホバーしたらコンポーネント表示」などを実現させることができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/ff8010ad-ba0e-0d45-db34-c9c6f97add6c.jpeg" width="800">
アニメーションについては複雑な動作はできませんが、プロトタイピングの段階では画面遷移だけわかれば良いということがほとんどだと思うので私は十分だと思いました。フェードイン・アウトやムーブイン・アウトなど基本的動作は普通にできます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/c51e18d8-d7bf-2fb9-2126-2e41f598e9b9.gif" width="800">

## コラボレーション
Figmaの一番の強みコラボレーション機能についてです。今回のアプリ制作は１人で作業していたためあまり効力を発揮しませんでしたが、試しに2台PCと2アカウントを用意してコラボレーション機能を試してみました。タイムラグが発生して使いにくいのではないかと思っていましたが、Google Documentのようにデザイン変更はリアルタイムで反映されストレスなく作業を行うことができました。何より、ファイルの受け渡しなく同時平行で複数人が作業・コメントできるのは作業効率や管理コスト面で相当便利な機能だと思います。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/8be39a16-6664-67ea-d4d6-418b81a7564e.gif" width="800">

***

# <font color="LightSeaGreen">なぜStoryBookを使うのか</font>
> StoryBook公式サイト
> https://storybook.js.org/

StoryBookは`UI Component 管理ツール`という位置づけです。スタイルガイドを作成するだけなら、上記で解説した`Figma`でも可能です。しかし、実際運用・保守の観点で考えるとデザイン改修の際にFigmaファイルを修正して、その上でアプリのコードを修正するとなると2度手間になってしまいます。`Storybook`を利用するとアプリのコードをそのままスタイルガイドにできるので、スタイルガイドを修正する=アプリのコードを修正するということになり「アプリ側のコードだけしか修正されずスタイルガイドの意味がなくなってしまった」というリスクを防ぐことが可能になります。
Figmaはベータ版の段階でざっくりとスタイルガイドを作成する、ある程度案が固まって実際にコードを書き始めたらStorybookでしっかりとスタイルガイドを作り込んで運用するといった感じになると思います。StorybookはFigmaのようにGUIベースではなくコードベースなので、それなりに学習コストもかかりますが、作成したコンポーネントはそのままアプリで使えますのでスタイルガイド作成作業が全く無駄になってしまうということはないと思います。
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/5e648043-adfe-f49e-a483-b49753e27611.gif">
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/e755f386-76bb-0ccd-e6c8-ca2b1a5338cb.gif">



# <font color="LightSeaGreen">Storybook for Reactの導入</font>
Storybookを導入したいreactアプリケーションのプロジェクトルートで以下コマンドを入力すると自動でStorybookのsettingを行ってくれます。

```shell:shell
npx -p @storybook/cli sb init --type react
```
以下コマンドでStorybookを起動するとブラウザでスタイルガイドが開きます。

```shell:shell
yarn storybook
```
<img width="800" alt="6074184e7a098f7a94042679654ca0e8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/740ef96e-731d-2974-0a84-3ce5f2722f99.png">

`typescript`を使用する場合は、まず`.storybook`配下の2つのファイルに変更を加えます。
（ファイルが存在しなければ追加します）

```javascript:.storybook/webpack.config.js
module.exports = ({ config }) => {
  config.module.rules.push({
    test: /\.(ts|tsx)$/,
    use: [
      {
        loader: require.resolve('babel-loader'),
        options: {
          presets: [['react-app', { flow: false, typescript: true }]],
        },
      },
      {
        loader: require.resolve('react-docgen-typescript-loader'),
      },
    ],
  });
  config.resolve.extensions.push('.ts', '.tsx');
  return config;
 };
```
```javascript:.storybook/config.js
import { configure } from '@storybook/react';

// automatically import all files ending in *.stories.js
configure(require.context('../stories', true, /\.stories\.tsx$/), module);
```

次にプロジェクトルートに存在する`tsconfig.json`ファイルを書き換えます

```json:tsconfig.json
{
  "compilerOptions": {
    "outDir": "build/lib",
    "module": "commonjs",
    "target": "es5",
    "lib": ["es5", "es6", "es7", "es2017", "dom"],
    "sourceMap": true,
    "allowJs": false,
    "jsx": "react",
    "moduleResolution": "node",
    "rootDirs": ["src", "stories"],
    "baseUrl": "src",
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "suppressImplicitAnyIndexErrors": true,
    "noUnusedLocals": true,
    "declaration": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build", "scripts"]
}
```

# <font color="LightSeaGreen">Storybookアドオン</font>
Storybook単体ではスタイルガイドの機能としては不十分なので、アドオンを追加していきます。
アドオンの追加は基本的には以下の手順です。
アドオンのインストールを行い

```shell:shell
npm i -D @storybook/{ アドオン名 }
```
addons.jsファイルに以下コードを記述

```javascript:.storybook/addons.js
import '@storybook/{ アドオン名 }/register';
```


## addon-knobs
スタイルガイドの対象コンポーネントに`props`を渡して、コンポーネントの状態を変化させることができます。GUIで簡単に確認できるので、非エンジニアでもコンポーネントのデザインチェックを行うことができます。
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/2dd8e90e-78c9-35cc-f3ca-cc6b11e68154.gif">

```typescript:コード一例
import React from 'react';
import { withKnobs, text, boolean, select } from '@storybook/addon-knobs';

import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';

import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import StarBorderIcon from '@material-ui/icons/StarBorder';

import { storiesOf } from '@storybook/react';

const colorOptions: any = {
  primary: 'primary',
  secondary: 'secondary',
  inherit: 'inherit',
  default: 'default',
};

const sizeOptions: any = {
  small: 'small',
  medium: 'medium',
  large: 'large',
};

const ButtonComponent: React.FC = () => {
  return (
    <List>
      <ListItem>
        <Button
          color={select('Color', colorOptions, colorOptions.primary)}
          size={select('Size', sizeOptions, sizeOptions.medium)}
          disabled={boolean('disabled', false)}
        >
          {text('Label', 'Menu Button')}
        </Button>
      </ListItem>
      <ListItem>
        <IconButton
          color={select('Color', colorOptions, colorOptions.primary)}
        >
          <StarBorderIcon />
        </IconButton>
      </ListItem>
    </List>
  );
};

storiesOf('./Button', module)
  .addDecorator(withKnobs)
  .add('./Button', () => <ButtonComponent />);
```

## addon-actions
`onClick`などのイベントログを表示することができます。
<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/a6a07d48-29a9-8fd3-c170-c89215e2de7d.gif">

```typescript:コード一例
import React from 'react';
import { action } from '@storybook/addon-actions';

import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';

import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import StarBorderIcon from '@material-ui/icons/StarBorder';

import { storiesOf } from '@storybook/react';

const ButtonComponent: React.FC = () => {
  return (
    <List>
      <ListItem>
        <Button
          onClick={action('button-click')}
        >
          Menu Button
        </Button>
      </ListItem>
      <ListItem>
        <IconButton
          onClick={action('icon-button-click')}
        >
          <StarBorderIcon />
        </IconButton>
      </ListItem>
    </List>
  );
};

storiesOf('./Button', module)
  .add('./Button', () => <ButtonComponent />);
```

## addon-docs
`addon-info`の上位互換のアドオンです。`markdown`を使ったドキュメントを作ったり、`MDX`形式でスタイルガイドを作成したりできるようです。
<img width="800" alt="f11fb2d33d7d27652c18d232480cb4f9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/41d1fb39-e449-c6e0-017c-43b4360ad665.png">

導入にはnpmモジュールのインストールの他に一手間かける必要があります。
まず、`.storybook`配下に`presets.js`を作成し、以下コードを記述します。

```typescript:.storybook/presets.js
module.exports = ['@storybook/addon-docs/react/preset'];
```
`mdx`を使いたい場合`config.js`を書き換えます

```typescript:.storybook/config.js
configure(require.context('../stories', true, /\.stories\.(tsx|mdx)$/), module);
```
以下のように`.mdx`形式ファイルは`markdown`が使えるため、簡単にきれいなドキュメントを作成できます。

```markdown:storyコード一例（mdx使用）
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { ButtonComponent } from './Button'; //reactコンポーネントを読み込み

<Meta title="MDX|ButtonComponent" component={ButtonComponent} />

# ButtonComponent

With `MDX` we can define a story for `ButtonComponent` right in the middle of our
markdown documentation.

<Preview>
  <Story name="ButtonComponent">
    <ButtonComponent />
  </Story>
</Preview>

|  TH  |  TH  |
| ---- | ---- |
|  TD  |  TD  |
|  TD  |  TD  |

```

## addon-storyshots
`addon-storyshots`を使えば、jestと組合あせてStorybookに登録されているコンポーネントのテストを行うことができます。Storybookに登録されているコンポーネントに何かしらの変更を加えるとエラーとして検出され異常をすぐに検知することができます。
こちらも`addon-docs`同様、導入にはnpmモジュールのインストールの他に一手間かける必要があります。
ただ、今回は`addon.js`へのregister記述は必要ありません。

まず、追加で以下のモジュールをインストールします。jestスナップショットを行うために必要です。

```shell
npm i -D require-context.macro react-test-renderer
```
次に`config.js`を書き換えます

```typescript:.storybook/config.js
import { configure } from '@storybook/react';
import requireContext from 'require-context.macro';

// automatically import all files ending in *.stories.js
const req = requireContext('../stories', true, /\.stories\.(tsx|mdx)$/);

function loadStories() {
  req.keys().forEach(filename => req(filename));
}

configure(loadStories, module);
```
最後にjestが検知されるパス（おそらくデフォルトは`.src/`）に`storyshots.test.tsx`を作成し、以下のコードを記述します。

```typescript:src/storyshots.test.tsx
import initStoryshots from '@storybook/addon-storyshots';

initStoryshots();
```

jestテストを実行すると、コンポーネントテストが行われ、src配下`__snapshots__`フォルダ内にスナップショットが記録されます。

```shell
yarn test

 PASS  src/storyshots.test.tsx
  Storyshots
    ./Button
      ✓ ./Button (64ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 passed, 1 total
Time:        3.563s
Ran all test suites.
```

少しでも変更するとエラーになります。スタイルガイドに変更を加えたい場合は、`__snapshots__`を一度削除してjestテストを再度行うことで、変更したコンポーネントが正しいものとして登録されます。

```shell
FAIL  src/storyshots.test.tsx
  Storyshots
    ./Button
      ✕ ./Button (57ms)

  ● Storyshots › ./Button › ./Button

    expect(received).toMatchSnapshot()

    Snapshot name: `Storyshots ./Button ./Button 1`

    - Snapshot
    + Received

    @@ -23,11 +23,11 @@
            type="button"
          >
            <span
              className="MuiButton-label"
            >
    -         Menu Button
    +         Menu But
            </span>          </button>
        </li>
        <li
          className="MuiListItem-root MuiListItem-gutters"

      at match (node_modules/@storybook/addon-storyshots/dist/test-bodies.js:27:20)
      at node_modules/@storybook/addon-storyshots/dist/test-bodies.js:39:10
      at Object.<anonymous> (node_modules/@storybook/addon-storyshots/dist/api/snapshotsTestsTemplate.js:44:33)

 › 1 snapshot failed.
Snapshot Summary
 › 1 snapshot failed from 1 test suite. Inspect your code changes or press `
u` to update them.

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   1 failed, 1 total
Time:        4.575s
Ran all test suites.
```

# <font color="LightSeaGreen">まとめ</font>
プロトタイピングやスタイルガイド作成は正直面倒です。ですが、全体イメージ・根底ルールをしっかりと定めておくと、改修フェーズで圧倒的に手戻りが少なく楽をすることができます。また、「Storybookに乗せるためにReactのコードをきれいに書かなくては」という衝動にもかられ、コードを書いていく上でもガイドラインがあるとないとでは全く意識が変わってきます。私自身まだ手探り状態なところではありますが、今回の学びを業務にも活かしていこうと思います。
ちなみに今回プロトタイプで作った、`写真共有Webブラウザアプリ`をReactで実装する過程を次回以降のアドベントカレンダーの投稿で記述するつもりです。