もう手書きは不要！？React開発にStorybookとCopilotを導入して、爆速でUIを編集・確認する方法

# はじめに
最近、インフラやバックエンドの開発が落ち着き、フロントエンドを触る機会が増えました。元々インフラ中心で開発をしていたためか、個人的にフロントエンドには苦手意識があり、特にデザイナーさんと連携しながらUIのイメージをすり合わせる点に難しさを感じていました。

具体的には先日チームで開発中のサイトのトップバーの色を変更する際、Figmaのデザインで見た色と、実際にコードで実装した際の実物の色が微妙に違うという問題が発生しました。この時チームメンバーは **「実物のコードをいじってはWeb画面のスクリーンショットを撮り、デザイナーに共有する」** という作業を何度も繰り返しており、もっと効率的な方法はないものかと感じていました。

実はこの課題に対してStorybookが使えそうということは以前から知っていたのですが、フロントエンド知識の不足や、導入後の保守コストへの懸念から、なんとなく導入を遠ざけてしまっていました。

それから最近、GitHub Copilotが使用できるようになり、「これならStorybookを導入できるのではないか？」と試してみたところ、期待以上に開発者体験が向上しました。

本記事では、こうした背景から私がたどり着いた、StorybookとGitHub Copilotを組み合わせた効率的な開発フローを提案します。具体的には、デザイナーがコードを書くことなくReactコンポーネントの見た目をGUIで直接編集・確認できる環境を構築する方法を解説します。これにより、コンポーネントの意図する表現を迅速に共有し、デザインと実装間の手戻りを大幅に削減することを目指します。



# 前提
React、Storybook、Github Copilotを使用します。AIエージェントの部分は適宜別のツールを使用することもできます。

# セットアップ
## Reactのインストール
説明用のReactプロジェクトを作成します。既に今回の記事の内容を適用したいReactプロジェクトがある場合は、ブランチを切ったりして試してみてください。

```bash
npm create vite@latest
```
プロジェクトの設定を入力します。以下自分の設定例です。
Project name: react-storybook-demo
Select a framework: React
Select a variant: TypeScript + SWC 

```bash
npm install
npm run dev
```
http://localhost:5173/ にアクセスして、画面が出れば完了です。

## サンプルアプリケーションの構築
プログラムを編集して今回使うサンプルアプリケーションを構築します。


srcの中に新しくcomponentsフォルダを作成し、フォルダ内にファイルを2つ作ります。

```src/components/EchoComponent.tsx
import { useState } from "react";
import "./EchoComponent.css";

const EchoComponent = () => {
  const [inputText, setInputText] = useState("");
  const [responseText, setResponseText] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsLoading(true);
    setError(null);

    try {
      // HTTPBin の echo API を使用
      const response = await fetch("https://httpbin.org/post", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ message: inputText }),
      });

      if (!response.ok) {
        throw new Error(`APIエラー: ${response.status}`);
      }

      const data = await response.json();
      setResponseText(JSON.parse(data.data).message);
    } catch (err) {
      setError(
        err instanceof Error ? err.message : "不明なエラーが発生しました"
      );
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="echo-container">
      <h2>Echo API デモ</h2>
      <form onSubmit={handleSubmit}>
        <div className="input-group">
          <label htmlFor="echo-input">メッセージを入力してください:</label>
          <input
            id="echo-input"
            type="text"
            value={inputText}
            onChange={(e) => setInputText(e.target.value)}
            placeholder="こんにちは、世界！"
          />
        </div>
        <button type="submit" disabled={isLoading || inputText.trim() === ""}>
          {isLoading ? "送信中..." : "送信"}
        </button>
      </form>

      {error && <div className="error-message">{error}</div>}

      {responseText && (
        <div className="response-container">
          <h3>応答:</h3>
          <p className="response-text">{responseText}</p>
        </div>
      )}
    </div>
  );
};

export default EchoComponent;
```

