112
81

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Moff EngineeringAdvent Calendar 2019

Day 5

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

Last updated at Posted at 2019-12-04

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

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

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

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

作成したプロトタイプの全体像

最初にどんなアプリなのかをスクリーンショットで提示してから、詳細な解説を進めていきます。
基本的なアプリに最低限必要な認証一覧表示登録編集 機能をプロトタイピングで再現しました。
マテリアルデザイン(Material-ui)をベースとしています。

ログイン画面

  • ユーザーサインイン
  • ユーザー新規登録
  • パスワードリセット

アルバム一覧・投稿画面

  • アルバム一覧表示
  • アルバム投稿
  • アルバム編集
  • アルバム削除

なぜFigmaを使うのか

Figma公式サイト
https://www.figma.com/

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

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

Figmaで作るスタイルガイド

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

フォント

Material-uiにもともと定義されているタイポグラフィ+タイトルのフォントを定義。

カラー

  • ベースカラー → Material-uiのオリジナルカラーgreyのカラーパターン
  • メインカラー → 独自のネイビー系の配色darkmainlightの3パターン
  • アクセントカラー → 独自のオレンジ系の配色darkmainlightの3パターン

アイコン・ボタン・フォーム

Figma便利機能

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

コンポーネント

図形やテキストなどのパーツをまとめたものをコンポーネントとして定義することができます。
画面上部メニューにある菱形アイコンをクリックするか、パーツを全て選択右クリックCreate Componentで簡単にコンポーネントを作成できます。

コンポーネントはコピーすることで、インスタンスを生成することができ、コピー元コンポーネント(マスターコンポーネント)の変更がリアルタイムで反映されるようになります。
生成されたインスタンスはパーツの位置のみは固定で、以下項目についてはオーバーライドして独自のコンポーネントを作成することが可能です。

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

コンストレイント

上記のコンポーネント機能で、インスタンスを作成しコンポーネントのサイズ感を変更したい。しかし、コンポーネントのサイズを変更すると図形の比率や配置が変更されてしまった。そんなときに使えるのがコンストレイント機能です。コンストレイント機能を使うと、「左上」「右上」「左下」「右下」「中央」の中から、位置固定の基準位置を設定することができます。

コンポーネントの整列

プロトタイプを作成していると、複数のコンポーネント間を一発で調整したい時があるはずです。Figmaなら整列ボタンを使わなくても直感的にドラックアンドドロップで調整できるので作業効率が上がります。Illustratorやphotoshopではこんな機能なかったのですごく便利に感じました。

適当に配置した、オブジェクトをいい感じに整列してくれる「Tidy Up」もそこそこ便利です。整列ボタンから選択できます。

画像の一括配置

画像も複数画像を選択してサクサク配置することができます。アプリのイメージを変えたいなと思った時も10秒とかからず変更することができます。ただ、Sketchのようにテキストの一括流し込みができないのは残念です。

プロトタイピング

Figmaでは画面のレイアウトを作成したら、そのままプロトタイピングも実装できます。サイドバーメニューからPrototypeタブを選択し、あとはトリガーとなるボタンまたはフォームから遷移先のフレームに以下画像のように動線を結べば「クリックしたら画面遷移」や「ホバーしたらコンポーネント表示」などを実現させることができます。

アニメーションについては複雑な動作はできませんが、プロトタイピングの段階では画面遷移だけわかれば良いということがほとんどだと思うので私は十分だと思いました。フェードイン・アウトやムーブイン・アウトなど基本的動作は普通にできます。

コラボレーション

Figmaの一番の強みコラボレーション機能についてです。今回のアプリ制作は1人で作業していたためあまり効力を発揮しませんでしたが、試しに2台PCと2アカウントを用意してコラボレーション機能を試してみました。タイムラグが発生して使いにくいのではないかと思っていましたが、Google Documentのようにデザイン変更はリアルタイムで反映されストレスなく作業を行うことができました。何より、ファイルの受け渡しなく同時平行で複数人が作業・コメントできるのは作業効率や管理コスト面で相当便利な機能だと思います。


なぜStoryBookを使うのか

StoryBook公式サイト
https://storybook.js.org/

StoryBookはUI Component 管理ツールという位置づけです。スタイルガイドを作成するだけなら、上記で解説したFigmaでも可能です。しかし、実際運用・保守の観点で考えるとデザイン改修の際にFigmaファイルを修正して、その上でアプリのコードを修正するとなると2度手間になってしまいます。Storybookを利用するとアプリのコードをそのままスタイルガイドにできるので、スタイルガイドを修正する=アプリのコードを修正するということになり「アプリ側のコードだけしか修正されずスタイルガイドの意味がなくなってしまった」というリスクを防ぐことが可能になります。
Figmaはベータ版の段階でざっくりとスタイルガイドを作成する、ある程度案が固まって実際にコードを書き始めたらStorybookでしっかりとスタイルガイドを作り込んで運用するといった感じになると思います。StorybookはFigmaのようにGUIベースではなくコードベースなので、それなりに学習コストもかかりますが、作成したコンポーネントはそのままアプリで使えますのでスタイルガイド作成作業が全く無駄になってしまうということはないと思います。

Storybook for Reactの導入

Storybookを導入したいreactアプリケーションのプロジェクトルートで以下コマンドを入力すると自動でStorybookのsettingを行ってくれます。

shell
npx -p @storybook/cli sb init --type react

以下コマンドでStorybookを起動するとブラウザでスタイルガイドが開きます。

shell
yarn storybook
6074184e7a098f7a94042679654ca0e8.png

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

.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;
 };
.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ファイルを書き換えます

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"]
}

Storybookアドオン

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

shell
npm i -D @storybook/{ アドオン名 }

addons.jsファイルに以下コードを記述

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

addon-knobs

スタイルガイドの対象コンポーネントにpropsを渡して、コンポーネントの状態を変化させることができます。GUIで簡単に確認できるので、非エンジニアでもコンポーネントのデザインチェックを行うことができます。

コード一例
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などのイベントログを表示することができます。

コード一例
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形式でスタイルガイドを作成したりできるようです。
f11fb2d33d7d27652c18d232480cb4f9.png

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

.storybook/presets.js
module.exports = ['@storybook/addon-docs/react/preset'];

mdxを使いたい場合config.jsを書き換えます

.storybook/config.js
configure(require.context('../stories', true, /\.stories\.(tsx|mdx)$/), module);

以下のように.mdx形式ファイルは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スナップショットを行うために必要です。

npm i -D require-context.macro react-test-renderer

次にconfig.jsを書き換えます

.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を作成し、以下のコードを記述します。

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

initStoryshots();

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

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テストを再度行うことで、変更したコンポーネントが正しいものとして登録されます。

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.

まとめ

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

112
81
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
112
81

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?