Storybookを導入しよう！

# はじめに
こんにちは！株式会社80&Companyの技術広報です。
弊社の開発部署では毎週火曜日の朝9:30から10:00に社内勉強会を行なっています。

今回の記事は社内のエンジニアがフロントエンド業務を担当していた際に、Storybookを初めて触ったことに関して社内勉強会で発表したものです。
Storybookを初めて触る際はぜひ参考にしてみて下さい♪

# 読者の対象
・Storybookを知らない方、使用したことがない方
・Storybookに興味がある方

# 事前知識
・Atomic DesignとTailwind CSSに対する理解があること

# Storybookとは
まずはStorybookの概要について簡単に説明します。
StorybookとはUIコンポーネント開発環境を提供するオープンソースのツールです。
各componentのUIやpropsに応じた挙動の確認、component単位でドキュメントの用意が可能です。
React、Vue、Angularなどの主要なJSフレームワークで導入でき、利用範囲も広くなっています。

React向けStorybookの概要については下記のページに書いてあります。

https://storybook.js.org/tutorials/intro-to-storybook/react/ja/get-started/

弊社の勉強会でStorybookについて調べてきたエンジニアがプロジェクトへの導入を志願したことがきっかけでStorybookについての使用を検討するようになりました。

# コンポーネント作成(Button)について
まずは簡単なコンポーネントカタログの作成を例に説明します。
今回のボタンコンポーネントはAtomic Designのatoms(原子)の一つとしてカウントしています。
Atomic Designの設計思想はコンポーネント志向のReactと相性がいいので、Atomic DesignはReactでプロダクトを作る場合に検討されることが多いです。

それではButtonコンポーネントの作成の流れを確認しましょう。

## 1. storiesファイルの作成

Storybookを使用する際にはコンポーネントごとにstoriesファイルの作成が必要です。

```react:Button/Button.stories.tsx
import Button from 'components/Button/Button'
import type { Meta, Story } from '@storybook/react';

export default {
  component: Button,
  title: 'front/button/Button',
} as Meta;

const Template: Story<ButtonProps> = (args: ButtonProps) => (
  <ButtonContainer tag='button' onClick={() => {}} onTimeout>
    <Button {...args} />
  </ButtonContainer>
);

export const Default = Template.bind({});
Default.args = {
  text: 'Button',
  color: 'primary',
};

```


export defaultでは、Storybookのタイトルや対象のコンポーネントを定義しています。
ここで定義したファイル構成が実際にStorybook上での構成となります。
```typescript:Button/Button.stories.tsx
import Button from 'components/Button/Button'
import type { Meta, Story } from '@storybook/react';

export default {
  component: Button,// 対象のコンポーネントを指定
  title: 'front/button/Button',// Storybookのディレクトリ・ファイル名
} as Meta;

```

ここでは実際にStorybookに表示するDefaultボタンを定義します。 
初めにargsで受け取った引数をButtonのコンポーネントに渡すTemplateを定義して、引数によって表示が異なるButtonで使いまわせるようにします。
その後、Templateをbindしてargsに引数を定義し、Buttonコンポーネントに必要な引数が渡されDefaultのButtonを表示します。

```react:Button/Button.stories.tsx
import Button from 'components/Button/Button'
import type { Meta, Story } from '@storybook/react';
import type { ButtonProps } from 'components/ui/button/Button/Button.type';
import { ButtonContainer } from 'components/ui/button/lib/ButtonContainer/ButtonContainer';

const Template: Story<ButtonProps> = (args: ButtonProps) => (
  <ButtonContainer tag='button' onClick={() => {}} onTimeout>
    <Button {...args} />　 //argsで引数を受け取って、Buttonに渡すTemplateを定義。
  </ButtonContainer>
);

export const Default = Template.bind({}); //各StoryでTemplateをBindして再利用
Default.args = {// 引数を定義
  text: 'Button',
  color: 'primary',
};

```

## 2. Button.type.tsの作成

与えるpropsでテキスト、カラー、サイズなどの変化をつけるために複数の値を`Button.type.ts`で設定します。

```typescript:Button.type.ts
import type { ReactNode } from 'react';

export type ButtonProps = {
  text: string | ReactNode;
  color:
    | 'none'
    | 'primary'
    | 'success'
　　 | 'opposite';
  size: 'none' | 'md' | 'full';
  textSize: 'none' |'md' | 'lg';
  textAlign: 'left' | 'center' | 'right';
  shape: 'none' | 'rounded' | 'square';
};
```

## 3. Button.styles.tsの作成

Tailwind CSSと呼ばれるCSSフレームワークを導入しており、`Button.styles.ts`ファイルでpropsに与えられた値ごとにクラス指定を行っています。
```typescript:Button.styles.ts
import { ButtonProps } from 'components/Button/Button.type';

export const getColor = (key: ButtonProps['color']) =>
  new Map<ButtonProps['color'], string>([
    ['none', ''],
    ['primary', 'text-white bg-primary hover:bg-primary-hover'],
    ['opposite', 'text-white bg-opposite hover:bg-opposite-hover'],
    ['success', 'text-white bg-success  hover:bg-success-hover'],
  ]).get(key);

export const getShape = (key: ButtonProps['shape']) =>
  new Map<ButtonProps['shape'], string>([
    ['none', 'rounded-none'],
    ['rounded', 'rounded'],
    ['square', 'rounded-sm'],
  ]).get(key);

export const getSize = (key: ButtonProps['size']) =>
  new Map<ButtonProps['size'], string>([
    ['none', ''],
    ['sm', 'w-32 block'],
    ['md', 'w-60 block'],
    ['full', 'w-full block'],
  ]).get(key);

export const getTextSize = (key: ButtonProps['textSize']) =>
  new Map<ButtonProps['textSize'], string>([
    ['none', ''],
    ['sm', 'text-sm'],
    ['md', 'text-base'],
  ]).get(key);

export const getTextAlign = (key: ButtonProps['textAlign']) =>
  new Map<ButtonProps['textAlign'], string>([
    ['left', 'text-left'],
    ['center', 'text-center'],
    ['right', 'text-right'],
  ]).get(key);
```

