「Next.jsのApp Routerに移行したいけど、Server ComponentsとClient Componentsの使い分けがわからない」「コンポーネント設計に時間がかかりすぎる」——React/Next.js開発でこうした悩みを抱えるエンジニアは少なくありません。
結論から言えば、Gemini CLIを活用することでReact/Next.jsのコンポーネント設計からApp Router対応、パフォーマンス最適化まで大幅に効率化できます。本記事では、実際の開発フローに沿った具体的なプロンプト例と実装パターンを解説します。
この記事を3秒でまとめると
- Gemini CLIはNext.js App Routerのコンポーネント設計に特に強い
- Server Components / Client Componentsの適切な使い分けをAIが提案
- デザインシステム構築からSSR/ISR最適化まで一貫して効率化できる

Gemini CLIがReact/Next.js開発に強い理由
Gemini CLIはGoogleが提供するCLI型AIコーディングツールです。React/Next.js開発において特に力を発揮する理由は、最新のフレームワーク仕様を深く理解している点にあります。
Gemini CLIの3つの強み
- App Router対応: Next.js 13以降のApp Routerの設計パターンを正確に理解
- TypeScript親和性: 型安全なコンポーネント定義を自然に生成
- エコシステム理解: Tailwind CSS、Shadcn/UI、Zustand、React Queryなど主要ライブラリとの連携に対応
SES案件でのReact/Next.js需要
SES市場でのフロントエンド案件は常に高需要です。特にNext.jsを使った案件は増加傾向にあります。
| フレームワーク | 案件数推移 | 平均単価(月額) |
|---|---|---|
| Next.js (App Router) | ↑↑ 急増 | 75〜95万円 |
| React (SPA) | → 横ばい | 65〜85万円 |
| Vue.js / Nuxt | → 横ばい | 60〜80万円 |
| Angular | ↓ 微減 | 65〜85万円 |
Next.jsのApp Router対応スキルを持つエンジニアの需要は特に高く、Gemini CLIで効率的にキャッチアップできることは大きなメリットです。
GEMINI.mdでプロジェクト文脈を設定する
Gemini CLIの精度を最大限に引き出すために、プロジェクトルートにGEMINI.mdを配置しましょう。
# GEMINI.md
## プロジェクト概要
SES案件管理ダッシュボード(Next.js 15 App Router)
## 技術スタック
- Next.js 15 (App Router)
- TypeScript 5.6 (strict mode)
- Tailwind CSS v4
- Shadcn/UI コンポーネント
- Zustand(状態管理)
- TanStack Query v5(データフェッチ)
- Prisma(ORM)
- PostgreSQL
## ディレクトリ構造
src/
├── app/ # App Router ページ
├── components/ # 共有コンポーネント
│ ├── ui/ # 基本UIコンポーネント
│ └── features/ # 機能コンポーネント
├── hooks/ # カスタムフック
├── lib/ # ユーティリティ
├── types/ # 型定義
└── server/ # Server Actions
## コーディング規約
- コンポーネントは named export
- Server Components をデフォルトにし、必要な場合のみ 'use client'
- Tailwind CSS でスタイリング(CSS Modules 不使用)
- テストは Vitest + Testing Library
コンポーネント設計をGemini CLIで効率化する
Server Components vs Client Componentsの使い分け
Next.js App Routerでは、Server ComponentsとClient Componentsを適切に使い分けることが重要です。Gemini CLIに判断を任せられます。
プロンプト例:
SES案件一覧ページのコンポーネント設計をお願いします。
以下の要件をServer ComponentsとClient Componentsに適切に分割してください。
表示項目:
- 案件名、単価、勤務地、スキル要件、掲載日
- フィルタ機能(スキル、単価範囲、勤務地)
- ソート機能(単価順、掲載日順)
- ページネーション
- お気に入り登録ボタン
データ: Prisma経由でDBから取得
Gemini CLIが提案する設計:
// app/projects/page.tsx (Server Component)
import { Suspense } from 'react';
import { ProjectList } from '@/components/features/project/ProjectList';
import { ProjectFilters } from '@/components/features/project/ProjectFilters';
import { ProjectListSkeleton } from '@/components/features/project/ProjectListSkeleton';
import { getProjects } from '@/server/actions/projects';
type SearchParams = {
skill?: string;
minRate?: string;
maxRate?: string;
location?: string;
sort?: 'rate' | 'date';
page?: string;
};
export default async function ProjectsPage({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
const params = await searchParams;
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">SES案件一覧</h1>
{/* Client Component: インタラクティブなフィルタ */}
<ProjectFilters
defaultSkill={params.skill}
defaultMinRate={params.minRate}
defaultMaxRate={params.maxRate}
defaultLocation={params.location}
/>
{/* Server Component: データ取得とレンダリング */}
<Suspense fallback={<ProjectListSkeleton />}>
<ProjectList
skill={params.skill}
minRate={params.minRate ? Number(params.minRate) : undefined}
maxRate={params.maxRate ? Number(params.maxRate) : undefined}
location={params.location}
sort={params.sort || 'date'}
page={params.page ? Number(params.page) : 1}
/>
</Suspense>
</div>
);
}
// components/features/project/ProjectFilters.tsx
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback, useTransition } from 'react';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Slider } from '@/components/ui/slider';
const LOCATIONS = ['東京', '大阪', '名古屋', '福岡', 'リモート'];
const SKILLS = ['Java', 'Python', 'TypeScript', 'Go', 'AWS', 'React'];
type Props = {
defaultSkill?: string;
defaultMinRate?: string;
defaultMaxRate?: string;
defaultLocation?: string;
};
export function ProjectFilters({
defaultSkill,
defaultMinRate,
defaultMaxRate,
defaultLocation,
}: Props) {
const router = useRouter();
const searchParams = useSearchParams();
const [isPending, startTransition] = useTransition();
const updateFilter = useCallback(
(key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
params.delete('page'); // フィルタ変更時はページリセット
startTransition(() => {
router.push(`/projects?${params.toString()}`);
});
},
[router, searchParams]
);
return (
<div className={`grid grid-cols-1 md:grid-cols-4 gap-4 mb-8 ${
isPending ? 'opacity-60' : ''
}`}>
<Select
defaultValue={defaultSkill}
onValueChange={(v) => updateFilter('skill', v)}
>
<SelectTrigger>
<SelectValue placeholder="スキルで絞り込み" />
</SelectTrigger>
<SelectContent>
{SKILLS.map((skill) => (
<SelectItem key={skill} value={skill}>
{skill}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
defaultValue={defaultLocation}
onValueChange={(v) => updateFilter('location', v)}
>
<SelectTrigger>
<SelectValue placeholder="勤務地" />
</SelectTrigger>
<SelectContent>
{LOCATIONS.map((loc) => (
<SelectItem key={loc} value={loc}>
{loc}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="col-span-2">
<label className="text-sm text-muted-foreground mb-2 block">
単価範囲(万円/月)
</label>
<Slider
defaultValue={[
Number(defaultMinRate || 40),
Number(defaultMaxRate || 120),
]}
min={30}
max={150}
step={5}
onValueCommit={([min, max]) => {
updateFilter('minRate', String(min));
updateFilter('maxRate', String(max));
}}
/>
</div>
</div>
);
}
Server Actionsの活用
App RouterではServer Actionsでサーバーサイドの処理を直接呼び出せます。
プロンプト例:
お気に入り登録のServer Actionを作成してください。
- 楽観的更新(useOptimistic)対応
- ログインチェック
- エラーハンドリング
- revalidatePath
// server/actions/favorites.ts
'use server';
import { revalidatePath } from 'next/cache';
import { auth } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
export async function toggleFavorite(projectId: string) {
const session = await auth();
if (!session?.user?.id) {
throw new Error('ログインが必要です');
}
const existing = await prisma.favorite.findUnique({
where: {
userId_projectId: {
userId: session.user.id,
projectId,
},
},
});
if (existing) {
await prisma.favorite.delete({
where: { id: existing.id },
});
} else {
await prisma.favorite.create({
data: {
userId: session.user.id,
projectId,
},
});
}
revalidatePath('/projects');
return { isFavorite: !existing };
}
// components/features/project/FavoriteButton.tsx
'use client';
import { useOptimistic, useTransition } from 'react';
import { Heart } from 'lucide-react';
import { toggleFavorite } from '@/server/actions/favorites';
import { Button } from '@/components/ui/button';
type Props = {
projectId: string;
isFavorite: boolean;
};
export function FavoriteButton({ projectId, isFavorite }: Props) {
const [optimisticFav, setOptimisticFav] = useOptimistic(isFavorite);
const [isPending, startTransition] = useTransition();
const handleClick = () => {
startTransition(async () => {
setOptimisticFav(!optimisticFav);
try {
await toggleFavorite(projectId);
} catch (error) {
// 楽観的更新を戻す(revalidateで正しい状態に復元)
console.error('お気に入り更新失敗:', error);
}
});
};
return (
<Button
variant="ghost"
size="icon"
onClick={handleClick}
disabled={isPending}
aria-label={optimisticFav ? 'お気に入り解除' : 'お気に入り登録'}
>
<Heart
className={`h-5 w-5 transition-colors ${
optimisticFav
? 'fill-red-500 text-red-500'
: 'text-muted-foreground'
}`}
/>
</Button>
);
}
データフェッチパターンの最適化
Parallel Data Fetching
Next.js App Routerでは、並列データフェッチが重要なパフォーマンス最適化テクニックです。
プロンプト例:
ダッシュボードページで複数のデータを並列取得してください。
取得データ:
1. 稼働中の案件一覧
2. 今月の売上サマリー
3. 未読メッセージ数
4. お知らせ一覧
各データのローディング状態を独立して管理し、
Suspenseで段階的に表示すること。
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { ActiveProjects } from './components/ActiveProjects';
import { RevenueSummary } from './components/RevenueSummary';
import { UnreadMessages } from './components/UnreadMessages';
import { Announcements } from './components/Announcements';
import { CardSkeleton } from '@/components/ui/CardSkeleton';
export default function DashboardPage() {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">ダッシュボード</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<Suspense fallback={<CardSkeleton />}>
<RevenueSummary />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<ActiveProjects />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<UnreadMessages />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<Announcements />
</Suspense>
</div>
</div>
);
}
ISR(Incremental Static Regeneration)の活用
頻繁に更新されないデータにはISRを使い、パフォーマンスとデータ鮮度のバランスをとりましょう。
// app/projects/[id]/page.tsx
import { notFound } from 'next/navigation';
import { prisma } from '@/lib/prisma';
// ISR: 60秒ごとに再生成
export const revalidate = 60;
export async function generateStaticParams() {
const projects = await prisma.project.findMany({
select: { id: true },
where: { status: 'active' },
take: 100, // ビルド時に上位100件を事前生成
});
return projects.map((p) => ({ id: p.id }));
}
export default async function ProjectDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const project = await prisma.project.findUnique({
where: { id },
include: {
skills: true,
company: { select: { name: true, rating: true } },
},
});
if (!project) notFound();
return (
<article className="container mx-auto px-4 py-8 max-w-4xl">
<h1 className="text-3xl font-bold mb-4">{project.title}</h1>
<div className="flex gap-2 mb-6">
{project.skills.map((skill) => (
<span
key={skill.id}
className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm"
>
{skill.name}
</span>
))}
</div>
<div className="prose prose-lg max-w-none">
<p>{project.description}</p>
</div>
</article>
);
}
状態管理の設計
Zustandでのグローバル状態管理
プロンプト例:
案件検索の状態管理をZustandで設計してください。
- 検索条件の永続化(URL同期)
- 検索履歴の保持(最新10件)
- お気に入りのローカルキャッシュ
// stores/projectSearchStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
type SearchFilters = {
skill: string | null;
minRate: number;
maxRate: number;
location: string | null;
sort: 'rate' | 'date';
keyword: string;
};
type SearchHistory = {
filters: SearchFilters;
timestamp: number;
resultCount: number;
};
type ProjectSearchState = {
filters: SearchFilters;
history: SearchHistory[];
favorites: Set<string>;
setFilter: <K extends keyof SearchFilters>(
key: K,
value: SearchFilters[K]
) => void;
resetFilters: () => void;
addToHistory: (resultCount: number) => void;
toggleFavorite: (projectId: string) => void;
isFavorite: (projectId: string) => boolean;
};
const DEFAULT_FILTERS: SearchFilters = {
skill: null,
minRate: 40,
maxRate: 120,
location: null,
sort: 'date',
keyword: '',
};
export const useProjectSearchStore = create<ProjectSearchState>()(
persist(
(set, get) => ({
filters: { ...DEFAULT_FILTERS },
history: [],
favorites: new Set<string>(),
setFilter: (key, value) =>
set((state) => ({
filters: { ...state.filters, [key]: value },
})),
resetFilters: () =>
set({ filters: { ...DEFAULT_FILTERS } }),
addToHistory: (resultCount) =>
set((state) => ({
history: [
{
filters: { ...state.filters },
timestamp: Date.now(),
resultCount,
},
...state.history,
].slice(0, 10),
})),
toggleFavorite: (projectId) =>
set((state) => {
const newFavorites = new Set(state.favorites);
if (newFavorites.has(projectId)) {
newFavorites.delete(projectId);
} else {
newFavorites.add(projectId);
}
return { favorites: newFavorites };
}),
isFavorite: (projectId) => get().favorites.has(projectId),
}),
{
name: 'project-search',
partialize: (state) => ({
history: state.history,
favorites: Array.from(state.favorites),
}),
}
)
);
パフォーマンス最適化
画像最適化
Next.jsのImageコンポーネントを使った最適化パターンです。
// components/ui/OptimizedImage.tsx
import Image from 'next/image';
type Props = {
src: string;
alt: string;
width: number;
height: number;
priority?: boolean;
className?: string;
};
export function OptimizedImage({
src,
alt,
width,
height,
priority = false,
className,
}: Props) {
return (
<Image
src={src}
alt={alt}
width={width}
height={height}
priority={priority}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className={className}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
quality={80}
/>
);
}
バンドルサイズの最適化
プロンプト例:
Next.jsアプリのバンドルサイズを分析して最適化してください。
- 動的インポートの活用
- ツリーシェイキングの改善
- 不要な依存関係の特定
// 動的インポートの活用例
import dynamic from 'next/dynamic';
// 重いチャートコンポーネントを遅延ロード
const RevenueChart = dynamic(
() => import('@/components/features/dashboard/RevenueChart'),
{
loading: () => <div className="h-64 animate-pulse bg-muted rounded" />,
ssr: false, // クライアントサイドのみでレンダリング
}
);
// マークダウンエディタを遅延ロード
const MarkdownEditor = dynamic(
() => import('@/components/features/editor/MarkdownEditor'),
{
loading: () => <div className="h-96 animate-pulse bg-muted rounded" />,
}
);
テストの効率化
Testing Libraryでのコンポーネントテスト
// __tests__/components/FavoriteButton.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FavoriteButton } from '@/components/features/project/FavoriteButton';
import { vi, describe, it, expect } from 'vitest';
vi.mock('@/server/actions/favorites', () => ({
toggleFavorite: vi.fn().mockResolvedValue({ isFavorite: true }),
}));
describe('FavoriteButton', () => {
it('お気に入り未登録状態で表示される', () => {
render(<FavoriteButton projectId="1" isFavorite={false} />);
expect(
screen.getByLabelText('お気に入り登録')
).toBeInTheDocument();
});
it('クリックでお気に入り状態が切り替わる', async () => {
const user = userEvent.setup();
render(<FavoriteButton projectId="1" isFavorite={false} />);
await user.click(screen.getByLabelText('お気に入り登録'));
expect(
screen.getByLabelText('お気に入り解除')
).toBeInTheDocument();
});
it('お気に入り登録済みの場合はハートが塗りつぶされる', () => {
render(<FavoriteButton projectId="1" isFavorite={true} />);
const heart = screen.getByLabelText('お気に入り解除');
expect(heart.querySelector('svg')).toHaveClass('fill-red-500');
});
});
まとめ:Gemini CLIでReact/Next.js開発を加速しよう
React/Next.jsの開発は、App Routerの登場により設計の複雑性が増しています。しかし、Gemini CLIを活用すればServer Components/Client Componentsの適切な使い分け、Server Actions、ISR設計、状態管理まで効率的に進められます。
SESエンジニアにとって、Next.jsのApp Router対応スキルは案件単価の向上に直結します。Gemini CLIを武器に、最新のフロントエンド開発にチャレンジしてみてください。
Gemini CLIの基本的な使い方はGemini CLI入門ガイドを、フルスタック開発はGemini CLIフルスタック開発ガイドをご覧ください。テスト自動化についてはGemini CLIテスト自動化、パフォーマンスチューニングはGemini CLIパフォーマンスチューニングが参考になります。