v0の実力検証!AI時代のフロントエンド開発はどこまで進化したか

v0の実力検証!AI時代のフロントエンド開発はどこまで進化したか

こんにちは、Anycloudのやましたです!

最近、業務でv0を使ってみたので、個人的な感想やおすすめの使い方を紹介してみます。

結論から言うと、部分的にでも業務で導入できそうなもので、特にビューのみの実装であれば実装スピードはかなり早くなるかなと思います。

v0って何?

v0は、Vercelが開発したAIを活用した生成型ユーザーインターフェースシステムです。

簡単に言うと、指示内容をもとにReactコードを生成してくれるツールですね。特にフロントエンドの実装時に活躍するツールです。

v0の役割は、たたき台を作ってくれる優秀なエンジニアさんみたいなものです。使う側は出てきたアウトプットを確認して、そこから修正を加えていくという使い方が主流になりそうと思います。

おすすめの使い方

v0を使っている中で使い方にはいくつかコツがありそうなので、ここでは、僕が試してみて分かった、おすすめの使い方を紹介します。

1. 画像ベースでの指示がおすすめ

テキストよりも正確に伝わりやすい。僕は画像とテキストを組み合わせて細かく指示を書くことが多いです。

Figmaでデザインされたものを⌘+Shift+Cでコピーしv0に貼り付けるが僕の主なワークフローです

2. 画像の一部分だけを指定も可能

全体じゃなくても、画像の一部分だけを指定して実装してもらうこともできます。

3. API通信が絡む場合の指示出し

モックデータを作成して、それを使った実装を指示すると良いと思います。そうすると、実際のデータ取得処理に置き換えやすくなります。

このとき、モジュールの型情報も一緒に教えるとより精度が高くなります。

4. スタイリングに注意

デフォルトではshadcn/uiベースの出力になるので、使わない場合は別途指示が必要です。ただし、スタイルの綺麗さは少し落ちるかもしれません。

5. v0がやるべきことは1つにさせる

これは生成AIのプロンプトテクニックでもありますが、1度の指示で行うことのスコープが広すぎると精度は落ち、その逆では精度が上がる傾向にあります。

例えば、「10ページ同時につくって」などはうまくいかないので注意です。


上記からわかるように、v0に与える情報がより実装に必要な具体的なものであればあるほど、生成されるコードの精度が向上する傾向にあります。

これは人間のエンジニアと働く場合と同じです。例えば、新しく開発チームに加わったエンジニアに、テキストデータだけを渡して「よしなに開発してね!」と言っても、当然良いアウトプットは期待できません。

開発を始めるためには、作成する画面のデザイン、具体的な仕様、含まれる処理などの情報が必要です。これらの情報があって初めて、効果的な開発がスタートできます。

なので、v0に対しても同様のアプローチが重要です。質の高い開発を実現するためには、v0に対しても十分な情報を提供することが不可欠です。

実際に使ってみた!

では、実際にv0を使ってみた例をいくつか紹介しますね。

1. 簡単なログインフォームを実装

まずは、シンプルなログインフォームを実装してみました。

result-1

かなりいい感じにできていますよね。簡単な指示だけで、こういったコンポーネントであればすぐに作れてしまいます。

2. 画像内の一部のコンポーネントを実装

次に、画像の一部分だけを指定して、サイドバーを実装してもらいました。

result-2

スタイルは少し違いますが、たたき台としては十分な出来栄えですよね。メニューの選択機能もちゃんと実装されています。

面白いのは、コードの中身なんです。<nav>タグをしっかり使ってくれていたり、テーブルデザインの場合は<tr><td>タグを適切に使ってくれたりするんです。普段忘れがちな細かいところまで、きちんとコーディングしてくれているので、もしたたき台として使うのであればかなり良いたたき台ではないかと思います。

import { useState } from 'react'
import Link from 'next/link'
import {
  LayoutDashboard,
  Briefcase,
  Calendar,
  Users,
  MessageSquare,
  BarChart2,
  LogOut
} from 'lucide-react'

