v0によるUI作成の概要と流れ

# はじめに
手っ取り早くTODOアプリのUIを作る必要があったので、AIにコードを書いてもらうことにしました。調べてみるとVercel社のv0というAIツールがUIに特化しているという情報があったので試してみました。
 
# v0の概要
## v0とは
Next.jsの開発元であるVercel社が提供するWebデザインとコード生成に特化したAIツールです。以下の画面からプロンプトを実行することでコードを生成することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560123/442937f0-f92b-72db-d1a3-b1063cecbdf6.png)
https://v0.dev/chat

:::note warn
PublicBetaなので、まだ開発段階のツールとなります。この後、機能など変更される可能性があります。
:::

## どんなコードが生成されるか
- 生成されるコードは、Reactで出力されます。
  - https://ja.react.dev/
- UIコンポーネントは、shadcn/uiが使われます。
  - https://ui.shadcn.com/
- リンク、画像、フォントについては、Next.js Link、Next.js Image、およびNext.js Fontが使われます。

## 生成したコードをどう活用するか
- 生成したコードをコピーして、自身のプロジェクトで利用できます。
- または生成したコードをベースにCLIでNext.jsのプロジェクトを作ることができます。

## 「Chat」と「Legacy」
v0には、新しくリリースされた「Chat」と１つ前の「Legacy」が存在します。今回は「Chat」を使っていきます。
- Chat：https://v0.dev/chat
- Legacy：https://v0.dev/?from=chat

当記事内でv0という単語は度々登場しますが、「Chat」の方を指します。

## 料金

以下の3プランが用意されています。今回はFreeプランを使っていきます。
(FreeプランでChatをどのくらい使えるのか、ドキュメントからは読み取れず、あまり分かってないです...)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560123/bb4aa22b-dafd-6c51-a94d-64754a9c1709.png)

https://v0.dev/pricing?from=chat

## 公式ドキュメント
以下が公式のドキュメントになりますので、詳細はそちらをご覧ください。
https://v0.dev/docs

## そのほか
<details><summary>クレジットの使用状況の確認方法</summary>

以下からクレジットの使用状況を確認できます。
Chatは不要と思いますが、Legacyの方を使用する場合に重要になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560123/bdf227a3-16fb-ae10-010c-ffe072a24d57.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560123/5b02578e-b448-f392-fc4a-413894105aab.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560123/6ff51adb-0079-1e34-f44f-05325129c8b1.png)
</details>

<details><summary>日本語化の設定方法</summary>

以下から言語設定を英語から日本語に変更できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560123/bdf227a3-16fb-ae10-010c-ffe072a24d57.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560123/62a5f670-13bc-a40e-be69-03704774c3a4.png)

</details>

# 実際にTODOアプリ作ってみる
ここからは実際にv0にTODOアプリを作ってもらいます。

## 流すプロンプト
以下のプロンプトを流します。

```
ReactでTODOアプリを作りたいのでソースコードを書いてください。
要件は以下になります。
　・開発言語はTypeScript
　・必要な画面は１枚
　・TODOの作成 / TODOの削除 / TODOの更新 / TODOの参照が出来る画面が必要
　・バックエンドとの通信はRestAPIで行う
　・TODOのデータ自体は、RestAPIで取得する
```

## プロンプトの実行結果
実行結果は以下になります。期待通りTodoアプリのコードを生成してくれました。

<details><summary>v0の回答</summary>

はい、TypeScriptを使用したReactでTODOアプリを作成しましょう。以下の要件に基づいて、1つのコンポーネントにTODOの作成、削除、更新、参照の機能を実装します。また、バックエンドとの通信はREST APIを使用します。

それでは、TypeScriptを使用したReactコンポーネントを作成します。