```src/components/EchoComponent.css
.echo-container {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
  border-radius: 8px;
  background-color: #f9f9f9;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.input-group {
  margin-bottom: 15px;
}

.input-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: 500;
}

.input-group input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

button {
  background-color: #646cff;
  color: white;
  border: none;
  padding: 10px 16px;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 500;
  transition: background-color 0.3s;
}

button:hover {
  background-color: #535bf2;
}

button:disabled {
  background-color: #a5a6f6;
  cursor: not-allowed;
}

.error-message {
  margin-top: 15px;
  padding: 10px;
  background-color: #ffebee;
  color: #d32f2f;
  border-radius: 4px;
  border-left: 4px solid #d32f2f;
}

.response-container {
  margin-top: 20px;
  padding: 15px;
  background-color: #e8f5e9;
  border-radius: 4px;
  border-left: 4px solid #4caf50;
}

.response-text {
  font-size: 16px;
  word-break: break-word;
}

h2,
h3 {
  color: #333;
}

h3 {
  margin-bottom: 8px;
}
```

次にApp.tsxを編集して今作成したコンポーネントを呼び出します。すでに書かれているものを全て消して以下のコードに変更してください。

```src/App.tsx
import EchoComponent from "./components/EchoComponent";

function App() {
  return (
    <div className="container">
      <h1>Echo APIデモアプリケーション</h1>
      <EchoComponent />
    </div>
  );
}

export default App;
```
`npm run dev`をした状態でhttp://localhost:5173/ にアクセスして、以下ような画面が出れば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2365485/5ec527e2-7980-4854-8a9c-13ca96c7a198.png)



## Storybookのインストール
次にStorybookのインストールを行います。
```bash
npx storybook init
```
自分の場合はこの時`Chromatic: Unhandled promise rejection: {"sanitizedErr":{}}`というエラーが出てしまったのですが、 http://localhost:6006/ にアクセスしたらStorybookが表示されたので、一旦このまま進めることにしました。

以下のような画面が見られれば完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2365485/f5290013-3786-4b2e-8c09-9c444f7bdb4e.png)


# Storyの生成
ここから本題です。

## 1. プログラムの分割
早速Storyを生成したいところですが、まずはプログラムをファイル分割してStory（作りやすい構成にします。


Agentが使用できない環境の方は外部のAIに直接コードを貼り付けてからプロンプトを実行してください。
Agentを使用する場合は以下のプロンプトを実行入力してください。別のファイルを使用する場合は`EchoComponent.tsx`の部分を適宜書き換えてください。
```text
EchoComponent.tsxをコンテナ・プレゼンテーションパターンにしてください
```
すると処理を行う関数群が入ったプログラム(EchoContainer.tsxなど)と引数を受け取って描画を行うプログラム(EchoComponent.tsxなど)の2つに分かれると思います。
App.tsxからの呼び出しでエラーが発生していたら１行目の記述を以下のように変更してください。
```App.tsx
import EchoContainer from "./components/EchoContainer";
```

## 2. storyの作成
以下のプロンプトを入力してください。別のファイルを使用する場合は`EchoComponent.tsx`の部分を適宜書き換えてください。
```text
EchoComponent.tsxのstoryだけをstoriesに作成してください。argTypesも定義してください。
```
これでEchoComponent用のstoryが作成されるはずです。

`npm run storybook`が実行されている状態で以下の画面が確認できれば完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2365485/f8b4a1ac-cd90-483d-9cc5-5ce3173e2fa3.png)


## 3. storyで操作できる部分を増やす。
変更したい箇所が足りない場合には以下のプロンプトを追加してみてください。EchoComponent.stories.tsxと背景色の部分は適宜書き換えてください。
```text
EchoComponent.stories.tsxで背景色も操作できるようにプレゼンテーションコンポーネントの引数を変更してください。現在の設定は固定値としてコンテナコンポーネントから渡してください。
```
ページに移動して、以下のように操作できる場所が増えていれば完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2365485/97f3bbbd-d02e-4ab8-90da-0901ffaecff8.png)

この例だとbackgroundColorを編集するだけで簡単にコンポーネントの背景色を変えられるようになりました。カラーピッカーを操作したり、直接カラーコードを入力したりすることで、リアルタイムでコンポーネントの背景色が変わることを確認できます。デザイナーさんはこのGUIを操作することで、プロパティの調整が容易になります。実際私たちのチームでもデザイナーさんにとても便利だとフィードバックをもらえてます！

