効率的なUI開発の鍵：Next.js (TypeScript) にStorybookを導入 (storybook/main.jsではなくstorybook/main.tsを生成し構築)

<!-- TOC -->
  * [摘要](#摘要)
  * [プロジェクト の作成](#プロジェクト-の作成)
  * [Storybook のインストール](#storybook-のインストール)
  * [Storybook の設定](#storybook-の設定)
  * [番外編](#番外編)
  * [最後に](#最後に)
<!-- TOC -->


## 摘要
本稿では、Next.js (TypeScript) プロジェクトに Storybook を導入し、設定する手順を示す。Storybook は UI コンポーネントの開発を支援するツールであり、効率的な開発を実現することができる。番外編ではatomicデザインのatomの部分を作成し、storybookで確認を行う。

## プロジェクト の作成
まずは、プロジェクトを作成する。Next.js + TypeScript で開発を行うために、以下のコマンドを実行する。
```bash
npm create next-app --typescript <任意のプロジェクト名>
```

## Storybook のインストール
まず、プロジェクトのディレクトリ内で、以下のコマンドを実行し、Storybook の最新版をインストールする。
```bash
npm install --save-dev @storybook/react@next
```
## Storybook の設定
次に、以下のコマンドを実行し、Storybook の初期設定を行う。
```bash
npx sb@next init
```
このコマンド実行時に、「Do you want to run the 'eslintPlugin' fix on your project?」という質問が表示される場合がある。これは、Storybook の ESLint プラグインを導入するかどうかを問うものである。静的解析を行いたい場合は、"y" を入力し、"eslint-plugin-storybook" をインストールする。

ESLint の設定
その後、.eslintrc.json に以下の設定を追加することで、Storybook に関連する ESLint の設定を行う。
```json
{
  "extends": [
    "plugin:storybook/recommended"
  ]
}
```

package.json の変更
上記手順に従って操作を行うと、package.json に以下のような変更が加わる。
Storybook の関連パッケージが devDependencies に追加される。
"scripts" セクションに、Storybook を起動するための "storybook" コマンドが追加される。

ディレクトリの変化
srcディレクトリの中にstoriesが作成される。rootディレクトリに.storybook/main.tsと.storybook/preview.tsが出来上がる。

## 番外編
 自作したButtonをStorybookで確認
### 構築
Would you likeの部分はyes→yes→no→yes→no→enterの順で設定
```bash
＄ npm create next-app --typescript atomic-demo

Need to install the following packages:
  create-next-app@13.3.4
Ok to proceed? (y) y
✔ Would you like to use TypeScript with this project? … No / Yes
✔ Would you like to use ESLint with this project? … No / Yes
✔ Would you like to use Tailwind CSS with this project? … No / Yes
✔ Would you like to use `src/` directory with this project? … No / Yes
✔ Would you like to use experimental `app/` directory with this project? … No / Yes
? What import alias would you like configured? › @/*
・・・・・・・・・・・

・・・・・・・・・・・
uccess! Created atomic-demo

```

#### 移動と確認
cdで作成したatomic-demoに移動して作業を行う。
```bash
$ cd atomic-demo                                                                          
$ ls
README.md         next.config.js    package-lock.json public            tsconfig.json
next-env.d.ts     node_modules      package.json      src
```

#### ディレクトリを作成
```bash
$ pwd 
  <作成したところまでのpath>/atomic-demo    


$ mkdir src/components
$ mkdir src/components/atoms
$ mkdir src/components/atoms/Button && touch src/components/atoms/Button/Button.tsx && touch src/components/atoms/Button/Button.module.scss && touch src/components/atoms/Button/Button.stories.tsx

#確認
$ ls src/components/atoms
  Button.scss        Button.stories.tsx Button.tsx

# ディレクトリ構成の確認(treeコマンドがある場合)
$ tree -I "node_modules"
.
├── README.md
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   ├── next.svg
│   └── vercel.svg
├── src
│   ├── components
│   │   └── atoms
│   │       └── Button
│   │           ├── Button.module.scss
│   │           ├── Button.stories.tsx
│   │           └── Button.tsx
│   ├── pages
│   │   ├── _app.tsx
│   │   ├── _document.tsx
│   │   ├── api
│   │   │   └── hello.ts
│   │   └── index.tsx
│   └── styles
│       ├── Home.module.css
│       └── globals.css
└── tsconfig.json

```

### moduleのinstall
```bash
$ npm install --save-dev @storybook/react@next sass
```

### storybook初期化
```bash
$ npx sb@next init

Need to install the following packages:
  sb@7.0.8
Ok to proceed? (y) ←　yを入力してenter

```
万が一以下の文が出て.storiesなどのディレクトリができない場合は以下のコマンドを実行
```bash
Detecting project type. ✓

    There seems to be a Storybook already available in this project.
    Apply following command to force:

   sb init [options] -f


attention => Storybook now collects completely anonymous telemetry regarding usage.
This information is used to shape Storybook's roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://storybook.js.org/telemetry
```
実行コマンド
```bash
npx sb@next init -f
```

### Button.tsxを作成
```typescript
import React, {memo} from 'react';
import styles from './Button.module.scss';


export interface ButtonProps {
    size?: 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge';
    color?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
    shape?: 'square' | 'rounded' | 'pill';
    textAlign?: 'left' | 'center' | 'right';
    onClick?: () => void;
    text?: string;
}

// クラス名を生成する関数を追加
const getClassName = (
    size: string,
    color: string,
    shape: string,
    textAlign: string,
) => {
    return [
        styles[`btn-${size}`],
        styles[`btn-${color}`],
        styles[`btn-${shape}`],
        styles[`text-${textAlign}`],
    ].join(' ');
};

const Button = memo((
    {
        size = 'medium',
        color = 'primary',
        shape = 'square',
        textAlign = 'center',
        onClick,
        ...props
    }: ButtonProps) => {

    return (
        <button
            // 生成されたクラス名を適用
            className={getClassName(size, color, shape, textAlign)}
            onClick={onClick}
        >
            {props.text}
        </button>
    );
});

export default Button;
```
eslintのエラーが出たら以下の方法で一旦応急処置
```typescript
// eslint-disable-next-line react/display-name
const Button = memo((
```

### Button.module.scss
```typescript
// サイズ
.btn-small {
  font-size: 12px;
  padding: 4px 8px;
}
.btn-medium {
  font-size: 16px;
  padding: 6px 12px;
}
.btn-large {
  font-size: 20px;
  padding: 8px 16px;
}
.btn-xlarge {
  font-size: 24px;
  padding: 10px 20px;
}
.btn-xxlarge {
  font-size: 28px;
  padding: 12px 24px;
}

// 形状
.btn-square { border-radius: 0; }
.btn-rounded { border-radius: 4px; }
.btn-pill { border-radius: 9999px; }

// カラー
.btn-primary {
  background-color: #007bff;
  color: #fff;
  &:hover { background-color: #0069d9; }
}
.btn-secondary {
  background-color: #6c757d;
  color: #fff;
  &:hover { background-color: #5a6268; }
}
.btn-success {
  background-color: #28a745;
  color: #fff;
  &:hover { background-color: #218838; }
}
.btn-warning {
  background-color: #ffc107;
  color: #212529;
  &:hover { background-color: #e0a800; }
}
.btn-danger {
  background-color: #dc3545;
  color: #fff;
  &:hover { background-color: #c82333; }
}

// レスポンシブデザインのブレークポイント
$breakpoint-mobile: 480px;
$breakpoint-tablet: 768px;

// モバイルデバイス
@media (max-width: $breakpoint-mobile) {
  .btn-small {
    font-size: 10px;
    padding: 3px 6px;
  }
  .btn-medium {
    font-size: 14px;
    padding: 5px 10px;
  }
  .btn-large {
    font-size: 18px;
    padding: 7px 14px;
  }
  .btn-xlarge {
    font-size: 22px;
    padding: 9px 18px;
  }
  .btn-xxlarge {
    font-size: 26px;
    padding: 11px 22px;
  }
}

// タブレットデバイス
@media (min-width: $breakpoint-mobile + 1) and (max-width: $breakpoint-tablet) {
  .btn-small {
    font-size: 11px;
    padding: 3.5px 7px;
  }
  .btn-medium {
    font-size: 15px;
    padding: 5.5px 11px;
  }
  .btn-large {
    font-size: 19px;
    padding: 7.5px 15px;
  }
  .btn-xlarge {
    font-size: 23px;
    padding: 9.5px 19px;
  }
  .btn-xxlarge {
    font-size: 27px;
    padding: 11.5px 23px;
  }
}

// デスクトップデバイス
@media (min-width: $breakpoint-tablet + 1) {}

```

### Button.stories.tsx
コードの説明は後ほど→ [Button.stories.tsx の解説](#Button.stories.tsx-の解説)
```typescript
import type {Meta, StoryObj} from '@storybook/react';
import Button from './Button';


const meta: Meta = {
    title: 'atoms/Button',
    component: Button,
    argTypes: {
        size: {
            options: ['small', 'medium', 'large', 'xlarge', 'xxlarge'],
            control: { type: 'radio' },
            defaultValue: 'medium',
        },
        color: {
            options: ['primary', 'secondary', 'success', 'warning', 'danger'],
            control: { type: 'radio' },
            defaultValue: 'primary',
        },
        shape: {
            options: ['square', 'rounded', 'pill'],
            control: { type: 'radio' },
            defaultValue: 'square',
        },
        textAlign: {
            options: ['left', 'center', 'right'],
            control: { type: 'radio' },
            defaultValue: 'center',
        },
        onClick: {
            action: 'clicked',
            description: 'Button clicked',
        },
        text: {
            control: 'text',
            defaultValue: 'Button',
        },
    },
};

export default meta;
type Button = StoryObj<typeof Button>;
export const Primary : Button = {
    args : {
        text: 'Button',
    }
};

export const Secondary : Button = {
    args : {
        color: 'secondary',
        size: 'xlarge',
        shape: 'rounded',
        textAlign: 'center',
        text: 'Outline',
    }
};

export const Danger : Button = {
    args : {
        color: 'danger',
        size: 'large',
        shape: 'rounded',
        textAlign: 'center',
        text: 'Danger',
    }
};

export const Pill : Button = {
    args : {
        color: 'success',
        size: 'xlarge',
        shape: 'pill',
        textAlign: 'center',
        text: 'Pill Button',
    }
}
```

### .storybook/main.tsにstorybookに表示するためのpathを追記
storybookに表示するためのpathを追記　する。
```typescript
import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
  stories: [
    "../src/**/*.mdx",
    "../src/**/*.stories.@(js|jsx|ts|tsx)",
    "../src/components/**/*.stories.@(js|jsx|ts|tsx)", //追加した場所
  ],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
  ],
  framework: {
    name: "@storybook/nextjs",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
};
export default config;

```

### storybookを立ち上げて確認
```bash
$ pwd
  <作成したところまでのpath>/atomic-demo
  
# Next.js立ち上げ
$ npm run dev 

# StoryBook立ち上げ(別のターミナルで)
$ pwd
  <作成したところまでのpath>/atomic-demo
$ npm run storybook
```

![スクリーンショット 2023-05-04 19.25.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2601888/cc9a8cee-0c97-7d0e-7c99-13302c3d652e.png)

### Button.stories.tsx の解説
```typescript
// Storybook で使用する型をインポート
import type { Meta, StoryObj } from '@storybook/react';
// Button コンポーネントをインポート
import Button from './Button';

// ストーリーに関するメタ情報を定義
const meta: Meta = {
  // ストーリーのタイトルを設定
  title: 'atoms/Button',
  // コンポーネントを指定
  component: Button,
  // 各プロパティの設定方法を定義
  argTypes: {
    // size プロパティの設定
    size: {
      // オプションを配列で指定
      options: ['small', 'medium', 'large', 'xlarge', 'xxlarge'],
      // ラジオボタンで選択できるように設定
      control: { type: 'radio' },
      // デフォルト値を設定
      defaultValue: 'medium',
    },
    // color プロパティの設定
    color: {
      options: ['primary', 'secondary', 'success', 'warning', 'danger'],
      control: { type: 'radio' },
      defaultValue: 'primary',
    },
    // shape プロパティの設定
    shape: {
      options: ['square', 'rounded', 'pill'],
      control: { type: 'radio' },
      defaultValue: 'square',
    },
    // textAlign プロパティの設定
    textAlign: {
      options: ['left', 'center', 'right'],
      control: { type: 'radio' },
      defaultValue: 'center',
    },
    // onClick プロパティの設定
    onClick: {
      // アクションを定義
      action: 'clicked',
      // 説明を記載
      description: 'Button clicked',
    },
    // text プロパティの設定
    text: {
      // テキスト入力で設定できるように設定
      control: 'text',
      // デフォルト値を設定
      defaultValue: 'Button',
    },
  },
};

// メタ情報をエクスポート
export default meta;

// StoryObj 型を使用して Button の型を定義
type Button = StoryObj<typeof Button>;

// Primary ストーリーを定義
export const Primary: Button = {
  // args を使用してプロパティを設定
  args: {
    text: 'Button',
  },
};

// Secondary ストーリーを定義
export const Secondary: Button = {
  args: {
    color: 'secondary',
    size: 'xlarge',
    shape: 'rounded',
    textAlign: 'center',
    text: 'Outline',
  },
};

// Danger ストーリーを定義
export const Danger: Button = {
  args: {
    color: 'danger',
        size: 'large',
    shape: 'rounded',
    textAlign: 'center',
    text: 'Danger',
  },
};

// Pill ストーリーを定義
export const Pill: Button = {
  args: {
    color: 'success',
    size: 'xlarge',
    shape: 'pill',
    textAlign: 'center',
    text: 'Pill Button',
  },
}

```
上記のコードでは、Primary、Secondary、Danger、および Pill という4つの異なるストーリーを定義している。それぞれのストーリーでは、args を使用して Button コンポーネントに渡すプロパティを設定している。これにより、Storybook 上で各ストーリーを表示した際に、指定したプロパティが適用された Button コンポーネントが表示される。

例えば、Danger ストーリーでは、color プロパティに 'danger'、size プロパティに 'large'、shape プロパティに 'rounded'、textAlign プロパティに 'center'、および text プロパティに 'Danger' が設定されている。これにより、Storybook 上で Danger ストーリーを選択すると、上記のプロパティが適用された Button コンポーネントが表示される。

## 最後に
以上で、Next.js (TypeScript) に Storybook を導入する手順を説明した。これにより、コンポーネントの開発が効率的に行えることが期待できるだろう。


## 参考文献

1. title : TypeScriptをベースにしたNext.jsのプロジェクトにStorybookのV7を使ってコンポーネント主導開発をやってみましょう
   url : https://dev-yakuza.posstree.com/react/nextjs/storybook/v7/start/

2. title : フロントエンドエンジニアなら知らないと損！アトミックデザインのすゝめ 
   url : https://www.youtube.com/watch?v=Un0aoW0kNeE

3. title : Atomic Design × Nuxt.js」コンポーネント毎に責務の範囲を明確にしたら幸せになった話 / GREE Tech Conference 2021
   url : https://www.youtube.com/watch?v=DXlKK6aVBPk