事例・基礎知識
v0の生成コードはそのまま使える?本番採用時にリファクタリングすべき3つのポイント
v0で生成されたコードを本番プロダクトとして採用する際に注意すべき品質リスクと、リファクタリングのポイントを3つに絞って解説します。
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はヘッダー・ナビゲーション・メインコンテンツを意味的に理解する)。また、スクリーンリーダーを使う視覚障害のあるユーザーが正しく使えるかどうかに直結します。div に onClick をつけるのではなく、button や a を使うだけで大きく改善されます。
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>
);
}
このコードにはデータ取得中(user が null の状態)にクラッシュするバグがあります。また、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コードを本番化する実践的なワークフロー
- v0でUIを生成する(5〜15分)
- コンポーネントを適切なファイルに分割する(30分〜1時間)
- ハードコーディングされた値をpropsまたはconstantsに移す(30分)
- TypeScriptの型を追加・修正する(30分〜1時間)
- エラー・ローディング・空状態を実装する(コンポーネントごとに30分〜1時間)
- セマンティックHTMLに修正する(30分)
v0で生成したコンポーネント1つを本番品質にするには、おおよそ1〜4時間かかります。UIが複雑なほど、ビジネスロジックが多いほど時間は伸びます。
v0のコードをそのまま使わない方がいいケース
以下のコンポーネントは、v0の生成コードをそのまま本番で使わないでください。
- 認証フォーム(ログイン・サインアップ): セキュリティ要件が複雑でバリデーション不備が危険
- 決済フォーム: カード情報の取り扱いにはPCI DSSへの対応が必要
- 個人情報を扱うフォーム: バリデーションとサニタイズが必須
v0は「0から1」を作る最強のツールです。しかし「1から10」にするには、エンジニアによる仕上げが不可欠です。v0を起点にして、本番品質に仕上げるプロセスをセットで考えましょう。