【AI駆動開発】v0ならこんなに簡単!UIデザインからフロント実装までが爆速に! #ai-driven-dev #フロント開発 #React

はじめに

別記事で紹介した「AIを利用してUIデザインを効率化するサービス」の中から、最も有望そうなv0を使って、実際にUIデザインと、そこからの実装までを行ってみましょう。

やること

シンプルなタイマーアプリを作成します。v0でUIデザインを行い、それをReactコードとして出力してから、VSCodeを使って簡単な機能実装までを実施します。その後で、デザインの変更にもチャレンジします。

やってみよう

UI設計

まずはアイデア出しからです。細かいことは指示せず、単純に「タイマー」とだけ入力してUIを生成させてみます。

10秒ほどで3つの画面案を生成してくれました。

これらが気に入らない場合は、Regenerateボタンから再生成させることもできます。

今回は案Aを採用することにします。

私が作りたかったのはカウントダウンするタイマーなので「時間を入力して、スタートするとカウントダウンする」と入力して更新させてみます。
10秒程度で新しいUIが生成されました。

指示通り、時間を入力できるようになっています。英語だった部分がなぜか日本語に変わりましたが、このまま進めることにしましょう。
入力欄が1つだと使うのが難しいので「時分秒の入力欄は個別に設ける」という指示を出してみます。

完璧です。
リセット機能は実装しないことにします。今回は当該ボタンを選択して「このボタンを削除」と指示してみます。

完璧すぎて驚きます。デザイナーと一緒に作業しているかのように錯覚してしまいます。
最後に、デザインを調整してみましょう。
スタートボタンの色を少し暗くしてみます。スタートボタンを選択して、プロンプトは「もう少し暗い色」です。

完成です。自然言語による指示だけで、思い通りのUIができあがりました。

Reactプログラム:初期設定

v0で作成したUIはHTMLやReactのコードとして出力できます。

手動でコピーするだけでなく、npxコマンドによってNext.jsのプロジェクトにコンポーネントとして取り込むこともできます。準備のため、まずはNext.jsのアプリを作成しましょう。npx create-next-appでプロジェクトのひな形を作成し、npm run devで動作を確認しておきます。

Reactプログラム:v0からの取り込み

v0の画面に表示されているnpx v0 add crmoZT83HuOを実行します。

初回はnpx v0@latest initを実行するように言われるので、その通りに実行します。終わったら、再度npx v0 add crmoZT83HuOを実行します。
コンポーネント名を尋ねられるので、Timerと指示します。

Timerコンポーネントと、そこから利用されているボタンなどのコンポーネントが取り込まれました。
page.tsxの内容を下記のように変更して、画面を確認してみます。

import { Timer } from '@/components/component/timer'

export default function Home() {
  return (
    <Timer />
  )
}

v0通りに表示されました。

Reactプログラム:機能実装

v0から生成されたコードが、実際にアプリを作成するベースとして十分かどうかを検証するために、タイマー機能の実装を進めてみます。元のコードとの差分は下記の通りです。

 components/component/timer.tsx | 73 ++++++++++++++++++++++++++++++++++++++----
 1 file changed, 67 insertions(+), 6 deletions(-)

diff --git a/components/component/timer.tsx b/components/component/timer.tsx
index 6d760b3..6e6c68c 100644
--- a/components/component/timer.tsx
+++ b/components/component/timer.tsx
@@ -1,11 +1,39 @@
+"use client"
+
 /**
  * This code was generated by v0 by Vercel.
  * @see https://v0.dev/t/crmoZT83HuO
  */
 import { CardHeader, CardContent, Card } from "@/components/ui/card"
 import { Button } from "@/components/ui/button"
