AIのあとしまつ

事例・基礎知識

v0の生成コードはそのまま使える?本番採用時にリファクタリングすべき3つのポイント

v0で生成されたコードを本番プロダクトとして採用する際に注意すべき品質リスクと、リファクタリングのポイントを3つに絞って解説します。

v0 コード品質v0 リファクタリングAI生成コード 本番化v0 本番 使えるVercel v0 品質コンポーネント 分割ハードコーディング 外部化AI コード 改善

Vercelの「v0」は、プロンプトから美しいUIを一瞬で生成してくれる素晴らしいツールです。「v0のコードはそのまま本番で使えますか?」という質問に、正直に答えます。

デモ・プロトタイプとして使うなら、そのままで十分です。投資家向けデモ、ユーザーインタビュー用のモックアップ、チーム内での仕様確認——これらの用途では品質よりスピードが優先されるため、v0の生成コードはそのまま使えます。

しかし本番プロダクトとして使う場合、そのままでは問題があります。ユーザーが実際に使い、データが蓄積され、機能を追加していくプロダクトとして、v0の生成コードはいくつかの重要な問題を抱えていることが多い。

以下に具体的なポイントを挙げます。

1. ハードコーディングされた値の外部化

v0は「見た目を作ること」に特化しているため、テキスト・URL・画像パスなどがコードに直接埋め込まれています。

問題のあるコード例(v0が生成するもの):

export function HeroSection() {
  return (
    <section>
      <h1>最高のSaaSプロダクト</h1>
      <p>あなたのビジネスを次のステージへ。月額3,980円から。</p>
      <a href="https://example.com/signup">今すぐ始める</a>
      <img src="/images/hero-placeholder.png" alt="Hero" />
    </section>
  );
}

リファクタリング後:

interface HeroSectionProps {
  title: string;
  description: string;
  ctaText: string;
  ctaHref: string;
  imageSrc: string;
}

export function HeroSection({ title, description, ctaText, ctaHref, imageSrc }: HeroSectionProps) {
  return (
    <section>
      <h1>{title}</h1>
      <p>{description}</p>
      <a href={ctaHref}>{ctaText}</a>
      <img src={imageSrc} alt="" />
    </section>
  );
}

propsとして受け取ることで、同じコンポーネントを異なるページや文脈で使い回せます。またCMSやデータベースからデータを渡すことも容易になります。文言の変更のたびにコードを触る必要もなくなります。

2. コンポーネントの分割と再利用性

v0が生成するコードは、しばしば1つのファイルに300〜500行の巨大なコードブロックとして記述されます。

問題のある状態(v0の生成コードによくある構造):

// page.tsx — 300行以上のファイル
export default function HomePage() {
  return (
    <main>
      {/* ナビゲーション(50行) */}
      <nav>
        <div className="flex items-center justify-between px-6 py-4">
          <div className="text-xl font-bold">MyApp</div>
          <div className="flex gap-4">
            <a href="/features">機能</a>
            <a href="/pricing">料金</a>
            <a href="/login">ログイン</a>
          </div>
        </div>
      </nav>

      {/* ヒーローセクション(80行) */}
      {/* 料金セクション(100行) */}
      {/* FAQセクション(70行) */}
      {/* フッター(50行) */}
    </main>
  );
}

リファクタリング後のディレクトリ構造:

components/
  layout/
    Navbar.tsx
    Footer.tsx
  sections/
    HeroSection.tsx
    PricingSection.tsx
    FaqSection.tsx

app/
  page.tsx  ← 30行程度になる

分割することで、FAQセクションのデザインを変えたい時はFaqSection.tsxだけを触ればよくなります。また同じNavbarを複数ページで使い回せます。チームで開発する場合もファイルの競合が起きにくくなります。

3. アクセシビリティとセマンティックHTML

見た目は整っていても、v0が生成するHTMLは div の乱用(いわゆる「divスープ」)になっていることがあります。

問題のあるコード例:

