Create React AppのTypeScript対応プロジェクトにStorybook v6を導入した

<h2>はじめに</h2>

Qiita初投稿なので最初に自己紹介。
都内のWEB系企業でアプリ開発をしているスズキと申します。
商社で約4年間営業として働いていたのですが、2020年7月のコロナ真っ只中にエンジニア転職しました。

記事投稿のようなアウトプットはこれまで面倒でやっていなかったのですが、知識の定着や同じ問題が起こったときの見返しメモとして有用だと思い、このたび始めた次第です。

月2くらいのペースで投稿していけたらと考えています。

ということで本題に入ります！


今回とりあげたのは、[Storybook](https://storybook.js.org/docs/react/get-started/introduction)というフロントエンド開発で最も有名なUI開発ツールです。
Githubのスターが55kを超えるような超有名ツールみたいなのですが、恥ずかしながら今まで全く知りませんでした。

私の場合はアプリのUI開発にあたるのですが、開発フローのなかでちょっとした手間が結構あるんですよね。
例えば、以下のようなことです。
・APIより先にUIができてしまったときの表示確認。
・デザイナーやディレクターとのUI微修正のやりとり。
・プロジェクト内のコンポーネントがアプリ内のどれにあたるのかわからない。

プロダクトと切り離したUI開発ができるツールとかないんかな。。。と探していたところで見つけたのがこのStorybookでした。

React Native(Expo)で早速検証したかったのですが、Storybook v6の導入に関する情報がほとんどなかったので、手始めにReact(Create React App)で検証することにしました。


<h2>インストール</h2>

プロジェクト作成の前にNode.jsとyarnがインストールされていることを確認します。
以下のコマンドでプロジェクトを作成することができます。

```
npx create-react-app my-app --template typescript
```

<code>cd my-app</code>でフォルダに移動し、以下のコマンドでStorybookを導入することができます。

```
npx sb init 
```

＊Qiitaの様々な記事で<code>npx -p @storybook/cli sb init</code>のコマンドが使われていますが、こちらはv5までの導入方法になります。今回はv6の導入記事なので、<code>npx sb init</code>を実行してください。

以下のコマンドを実行することでStorybookが起動します。

```
yarn storybook
```

![スクリーンショット 2020-12-05 17.28.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/ea2a61fd-1428-0898-22f8-b0602c959187.png)


<h2>Storybookの関連ファイルとフォルダ</h2>

ここまでのコマンド実行でプロジェクト直下に.storybook、src直下にstoriesというフォルダが作成されていると思います。
![スクリーンショット 2020-12-05 17.35.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/e7c6dcc1-4bcd-a1e1-7d6f-47f11aac0622.png)

.storybookフォルダ内のファイル構成はv6から、main.jsとpreview.jsの2ファイルになりました。
main.jsではstoryのパスや適用するアドオンを定義し、preview.jsでは全ストーリーに対してグローバルにアドオンを適用したりすることができます。

main.js

```javascript
module.exports = {
  "stories": [
    "../src/**/*.stories.mdx",
    "../src/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/preset-create-react-app"
  ]
}
```

preview.js

```javascript
export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
}
```

<h2>Storyの作成</h2>

今回はbuttonコンポーネントを使って簡単なStoryを作成します。
src直下にcomponentsフォルダを作成し、componentsフォルダの中でさらにコンポーネント単位でフォルダを作成します。
例えば、今回のケースでは、<code>src/components/Button</code>の中に<code>Button.tsx</code>, <code>Button.css</code>, <code>Button.stories.tsx</code>の3つファイルを作成します。

Button.tsx

```tsx
import React from 'react'
import './Button.css'

type Props = {
  variant: string
}

const Button: React.FC<Props> = (props) => {
  const {variant, children, ...rest} = props
  return (
    <button className={`button ${variant}`} {...rest}>
      {children}
    </button>
  )
}

export default Button
```

Button.css

```css
.button {
  border: none;
  color: white;
  padding: 15px 32px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;
  border-radius: 4px;
  cursor: pointer;
}

.primary {
  background-color: #008cba;
}
.secondary {
  background-color: #e7e7e7;
  color: black;
}
```

Button.stories.tsx

```tsx
import React from 'react'
import Button from './Button'

export default {
  title: 'form/Button',
  compoent: Button,
}

const Template = args => <Button {...args}/>

export const PrimaryA = Template.bind({})
PrimaryA.args = {
  variant: 'primary',
  children: 'Primary Args'
}
```

3箇所にわけて説明します。

```tsx
export default {
  title: 'form/Button',
  compoent: Button,
}
```
titleはStorybook内のサイドバーでの表示名を表します。
![スクリーンショット 2020-12-05 20.51.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/7af4c8a4-5cc9-f021-d1dd-41dfb7d15779.png)
componentにはimportしたtsxファイル名を指定します。


```tsx
const Template = args => <Button {...args}/>
```
こちらで各ストーリーのテンプレートを作成します。作成していくストーリーには、全てこのテンプレートを使用します。


```tsx
export const PrimaryA = Template.bind({})
PrimaryA.args = {
  variant: 'primary',
  children: 'Primary Args'
}
```
PrimaryAというストーリーを作成しました。
作成したTempleteをbindし、variantとchildrenにそれぞれ引数を与えます。
すると、Storybook内に作成したストーリーのコンポーネントが表示されます。
![スクリーンショット 2020-12-05 20.58.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/f6ae1950-dc1b-6393-0606-11820992133a.png)


Button.stories.tsxにさらに2つのストーリーを追加します。

```tsx
export const LongPrimaryA = Template.bind({})
LongPrimaryA.args = {
  ...PrimaryA.args,
  children: 'Long Primary Args'
}

export const SecondaryA = Template.bind({})
SecondaryA.args = {
  variant: 'secondary',
  children: 'Secondary Args'
}

```

さきほどのコンポーネントと同様に、Storybookへ反映されます。
![スクリーンショット 2020-12-05 21.03.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/91b109d0-3e4f-9b2d-ee3a-f41abf451386.png)
![スクリーンショット 2020-12-05 21.03.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/7608f36a-e2cb-6f65-ef9b-90616cd10556.png)


<h2>各種アドオンの紹介</h2>
 
Storybookには便利なアドオン(拡張機能)がいろいろ揃っています。
何種類かアドオンをいれて確かめてみたので、いくつか紹介します。

アドオンのインストールには以下のコマンドを用い、インストール後はmain.jsに使用するアドオンを追記してください。
*<code>@storybook/addon-docs</code>を使用する際に、<code>@storybook/addon-controls</code>がないとエラーがでました。

```
yarn add -D ~
```

main.js

```java
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-actions",
    "@storybook/preset-create-react-app",
    "@storybook/addon-viewport",
    "@storybook/addon-docs",
    "@storybook/addon-essentials",
    "@storybook/addon-controls",
    "@storybook/addon-knobs",
    "@storybook/addon-a11y"
  ]
```

<h3>addon-viewport</h3>

デフォルトではSmall, Medium, Large, Tabletの4つから画面サイズを選ぶことができるのですが、以下のようにINITIAL_VIEWPORTSをインポートすると、登録されている市販端末の画面サイズでコンポーネントの表示を確認することができます。

```tsx

import {addParameters} from '@storybook/react'
import {INITIAL_VIEWPORTS} from '@storybook/addon-viewport'

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
}

addParameters({
  viewport: {
    viewports: INITIAL_VIEWPORTS,
  }
})
```

![スクリーンショット 2020-12-05 21.51.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/6779500f-f99b-0c14-57e5-f13091fde19b.png)

<h3>addon-console</h3>

ストーリーでonClickイベントに指定したconsole.logをStorybook内で出力することができます。

Button.stories.tsx

```javascript
PrimaryA.args = {
  variant: 'primary',
  children: 'Primary Args',
  onClick: () => console.log('ButtonClicked')
}
```

preview.js

```tsx
import {addDecorator} from '@storybook/react'
import {withConsole} from '@storybook/addon-console'

addDecorator((storyFn, context) => withConsole()(storyFn)(context))
```

![スクリーンショット 2020-12-05 22.10.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/f6cd4d0c-e050-f690-8b1c-4517bd7b2873.png)

<h3>addon-docs</h3>

Storybook内のdocsタブで、コンポーネントに関するドキュメントをまとめることができます。
![スクリーンショット 2020-12-05 22.16.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/33afeb88-3804-ebd7-106d-5449f3822add.png)

<h3>addon-knobs</h3>

こちらのアドオンでは、Storybook下部のknobsタブ内でpropsを手動で変更することができます。
以下では、Buttonのdisabledとchildrenを変更できるようにしました。

preview.js

```tsx
import {addDecorator} from '@storybook/react'
import {withKnobs} from '@storybook/addon-knobs'

addDecorator(withKnobs)
```

stories.tsxファイルがv6の書き方だとうまくいかず、v5の書き方だとうまくいきました。
うまく行かなかった理由については調査中です。。。

Button.stories.tsx

```tsx
export const Knobs  = () => (
  <Button variant='primary' disabled={boolean('Disabled', false)}>
    {text('Label', 'Button Label')}
  </Button>
)
```
![スクリーンショット 2020-12-05 22.45.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/82aea9f6-024f-2bc1-56c1-08190612e30f.png)

<h3>addon-a11y</h3>

コンポーネントのアクセシビリティを確認することができます。
https://ja.reactjs.org/docs/accessibility.html

preview.js

```javascript
import {addDecorator} from '@storybook/react'
import {withA11y} from '@storybook/addon-a11y'

addDecorator(withA11y)
```

![スクリーンショット 2020-12-05 22.54.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/c33307ab-01aa-753b-e56b-d0bac8e4f347.png)

<h2>おわりに</h2>

初めての記事作成非常に疲れました。。。
よくいわれることですが、アウトプットしてみると知識の穴がめちゃくちゃ見つかりますね。

記事内容であやしい部分があったら都度修正していきます。

それでは！