+import { useState, useEffect } from "react"
 
 export function Timer() {
+  const [hour, setHour] = useState(0)
+  const [minute, setMinute] = useState(0)
+  const [second, setSecond] = useState(0)
+
+  const [goalTime, setGoalTime] = useState(new Date(0));
+
+  const [pausing, setPausing] = useState(false);
+  const [pauseStartTime, setPauseStartTime] = useState(0);
+
+  const [remainingTime, setRemainingTime] = useState(0)
+  useEffect(() => {
+    if (pausing) {
+      return;
+    }
+    
+    const interval = setInterval(() => {
+      const remainingTime = goalTime.getTime() - Date.now();
+      if (remainingTime >= 0) {
+        setRemainingTime(remainingTime);
+      }
+    }, 10);
+
+    return () => clearInterval(interval);
+  }, [goalTime, pausing]);
+
   return (
     <main key="1" className="flex flex-col items-center justify-center h-screen bg-gray-50">
       <Card className="max-w-sm w-full space-y-4">
@@ -18,28 +46,61 @@ export function Timer() {
               aria-label="時間を入力"
               className="w-20 px-3 py-2 border border-gray-300 rounded-md text-center"
               placeholder="時間"
-              type="text"
+              type="number"
+              value={hour}
+              onChange={(e) => setHour(parseInt(e.target.value))}
             />
             <input
               aria-label="分を入力"
               className="w-20 px-3 py-2 border border-gray-300 rounded-md text-center"
               placeholder="分"
-              type="text"
+              type="number"
+              value={minute}
+              onChange={(e) => setMinute(parseInt(e.target.value))}
             />
             <input
               aria-label="秒を入力"
               className="w-20 px-3 py-2 border border-gray-300 rounded-md text-center"
               placeholder="秒"
-              type="text"
+              type="number"
+              value={second}
+              onChange={(e) => setSecond(parseInt(e.target.value))}
             />
           </div>
-          <h3 className="text-4xl font-bold mb-4">00:00:00</h3>
+          <h3 className="text-4xl font-bold mb-4">{formatRemaining(remainingTime)}</h3>
           <div className="flex space-x-4">
-            <Button className="bg-blue-700 text-white w-24">スタート</Button>
-            <Button className="bg-yellow-500 text-white w-24">一時停止</Button>
+            <Button className="bg-blue-700 text-white w-24"
+              onClick={() => {
+                setGoalTime(new Date(Date.now() + hour * 60 * 60 * 1000 + minute * 60 * 1000 + second * 1000));
+                setPausing(false);
+              }}
+            >スタート</Button>
+            <Button className="bg-yellow-500 text-white w-24"
+              onClick={() => {
+                if (pausing) {
+                  const newGoalTime = new Date(goalTime.getTime() + Date.now() - pauseStartTime);
+                  setGoalTime(newGoalTime);
+                }
+                setPausing(pausing => !pausing);
+                setPauseStartTime(Date.now());
+              }}
+            >一時停止</Button>
           </div>
         </CardContent>
       </Card>
     </main>
   )
 }
+
+function formatRemaining(remainingTime: number): string {
+  const hours = Math.floor(remainingTime / 3600000);
+  remainingTime %= 3600000;
+  const minutes = Math.floor(remainingTime / 60000);
+  remainingTime %= 60000;
+  const seconds = Math.floor(remainingTime / 1000);
+
+  const hoursString = hours.toFixed(0).toString().padStart(2, '0');
+  const minutesString = minutes.toFixed(0).toString().padStart(2, '0');
+  const secondsString = seconds.toFixed(0).toString().padStart(2, '0');
+  return `${hoursString}:${minutesString}:${secondsString}`;
+}

特に難しいや変なことはしておらず、素直に実装できました。動作も問題ありません。

v0が生成したコードはアプリ開発のベースとしても十分そうです。

デザイン変更

今度は、後からデザインを変更したくなった場合を想定してみます。タイトルを中央寄せにしてみましょう。v0の画面に戻り「タイトルを中央寄せに」という指示を与えてみました。

ここまでは問題なさそうです。これを実装側に取り込んでいくには、どうすればよいでしょうか?先ほどのようにnpx v0 addを再試行すると、全て上書きされ、実装済みの機能が破棄されてしまいます。これを回避するために、先ほど取り込まれた時点のコードと新たにv0上に表示されているコードとで差分を確認して、手動で適用していくことにしましょう。差分は下記の通りでした。

どうやらCardHeaderitems-centerクラスを適用すればよさそうなので、そうしてみます。

動作を確認してみましょう。

成功です!

まとめ

UIデザインをアイデア出しから進める場合、v0があれば、専門知識を持ったデザイナーがいなくてもサクサクと画面を作成してくれます。作成された画面は、変更を指示するとその通りに更新されます。v0の画面上でコードを編集して反映させることもできますが、今回はその機能を使わなくても自然言語による指示だけで十分なほど精度が高く、動作も高速でした。
UIが完成したら実装です。VSCodeへの取り込みはコマンド一発です。単純なUIではありますが、生成されたコードも特に問題ないものでした。これをベースに機能を実装することができました。
後からデザインを変更する場合は、差分だけを機械的に取り込むことができないため、手動でのマージ作業が必要になってしまいました。vendor branchのような手法を使えば、ある程度は機械的にマージできるかもしれません。

製品レベルの手前にある提案やPoCの段階などで、ある程度使えるUIを「デザイナー抜きで」作れてしまう可能性を感じました。ステークホルダー間での認識合わせ時に、手書き感覚で「もう少しこれをこうして、これを追加」などの際にも、非常に強力な助けになってくれるでしょう。

勉強会のお知らせ

「AI駆動開発(AI-Driven Development) 勉強会(第1回)」を2024年2月2日(金)に開催予定です。今後の生成AI・LLMを利用した開発スタイルについて共有・議論したいと思いますので、本記事の内容に興味を持っていただける方はぜひご参加いただければと思います!!

URL:https://connpass.com/event/306406/

Author

教育系エンジニア。開発やR&Dで最前線の技術を実践しつつ、後進の育成にも魂を燃やす。排出者は数千名。無類の子ども好きで、平日夕方は仕事を中抜けして近所の子どもと遊ぶ毎日。小学校でのプログラミング授業なども経験あり。

上村国慶の記事一覧

新規CTA