const menuItems = [
  { name: 'Dashboard', icon: LayoutDashboard },
  { name: 'Jobs', icon: Briefcase },
  { name: 'Schedule', icon: Calendar },
  { name: 'Community', icon: Users },
  { name: 'Messages', icon: MessageSquare },
  { name: 'Analysis', icon: BarChart2 },
  { name: 'Log out', icon: LogOut },
]

export default function Sidebar() {
  const [activeItem, setActiveItem] = useState('Community')

  return (
    <div className="w-64 h-screen bg-gray-100 flex flex-col">
      <div className="p-4">
        <Link href="/" className="flex items-center space-x-2">
          <div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
            <div className="w-4 h-4 border-2 border-white rotate-45"></div>
          </div>
          <span className="text-xl font-semibold">Workhubs</span>
        </Link>
      </div>
      <nav className="flex-1 pt-4">
        {menuItems.map((item) => (
          <Link
            key={item.name}
            href="#"
            className={`flex items-center space-x-3 px-4 py-3 text-gray-700 hover:bg-blue-50 ${
              activeItem === item.name ? 'bg-blue-100 text-blue-600' : ''
            }`}
            onClick={() => setActiveItem(item.name)}
          >
            <item.icon className={`w-5 h-5 ${activeItem === item.name ? 'text-blue-600' : 'text-gray-400'}`} />
            <span>{item.name}</span>
          </Link>
        ))}
      </nav>
    </div>
  )
}

3. データ取得と表示の実装

最後に、データ取得とその表示を実装してみました。今回は、Airbnbのホーム画面風のタブとリスト部分を作成してもらいます。

result-3

ここでのコツは、取得予定のデータの型など構造がわかるものを渡すことです。そうすることで、より適切な実装をしてくれるんです。また、モックデータからデータ取得処理に置き換えられるよう、fetch処理なども仮で実装してもらいました。

import { useState, useEffect } from 'react'
import Image from 'next/image'

type Place = {
  id: string
  name: string
  description: string
  image: string
  location: string
  price: number
  startDate: Date
  category: string[]
}

const categories = [
  "アイコニック",
  "最高の眺め",
  "ログハウス",
  "素敵なプール",
  "湖畔",
  "オフグリッドな暮らし",
  "大邸宅・屋敷",
  "城",
  "ツリーハウス",
  "ビーチフロント",
  "洗練されたデザイン",
  "田園地帯",
]

const mockPlaces: Place[] = [
  {
    id: '1',
    name: "プリンスの「パープル・レイン」の家に滞在",
    description: "プリンスゆかりの地で贅沢な時間を",
    image: "/placeholder.svg?height=300&width=300",
    location: "ミネソタ州、アメリカ",
    price: 1000,
    startDate: new Date("2023-10-01"),
    category: ["アイコニック", "洗練されたデザイン"]
  },
  {
    id: '2',
    name: "Doja Catのリビングルームセッション",
    description: "人気アーティストと過ごす特別な時間",
    image: "/placeholder.svg?height=300&width=300",
    location: "ロサンゼルス、アメリカ",
    price: 500,
    startDate: new Date("2023-10-15"),
    category: ["アイコニック"]
  },
  {
    id: '3',
    name: "ポリーポケットのコンパクトにお泊まり",
    description: "夢のようなミニチュアハウス体験",
    image: "/placeholder.svg?height=300&width=300",
    location: "トイランド",
    price: 200,
    startDate: new Date("2023-09-20"),
    category: ["ユニーク", "洗練されたデザイン"]
  },
  {
    id: '4',
    name: "ポリーポケットのコンパクトで遊ぼう",
    description: "ミニチュアの世界で冒険",
    image: "/placeholder.svg?height=300&width=300",
    location: "トイランド",
    price: 150,
    startDate: new Date("2023-09-25"),
    category: ["ユニーク", "洗練されたデザイン"]
  },
]