## 4. Button.tsxの作成

ボタンのマークアップは`Button.tsx`で行います。
```react:Button.tsx
import { ButtonProps } from 'components/Button/Button.type';
import {
  getColor,
  getSize,
  getTextAlign,
  getShape,
  getTextSize,
} from 'components/Button/Button.styles';

export default function Button({
  color = 'white',
  size = 'md',
  textSize = 'md',
  textAlign = 'center',
  shape = 'square',
  text = 'Button',
  children,
}: ButtonProps) {
  const textCenter = 'flex justify-center items-center';
  const colorStyle = getColor(color);
  const sizeStyle = getSize(size);
  const textAlignStyle = getTextAlign(textAlign);
  const shapeStyle = getShape(shape);
  const textSizeStyle = getTextSize(textSize);

  return (
    <>
        <button
          className={`${textCenter} ${colorStyle} ${sizeStyle} ${shapeStyle} ${textAlignStyle} ${textSizeStyle}`}
        >
         {children || text}
        </button>
    </>
  );
}

```

上記の4つのファイル構成を作成できたらButtonコンポーネントを使い回すことができます。propsに`Button.type.ts`で定義した値を渡すことでテキスト文字や色、大きさを変えることができるようになります。

```html:
<Button
 text='チケットを追加購入する'
 shape='rounded'
 textSize='md'
 textAlign='center'
 color='primary'
 size='full'
/>
```

Buttonコンポーネントは完成したものの、作成者以外の方がコンポーネント使用時に下記のようなデザインの出力をコードから予測することは難しいです。
![スクリーンショット 2022-12-21 10.49.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3059557/aaed4498-c99b-c82f-d583-d766884ac4b4.png)
作成者自身でも`Button.types.ts`や`Button.styles.ts`で定義されたpropsの値を確認する時間コストがかかります。
そこで以下のStorybookを用いて、コンポーネントのUIの管理や変更を行いました。


# Storybook実践、作成方法
導入しているフォルダー配下で
```shell:
$ npm run storybook
```
上記をターミナルで打ち、以下のような画面が自動的にブラウザで開きます。

![スクリーンショット 2022-12-21 13.36.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3059557/b9e5b34c-dcdd-2f8b-d607-59ba6a61fc80.png)

1. テキスト文字や色の変更が可能
右端の選択肢**text**と**color**の値を変更するごとに、
上記のボタンの文字や色をリアルタイムで変更することができます。
![スクリーンショット 2022-12-21 13.39.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3059557/5dfc2400-c70f-b326-392c-c76c381befd6.png)

2. 画面サイズごとのコンポーネントの写り方を確認することが可能
iPhoneなどの画面サイズ時の写りを確認することができます。
![スクリーンショット 2022-12-21 13.39.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3059557/cc81200d-75bc-9bda-f9f9-aa4fd71fc273.png)

3. コンポーネントのソースコードが出力される
左上の文字**Canvas**の右横にある**Docs**を押すと下記のような画像が出てきます。
右下の**Show code**を押すとソースコードが出力され、コード上で使い回すこともできます。
![スクリーンショット 2022-12-21 14.15.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3059557/89c3b2f6-4125-ac98-8269-fce276e10c92.png)


# Storybookを導入するメリット
* リアルタイムで要素の変更を確認できる
    * コンポーネントの仕様を理解するための時間の削減を行えるようになった。
* コンポーネント使用時のソースコードをカタログから探すことが出来る
    * すぐにコピー&ペーストで再利用が出来るようになった。
* 大きさや色のパターンを柔軟に確認しやすくなった。

# Storybookを導入するデメリット
* コンポーネント作成ごとにstoryファイルを作成する必要がある
    * ファイルの管理コストがかかってしまう。
* 自動でコンポーネントが登録されてはない
    * 追加されたデザインパーツは都度 Storybookへの反映が必要。

# 最後に
今回の記事では、コンポーネントの作成からStorybookを実際に使用するまでの過程を扱いました。
「Storybook便利そうだから使ってみようかな。」と思ってもらえたら嬉しいです。

今後も継続的に80&Companyの社内勉強会の取り組みを発信していきます！
[Qiita Organization](https://qiita.com/organizations/80andco)のフォローもよろしくお願いいたします！
最後まで読んでいただきありがとうございます！

# 参考資料
・Storybook(公式サイト)
https://storybook.js.org/

・Storybookを理解する
https://zenn.dev/kyo9bo/articles/bd37f814b33909#storybook%E3%81%A8%E3%81%AF

・Storybookを使ってプロダクトのデザイン管理をやってみた
https://service.plan-b.co.jp/blog/tech/29109/

・ゼロから始めるStorybook入門(React入門)
https://reffect.co.jp/html/storybook

・React で実装する atomic design のコンポーネントごとの責務の分け方とコード規約
https://qiita.com/takano-h/items/8731d8e7413d7b1f6d7b#%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E7%9F%A5%E8%AD%98%E3%82%92%E6%8C%81%E3%81%A4%E3%81%A8%E3%81%AF

・Storybook超入門（＋Next.js＋TypeScript）
https://qiita.com/masakinihirota/items/ac552b8b492d2b962818