𝕏 f B! L
案件・求人数 12,345
案件を探す(準備中) エージェントを探す(準備中) お役立ち情報 ログイン
案件・求人数 12,345
Gemini CLIでReact/Next.js開発を効率化する方法|コンポーネント設計・SSR・App Router実践ガイド

Gemini CLIでReact/Next.js開発を効率化する方法|コンポーネント設計・SSR・App Router実践ガイド

Gemini CLIReactNext.jsフロントエンド開発AI開発ツール
目次

「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がReact/Next.js開発に強い理由

Gemini CLIはGoogleが提供するCLI型AIコーディングツールです。React/Next.js開発において特に力を発揮する理由は、最新のフレームワーク仕様を深く理解している点にあります。

Gemini CLIの3つの強み

  1. App Router対応: Next.js 13以降のApp Routerの設計パターンを正確に理解
  2. TypeScript親和性: 型安全なコンポーネント定義を自然に生成
  3. エコシステム理解: 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パフォーマンスチューニングが参考になります。

SES案件をお探しですか?

SES記事をもっと読む →
🏗️

SES BASE 編集長

SES業界歴10年以上のメンバーが在籍する編集チーム。SES企業での営業・エンジニア経験、フリーランス独立経験を持つメンバーが、業界のリアルな情報をお届けします。

📊 業界データに基づく記事制作 🔍 IPA・経済産業省データ参照 💼 SES実務経験者が執筆・監修