export default function AccommodationList() {
  const [places, setPlaces] = useState<Place[]>([])
  const [selectedCategory, setSelectedCategory] = useState<string>("アイコニック")

  useEffect(() => {
    // Simulating data fetching
    const fetchPlaces = async () => {
      // In a real application, you would fetch data from an API here
      await new Promise(resolve => setTimeout(resolve, 500)) // Simulate network delay
      setPlaces(mockPlaces)
    }

    fetchPlaces()
  }, [])

  const filteredPlaces = places.filter(place => place.category.includes(selectedCategory))

  return (
    <div className="container mx-auto px-4">
      <div className="overflow-x-auto whitespace-nowrap py-4 mb-6">
        <div className="inline-flex space-x-4">
          {categories.map((category) => (
            <button
              key={category}
              onClick={() => setSelectedCategory(category)}
              className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
                selectedCategory === category
                  ? 'bg-gray-900 text-white'
                  : 'bg-gray-100 text-gray-800 hover:bg-gray-200'
              }`}
            >
              {category}
            </button>
          ))}
        </div>
      </div>
      <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
        {filteredPlaces.map((place) => (
          <div key={place.id} className="border rounded-lg overflow-hidden shadow-lg bg-white">
            <div className="relative aspect-w-16 aspect-h-9">
              <Image
                src={place.image}
                alt={place.name}
                layout="fill"
                objectFit="cover"
              />
              <div className="absolute top-3 right-3">
                <button className="bg-white rounded-full p-2 shadow-md hover:bg-gray-100 transition-colors">
                  <svg xmlns="<http://www.w3.org/2000/svg>" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                    <path d="M15 8a3 3 0 10-2.977-2.63l-4.94 2.47a3 3 0 100 4.319l4.94 2.47a3 3 0 10.895-1.789l-4.94-2.47a3.027 3.027 0 000-.74l4.94-2.47C13.456 7.68 14.19 8 15 8z" />
                  </svg>
                </button>
              </div>
            </div>
            <div className="p-4">
              <h3 className="font-bold text-lg mb-2 line-clamp-2">{place.name}</h3>
              <p className="text-gray-600 text-sm mb-2 line-clamp-2">{place.description}</p>
              <p className="text-gray-500 text-sm">{place.location}</p>
              <p className="font-semibold mt-2">¥{place.price.toLocaleString()} / 泊</p>
              <p className="text-sm text-gray-500 mt-1">
                {place.startDate.toLocaleDateString('ja-JP')}から
              </p>
            </div>
          </div>
        ))}
      </div>
    </div>
  )
}

結果はかなり良く、タブ遷移もちゃんと動作します。ただ、スタイルが少し崩れていたので、追加でプロンプトを送って修正してもらいました。レスポンシブ対応も問題なくできるので、SP用の画像も一緒に渡してあげると、もっと良い感じになりそうですね。

以下は追加で「修正してください」と伝えた実行結果です。

今後の課題

v0は素晴らしいツールですが、現時点ではいくつか注意点があります。特に重要なのは、生成されたコードのプライバシーに関する問題です。

v0の公式FAQによると、以下のように述べられています:

Yes. Your generations may be reviewed by our AI team to improve our systems, but we will add the ability to opt-out in the coming weeks during the Beta.

参照先:Will my generations be used for training?

つまり、現時点では生成されたコードがAIチームによってレビューされる可能性があり、オプトアウトができないんです。これは、機密性の高いプロジェクトでの使用には向いていないということですね。

数週間以内には追加されるみたいなので、それまでの間は、個人開発や機密性の低いプロジェクトでの使用がおすすめです。もちろん、学習目的で使ってみるのも良いかと思います。

また、まだベータ版ということもあるので、本格導入の決定はまだ慎重になる必要がありそうです。

まとめ

v0を使うと、フロントエンド開発がぐっと楽になりそうです。

まだ完璧ではないですが、たたき台を作るのにはとても便利なのでぜひ、みなさんも試してみてください!

Anycloudではプロダクト開発の支援を行っています

プロダクト開発をお考えの方はぜひAnycloudにご相談ください。

まずは相談する

記事を書いた人

やました

PdM

やました

Twitter

株式会社AnycloudでPdMをしています