{/* divスープ — スクリーンリーダーが意味を読み取れない */}
<div className="nav-container">
  <div className="nav-logo">MyApp</div>
  <div className="nav-links">
    <div onClick={() => navigate('/features')}>機能</div>
    <div onClick={() => navigate('/pricing')}>料金</div>
  </div>
  <div className="cta-button" onClick={handleSignup}>登録する</div>
</div>

修正後:

<nav aria-label="メインナビゲーション">
  <a href="/" aria-label="MyApp ホームへ">MyApp</a>
  <ul>
    <li><a href="/features">機能</a></li>
    <li><a href="/pricing">料金</a></li>
  </ul>
  <button type="button" onClick={handleSignup}>登録する</button>
</nav>

セマンティックHTMLはSEOに影響します(Googleはヘッダー・ナビゲーション・メインコンテンツを意味的に理解する)。また、スクリーンリーダーを使う視覚障害のあるユーザーが正しく使えるかどうかに直結します。divonClick をつけるのではなく、buttona を使うだけで大きく改善されます。

4. エラー状態・ローディング状態の欠如

v0が生成するコンポーネントは、「正常に動いている時」の見た目しか含んでいないことがほとんどです。

// v0が生成するコンポーネント(正常系のみ)
export function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  return (
    <div>
      <img src={user.avatarUrl} alt={user.name} />
      <h2>{user.name}</h2>
    </div>
  );
}

このコードにはデータ取得中(usernull の状態)にクラッシュするバグがあります。また、APIエラーが起きた時の処理もありません。

本番品質のコード:

export function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchUser(userId)
      .then(setUser)
      .catch(() => setError("ユーザー情報の取得に失敗しました"))
      .finally(() => setLoading(false));
  }, [userId]);

  if (loading) return <Skeleton />;
  if (error) return <ErrorMessage message={error} />;
  if (!user) return <p>ユーザーが見つかりません</p>;

  return (
    <div>
      <img src={user.avatarUrl} alt={user.name} />
      <h2>{user.name}</h2>
    </div>
  );
}

ローディング状態・エラー状態・空状態の3つを必ず実装してください。

5. TypeScript型の不完全さ

v0が生成するコードには、型定義が甘い部分が多く含まれます。

// よくあるv0の型定義
function ProductCard({ product }: any) { // anyは型安全でない
  return <div>{product.name}</div>;
}

// または
function ProductCard({ product }) { // 型定義なし
  return <div>{product.name}</div>;
}

any 型は「TypeScriptの型チェックを無効化する」と同義です。これがあると、product.nam とタイポしても何もエラーが出ず、ランタイムエラーとして本番で発生します。

// 正しい型定義
interface Product {
  id: string;
  name: string;
  price: number;
  imageUrl: string | null;
}

function ProductCard({ product }: { product: Product }) {
  return <div>{product.name}</div>;
}

v0コードを本番化する実践的なワークフロー

  1. v0でUIを生成する(5〜15分)
  2. コンポーネントを適切なファイルに分割する(30分〜1時間)
  3. ハードコーディングされた値をpropsまたはconstantsに移す(30分)
  4. TypeScriptの型を追加・修正する(30分〜1時間)
  5. エラー・ローディング・空状態を実装する(コンポーネントごとに30分〜1時間)
  6. セマンティックHTMLに修正する(30分)

v0で生成したコンポーネント1つを本番品質にするには、おおよそ1〜4時間かかります。UIが複雑なほど、ビジネスロジックが多いほど時間は伸びます。

v0のコードをそのまま使わない方がいいケース

以下のコンポーネントは、v0の生成コードをそのまま本番で使わないでください。

  • 認証フォーム(ログイン・サインアップ): セキュリティ要件が複雑でバリデーション不備が危険
  • 決済フォーム: カード情報の取り扱いにはPCI DSSへの対応が必要
  • 個人情報を扱うフォーム: バリデーションとサニタイズが必須

v0は「0から1」を作る最強のツールです。しかし「1から10」にするには、エンジニアによる仕上げが不可欠です。v0を起点にして、本番品質に仕上げるプロセスをセットで考えましょう。