```typescript
import React, { useState, useEffect } from 'react';
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Checkbox } from "@/components/ui/checkbox"

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

const API_BASE_URL = 'https://api.example.com/todos'; // APIのベースURLを適切に設定してください

export default function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [newTodo, setNewTodo] = useState('');
  const [editingTodo, setEditingTodo] = useState<Todo | null>(null);

  useEffect(() => {
    fetchTodos();
  }, []);

  const fetchTodos = async () => {
    try {
      const response = await fetch(API_BASE_URL);
      const data = await response.json();
      setTodos(data);
    } catch (error) {
      console.error('Error fetching todos:', error);
    }
  };

  const addTodo = async () => {
    if (!newTodo.trim()) return;
    try {
      const response = await fetch(API_BASE_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title: newTodo, completed: false }),
      });
      const data = await response.json();
      setTodos([...todos, data]);
      setNewTodo('');
    } catch (error) {
      console.error('Error adding todo:', error);
    }
  };

  const updateTodo = async (todo: Todo) => {
    try {
      const response = await fetch(`${API_BASE_URL}/${todo.id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(todo),
      });
      const updatedTodo = await response.json();
      setTodos(todos.map(t => t.id === updatedTodo.id ? updatedTodo : t));
      setEditingTodo(null);
    } catch (error) {
      console.error('Error updating todo:', error);
    }
  };

  const deleteTodo = async (id: number) => {
    try {
      await fetch(`${API_BASE_URL}/${id}`, { method: 'DELETE' });
      setTodos(todos.filter(todo => todo.id !== id));
    } catch (error) {
      console.error('Error deleting todo:', error);
    }
  };

  const toggleComplete = (todo: Todo) => {
    updateTodo({ ...todo, completed: !todo.completed });
  };

  return (
    <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-xl">
      <h1 className="text-2xl font-bold mb-4">TODO App</h1>
      <div className="flex mb-4">
        <Input
          type="text"
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="新しいTODOを入力"
          className="flex-grow mr-2"
        />
        <Button onClick={addTodo}>追加</Button>
      </div>
      <ul className="space-y-2">
        {todos.map(todo => (
          <li key={todo.id} className="flex items-center justify-between bg-gray-100 p-2 rounded">
            {editingTodo?.id === todo.id ? (
              <Input
                type="text"
                value={editingTodo.title}
                onChange={(e) => setEditingTodo({ ...editingTodo, title: e.target.value })}
                className="flex-grow mr-2"
              />
            ) : (
              <div className="flex items-center">
                <Checkbox
                  checked={todo.completed}
                  onCheckedChange={() => toggleComplete(todo)}
                  className="mr-2"
                />
                <span className={todo.completed ? 'line-through' : ''}>{todo.title}</span>
              </div>
            )}
            <div>
              {editingTodo?.id === todo.id ? (
                <Button onClick={() => updateTodo(editingTodo)} className="mr-2">保存</Button>
              ) : (
                <Button onClick={() => setEditingTodo(todo)} className="mr-2">編集</Button>
              )}
              <Button onClick={() => deleteTodo(todo.id)} variant="destructive">削除</Button>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}

```

このコンポーネントは以下の機能を提供します：

1. TODOの作成: 新しいTODOをテキスト入力し、「追加」ボタンをクリックして作成します。
2. TODOの削除: 各TODOの「削除」ボタンをクリックして削除します。
3. TODOの更新: 「編集」ボタンをクリックしてTODOを編集モードにし、変更後に「保存」ボタンをクリックして更新します。
4. TODOの参照: コンポーネントがマウントされた時点で、すべてのTODOをAPIから取得して表示します。
5. TODOの完了状態の切り替え: チェックボックスをクリックしてTODOの完了状態を切り替えます。

注意点：

- `API_BASE_URL`を実際のAPIエンドポイントに置き換える必要があります。
- エラーハンドリングは基本的なものしか実装していないので、実際の使用時にはより堅牢なエラーハンドリングを追加することをお勧めします。
- このコンポーネントは基本的な機能を提供していますが、実際のアプリケーションではさらにスタイリングやエラー表示、ローディング状態の表示などを追加することで、ユーザー体験を向上させることができます。

このコードをベースに、必要に応じて機能を追加したり、スタイリングをカスタマイズしたりすることができます。

</details>

## ローカルにNext.js環境を作成
次にローカルで画面を立ち上げて、TODOアプリを確認したいので環境を作ります。

v0では、CLIから生成したコードをベースとしたNext.jsのプロジェクトを立ち上げる機能を提供しています。

### 画面右上から「Add to Codebase」を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560123/587c4c98-d02d-9052-b4eb-5f8bb455a8b0.png)

### CLIコマンドが表示されるのでコピーします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560123/bcdf0751-4aca-609b-97a7-095685f13879.png)

### CLIコマンドを実行して、プロジェクトを作成します。
```
npx shadcn@latest add "https://v0.dev/chat/b/b_vowbOz3fa72?token=abcde12345"
```

## 結果の確認
Next.jsでローカル実行すると、先ほどのTODOアプリが表示されます。

以下は、左の画像がTODOアプリのTODOが空の状態で、右の画像が試しにTODOを追加して表示した状態となります。

|初期表示|TODOを表示|
|---|---|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560123/4f737bfa-6133-171d-c6aa-6efdd6b0253a.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560123/9be77614-f88c-b9af-0c1a-bd401f1074a1.png)|

これだけで簡単にUIを作れちゃいました。

<details><summary>番外編：Claude 3.5 SonetでTODOアプリを生成</summary>

ここでふと他の生成AIでも別によくね？と思いまして、Claude 3.5 Sonetで同じプロンプトを試しました。結果としては、やはりv0の方がリッチな画面を作ってくれます。

### 実行結果
生成AI(Claude 3.5 Sonet)に同じプロンプトを流すと、以下のようになります。

|初期表示|TODOを表示|
|---|---|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560123/fc574cf3-5ac7-9986-523a-2ad12d7bd65c.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560123/a2e8a8c2-669f-bf90-d0bd-e6feae172893.png)|

### AIの回答
はい、TypeScriptを使用したReactのTODOアプリケーションのソースコードを作成します。以下の要件に基づいて、1つの画面でTODOの作成、削除、更新、参照ができるコンポーネントを作成します。また、バックエンドとの通信はREST APIを使用して行います。

```typescript
import React, { useState, useEffect } from 'react';
import axios from 'axios';

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

const TodoApp: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [newTodo, setNewTodo] = useState('');
  const [editingTodo, setEditingTodo] = useState<Todo | null>(null);

  useEffect(() => {
    fetchTodos();
  }, []);

  const fetchTodos = async () => {
    try {
      const response = await axios.get<Todo[]>('https://api.example.com/todos');
      setTodos(response.data);
    } catch (error) {
      console.error('Failed to fetch todos:', error);
    }
  };

  const addTodo = async () => {
    if (!newTodo.trim()) return;
    try {
      const response = await axios.post<Todo>('https://api.example.com/todos', {
        title: newTodo,
        completed: false,
      });
      setTodos([...todos, response.data]);
      setNewTodo('');
    } catch (error) {
      console.error('Failed to add todo:', error);
    }
  };

  const deleteTodo = async (id: number) => {
    try {
      await axios.delete(`https://api.example.com/todos/${id}`);
      setTodos(todos.filter(todo => todo.id !== id));
    } catch (error) {
      console.error('Failed to delete todo:', error);
    }
  };

  const updateTodo = async (id: number, title: string, completed: boolean) => {
    try {
      const response = await axios.put<Todo>(`https://api.example.com/todos/${id}`, {
        title,
        completed,
      });
      setTodos(todos.map(todo => todo.id === id ? response.data : todo));
      setEditingTodo(null);
    } catch (error) {
      console.error('Failed to update todo:', error);
    }
  };

  const startEditing = (todo: Todo) => {
    setEditingTodo(todo);
  };

  return (
    <div>
      <h1>TODO App</h1>
      <input
        type="text"
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
        placeholder="New todo"
      />
      <button onClick={addTodo}>Add Todo</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            {editingTodo && editingTodo.id === todo.id ? (
              <>
                <input
                  type="text"
                  value={editingTodo.title}
                  onChange={(e) => setEditingTodo({ ...editingTodo, title: e.target.value })}
                />
                <button onClick={() => updateTodo(todo.id, editingTodo.title, todo.completed)}>
                  Save
                </button>
              </>
            ) : (
              <>
                <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
                  {todo.title}
                </span>
                <input
                  type="checkbox"
                  checked={todo.completed}
                  onChange={() => updateTodo(todo.id, todo.title, !todo.completed)}
                />
                <button onClick={() => startEditing(todo)}>Edit</button>
                <button onClick={() => deleteTodo(todo.id)}>Delete</button>
              </>
            )}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default TodoApp;
```
</details>

## APIの作成
先ほどv0に生成してもらったコードは、API呼んでサーバからTODOを取得したり、編集したりするロジック周りも含まれています。

とすると、APIも欲しくなってきます。

ということでv0にAPIも作ってもらいます。今回は、OpenAPIで定義してもらうこ都にしました。

:::note info
OpenAPIは、APIを定義するための共通的なインターフェース記述言語の仕様です。
https://ja.wikipedia.org/wiki/OpenAPI_Specification
:::

### 流すプロンプト
```
上記のTODOアプリに使用するAPIについて、opneapi.ymlとして出力してもらえますか？
```

### 実行結果

<details><summary>v0の回答</summary>

はい、もちろんです。TODOアプリ用のREST APIを定義するOpenAPI仕様（YAML形式）を作成します。この仕様は、先ほど実装したTODOアプリの機能に合わせて設計されています。

以下が、TODOアプリ用のOpenAPI仕様（YAML形式）です：

```yaml
openapi: 3.0.0
info:
  title: TODO API
  version: 1.0.0
  description: A simple API for managing TODOs

servers:
  - url: https://api.example.com/v1

paths:
  /todos:
    get:
      summary: List all TODOs
      responses:
        '200':
          description: Successful response
          content:
            application/json:    
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Todo'
    post:
      summary: Create a new TODO
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TodoCreate'
      responses:
        '201':
          description: Created
          content:
            application/json:    
              schema:
                $ref: '#/components/schemas/Todo'
        '400':
          $ref: '#/components/responses/BadRequest'

  /todos/{todoId}:
    get:
      summary: Get a specific TODO
      parameters:
        - name: todoId
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Successful response
          content:
            application/json:    
              schema:
                $ref: '#/components/schemas/Todo'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update a TODO
      parameters:
        - name: todoId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TodoUpdate'
      responses:
        '200':
          description: Successful response
          content:
            application/json:    
              schema:
                $ref: '#/components/schemas/Todo'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete a TODO
      parameters:
        - name: todoId
          in: path
          required: true
          schema:
            type: integer
      responses:
        '204':
          description: Successful response
        '404':
          $ref: '#/components/responses/NotFound'

components:
  schemas:
    Todo:
      type: object
      properties:
        id:
          type: integer
        title:
          type: string
        completed:
          type: boolean
      required:
        - id
        - title
        - completed
    TodoCreate:
      type: object
      properties:
        title:
          type: string
        completed:
          type: boolean
          default: false
      required:
        - title
    TodoUpdate:
      type: object
      properties:
        title:
          type: string
        completed:
          type: boolean
      minProperties: 1
  responses:
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Error:
      type: object
      properties:
        message:
          type: string
      required:
        - message
```

この OpenAPI 仕様は以下の機能を定義しています：

1. TODOの一覧取得 (GET /todos)
2. 新しいTODOの作成 (POST /todos)
3. 特定のTODOの取得 (GET /todos/todoId)
4. TODOの更新 (PUT /todos/todoId)
5. TODOの削除 (DELETE /todos/todoId)


また、以下のスキーマも定義しています：

- `Todo`: 完全なTODOオブジェクト（id、title、completed を含む）
- `TodoCreate`: 新しいTODOを作成する際に使用（title は必須、completed はオプション）
- `TodoUpdate`: TODOを更新する際に使用（title と completed の少なくとも1つが必要）
- `Error`: エラーレスポンス用のスキーマ


この OpenAPI 仕様は、先ほど作成した React コンポーネントと互換性があり、必要な全ての操作をカバーしています。API の実装者はこの仕様に基づいてバックエンドを構築し、フロントエンド開発者（React アプリケーションの開発者）はこの仕様を参照して API を利用することができます。

必要に応じて、認証やページネーションなどの機能を追加することで、より本格的な API 仕様にすることができます。

</details>

# 最後に
ここまでv0の概要と使用する際の流れを説明しました。

UIはもちろんのことAPIなんかも作れちゃうので、v0があればアプリ開発が捗りそうです。

ただ今のところUIコンポーネントはshadcn/uiやNext.jsに依存するので、あとはプロジェクトとの相性次第と思います。PoCなどはv0で良さそうですね。

まだPublicBetaということで、今後はMUIなど様々なUIコンポーネントに対応してもらえると嬉しいなと思いました。

以上です。ありがとうございました。