# おまけ
Storybookの起動を助けるシェルスクリプトを作ったのでよければ使ってみてください。
```bash
#!/bin/zsh

# エラーを検知して終了するようにする
set -e

# エラーハンドリング
trap 'echo "${RED}エラーが発生しました。スクリプトを終了します。${NC}"; exit 1' ERR

# 色の定義
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color

# ヘッダー表示
echo "${BLUE}=================================================${NC}"
echo "${BLUE}      Storybook ヘルパースクリプト      ${NC}"
echo "${BLUE}=================================================${NC}"
echo ""

# 現在のディレクトリを保存
ORIGINAL_DIR=$(pwd)

# プロジェクトディレクトリに移動
cd $(dirname "$0")

# 関数定義: コマンド実行用
execute_command() {
  echo "${YELLOW}実行中: $1${NC}"
  eval $1
  if [ $? -eq 0 ]; then
    echo "${GREEN}✓ 成功しました！${NC}"
  else
    echo "${RED}✗ エラーが発生しました${NC}"
    return 1
  fi
  echo ""
}

# メニュー表示関数
show_menu() {
  echo "次の操作を選択してください:"
  echo "1) 最新のコードを取得する (git pull)"
  echo "2) 必要なパッケージをインストールする (npm install)"
  echo "3) Storybookを起動する (npm run storybook)"
  echo "4) 開発サーバーを起動する (npm run dev)"
  echo "5) プロジェクトをビルドする (npm run build)"
  echo "6) 全ての更新を行い、Storybookを起動する (1+2+3)"
  echo "q) 終了"
  echo ""
}

# メイン処理
while true; do
  show_menu
  echo -n "${YELLOW}選択してください (1-6, q): ${NC}"
  read choice
  echo ""

  case $choice in
    1)
      echo "${BLUE}最新のコードを取得しています...${NC}"
      execute_command "git pull"
      ;;
    2)
      echo "${BLUE}必要なパッケージをインストールしています...${NC}"
      execute_command "npm install"
      ;;
    3)
      echo "${BLUE}Storybookを起動しています...${NC}"
      echo "${YELLOW}ブラウザで http://localhost:6006 を開いてください${NC}"
      echo "${YELLOW}終了するには Ctrl+C を押してください${NC}"
      execute_command "npm run storybook"
      ;;
    4)
      echo "${BLUE}開発サーバーを起動しています...${NC}"
      echo "${YELLOW}ブラウザで http://localhost:5173 を開いてください${NC}"
      echo "${YELLOW}終了するには Ctrl+C を押してください${NC}"
      execute_command "npm run dev"
      ;;
    5)
      echo "${BLUE}プロジェクトをビルドしています...${NC}"
      execute_command "npm run build"
      ;;
    6)
      echo "${BLUE}全ての更新を行い、Storybookを起動します...${NC}"
      echo "${BLUE}Step 1: 最新のコードを取得しています...${NC}"
      execute_command "git pull"
      
      echo "${BLUE}Step 2: 必要なパッケージをインストールしています...${NC}"
      execute_command "npm install"
      
      echo "${BLUE}Step 3: Storybookを起動しています...${NC}"
      echo "${YELLOW}ブラウザで http://localhost:6006 を開いてください${NC}"
      echo "${YELLOW}終了するには Ctrl+C を押してください${NC}"
      execute_command "npm run storybook"
      ;;
    q|Q)
      echo "${BLUE}スクリプトを終了します${NC}"
      # 元のディレクトリに戻る
      cd "$ORIGINAL_DIR"
      exit 0
      ;;
    *)
      echo "${RED}無効な選択です。1-6、またはqを入力してください。${NC}"
      ;;
  esac
done
```

# おわりに
以上、Storybookによってデザイナーさんでも直接プロパティを変更して仕上がりを見ることができるようになりました。もっとStorybookを活用していきたい場合にも、これで基本構成は出来上がっているので、ここからならかなり簡単にカスタマイズしていけると思います。
本記事で紹介した手順を参考に、ぜひプロジェクトにStorybookとAIアシスタントを導入し、フロントエンド開発の効率化と品質向上に役立ててみてください。