Documentation Next.js

はじめに

新しい Next.js プロジェクトを始めるたびに、同じ設定を繰り返していませんか?TypeScript の設定、ESLint のルール追加、Prettier の導入、テスト環境の構築など、毎回同じ作業を行うのは非効率的です。

この記事では、再利用可能なプロジェクトテンプレートを作成する方法を解説します。一度テンプレートを作成しておけば、新しいプロジェクトを数分で立ち上げられるようになります。

この記事で学べること

  • プロジェクトテンプレートに含めるべき基本機能
  • TypeScript、Tailwind CSS、ESLint、Prettier の設定方法
  • Docker を使った開発環境の構築
  • Jest と React Testing Library によるテスト環境のセットアップ
  • GitHub Actions を使った CI/CD パイプラインの構築

対象読者

  • Next.js でのプロジェクト作成経験がある方
  • 開発効率を向上させたい方
  • チーム開発で統一された環境を構築したい方

プロジェクトテンプレートの基本構成

プロジェクトテンプレートとは、新しいプロジェクトを始める際のベースとなる設定済みのコードベースです。以下の機能を含めることで、すぐに開発に取り掛かれる環境を提供できます。

含めるべき主要機能

機能説明メリット
TypeScript静的型付け言語型安全性の確保、IDE補完の向上
Tailwind CSSユーティリティファーストCSSフレームワーク高速なスタイリング、一貫したデザイン
ESLintJavaScript/TypeScriptのリンターコード品質の維持、バグの早期発見
Prettierコードフォーマッター一貫したコードスタイル
Jestテストフレームワークユニットテストの実行
React Testing LibraryUIテストライブラリコンポーネントテスト
Dockerコンテナ化ツール環境の一貫性、デプロイの簡素化

推奨するディレクトリ構造

my-nextjs-template/
├── .github/
│   └── workflows/
│       └── ci.yml          # CI/CD設定
├── src/
│   ├── app/                # App Router
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   └── globals.css
│   ├── components/         # 共通コンポーネント
│   │   └── ui/
│   ├── lib/                # ユーティリティ関数
│   ├── hooks/              # カスタムフック
│   └── types/              # 型定義
├── __tests__/              # テストファイル
├── public/                 # 静的ファイル
├── .eslintrc.json
├── .prettierrc
├── Dockerfile
├── docker-compose.yml
├── jest.config.js
├── next.config.js
├── package.json
├── tailwind.config.ts
└── tsconfig.json

テンプレートの作成手順

Step 1: プロジェクトの初期設定

まず、create-next-app を使用して Next.js プロジェクトを作成します。対話形式で各種オプションを選択できます。

# 最新のNext.jsプロジェクトを作成
npx create-next-app@latest my-nextjs-template

# 以下のオプションを選択(推奨)
# ✔ Would you like to use TypeScript? → Yes
# ✔ Would you like to use ESLint? → Yes
# ✔ Would you like to use Tailwind CSS? → Yes
# ✔ Would you like to use `src/` directory? → Yes
# ✔ Would you like to use App Router? → Yes
# ✔ Would you like to customize the default import alias? → Yes (@/*)

このコマンドにより、TypeScript、ESLint、Tailwind CSS が有効化されたプロジェクトが作成されます。

# プロジェクトディレクトリに移動
cd my-nextjs-template

Step 2: ESLint と Prettier の設定

コード品質を担保するために、ESLint と Prettier を適切に設定します。

Prettier のインストール

# Prettierと関連パッケージをインストール
npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier

.prettierrc の作成

{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "tabWidth": 2,
  "useTabs": false,
  "printWidth": 80,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf"
}

各オプションの説明:

  • semi: 文末にセミコロンを付ける
  • trailingComma: 末尾のカンマを ES5 準拠で付ける
  • singleQuote: シングルクォートを使用
  • tabWidth: インデント幅を2スペースに設定

.eslintrc.json の更新

{
  "extends": [
    "next/core-web-vitals",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ],
  "plugins": ["@typescript-eslint", "prettier"],
  "rules": {
    "prettier/prettier": "error",
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/explicit-function-return-type": "warn",
    "react/react-in-jsx-scope": "off",
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  },
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2021,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  }
}

package.json にスクリプトを追加

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "lint:fix": "next lint --fix",
    "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
    "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\""
  }
}

Step 3: Tailwind CSS のカスタマイズ

tailwind.config.ts をプロジェクトに合わせてカスタマイズします。

import type { Config } from 'tailwindcss';

const config: Config = {
  // Tailwind CSSを適用するファイルを指定
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      // カスタムカラーパレット
      colors: {
        primary: {
          50: '#f0f9ff',
          100: '#e0f2fe',
          500: '#0ea5e9',
          600: '#0284c7',
          700: '#0369a1',
        },
        secondary: {
          50: '#f8fafc',
          100: '#f1f5f9',
          500: '#64748b',
          600: '#475569',
          700: '#334155',
        },
      },
      // カスタムフォント
      fontFamily: {
        sans: ['var(--font-inter)', 'sans-serif'],
        mono: ['var(--font-fira-code)', 'monospace'],
      },
      // カスタムスペーシング
      spacing: {
        '18': '4.5rem',
        '88': '22rem',
      },
    },
  },
  plugins: [],
};

export default config;

Step 4: 共通コンポーネントの作成

テンプレートに含める基本的なコンポーネントを作成します。

ボタンコンポーネント

// src/components/ui/Button.tsx
import { ButtonHTMLAttributes, forwardRef } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

// ボタンのバリエーションを定義
const buttonVariants = cva(
  // 基本スタイル
  'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default: 'bg-primary-600 text-white hover:bg-primary-700',
        secondary: 'bg-secondary-100 text-secondary-700 hover:bg-secondary-200',
        outline: 'border border-gray-300 bg-transparent hover:bg-gray-100',
        ghost: 'hover:bg-gray-100',
        destructive: 'bg-red-600 text-white hover:bg-red-700',
      },
      size: {
        sm: 'h-8 px-3 text-xs',
        md: 'h-10 px-4',
        lg: 'h-12 px-6 text-base',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'md',
    },
  }
);

// Propsの型定義
export interface ButtonProps
  extends ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  isLoading?: boolean;
}

// ボタンコンポーネント
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, isLoading, children, ...props }, ref) => {
    return (
      <button
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        disabled={isLoading || props.disabled}
        {...props}
      >
        {isLoading ? (
          <span className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
        ) : null}
        {children}
      </button>
    );
  }
);

Button.displayName = 'Button';

export { Button, buttonVariants };

ユーティリティ関数

// src/lib/utils.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

/**
 * クラス名を結合するユーティリティ関数
 * clsxで条件付きクラスを処理し、twMergeでTailwindの競合を解決
 */
export function cn(...inputs: ClassValue[]): string {
  return twMerge(clsx(inputs));
}

/**
 * 指定したミリ秒待機するPromiseを返す
 */
export function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * 日付をフォーマットする
 */
export function formatDate(date: Date, locale = 'ja-JP'): string {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  }).format(date);
}

必要なパッケージをインストールします。

npm install clsx tailwind-merge class-variance-authority

Step 5: Docker の設定

開発環境と本番環境で一貫した動作を保証するために Docker を設定します。

Dockerfile(本番用)

# Dockerfile

# ---- ベースイメージ ----
FROM node:20-alpine AS base

# ---- 依存関係のインストール ----
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

# package.jsonとlockファイルをコピー
COPY package.json package-lock.json* ./

# 依存関係をインストール
RUN npm ci

# ---- ビルドステージ ----
FROM base AS builder
WORKDIR /app

# 依存関係をコピー
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# 環境変数を設定(ビルド時に必要な場合)
# ENV NEXT_TELEMETRY_DISABLED 1

# アプリケーションをビルド
RUN npm run build

# ---- 本番イメージ ----
FROM base AS runner
WORKDIR /app

# 本番環境として設定
ENV NODE_ENV production

# セキュリティのため非rootユーザーを作成
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# 必要なファイルのみコピー
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

# 非rootユーザーに切り替え
USER nextjs

# ポートを公開
EXPOSE 3000
ENV PORT 3000

# アプリケーションを起動
CMD ["node", "server.js"]

docker-compose.yml(開発用)

# docker-compose.yml
version: '3.8'

services:
  # Next.js開発サーバー
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - '3000:3000'
    volumes:
      # ソースコードをマウント(ホットリロード用)
      - .:/app
      # node_modulesはコンテナ内のものを使用
      - /app/node_modules
    environment:
      - NODE_ENV=development
    command: npm run dev

  # PostgreSQL(必要に応じて)
  db:
    image: postgres:15-alpine
    ports:
      - '5432:5432'
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: myapp_dev
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Dockerfile.dev(開発用)

# Dockerfile.dev
FROM node:20-alpine

WORKDIR /app

# 依存関係をインストール
COPY package.json package-lock.json* ./
RUN npm install

# ソースコードはvolumeでマウントするためコピー不要
EXPOSE 3000

CMD ["npm", "run", "dev"]

next.config.js の更新(スタンドアロン出力用)

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Dockerでの本番デプロイ用にスタンドアロン出力を有効化
  output: 'standalone',

  // その他の設定
  reactStrictMode: true,

  // 画像最適化の設定
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**.example.com',
      },
    ],
  },
};

module.exports = nextConfig;

Step 6: テスト環境のセットアップ

Jest と React Testing Library を使ったテスト環境を構築します。

必要なパッケージのインストール

npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @types/jest

jest.config.js の作成

// jest.config.js
const nextJest = require('next/jest');

// Next.jsの設定を読み込むための関数を作成
const createJestConfig = nextJest({
  // next.config.jsとテスト環境用の.envファイルが配置されたディレクトリ
  dir: './',
});

/** @type {import('jest').Config} */
const customJestConfig = {
  // テストファイルのパターン
  testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],

  // セットアップファイル
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],

  // テスト環境
  testEnvironment: 'jest-environment-jsdom',

  // モジュールのエイリアス設定(tsconfig.jsonと一致させる)
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },

  // カバレッジ設定
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.stories.{js,jsx,ts,tsx}',
  ],

  // カバレッジの閾値
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 70,
      lines: 70,
      statements: 70,
    },
  },
};

// createJestConfigを使ってNext.jsの設定を反映
module.exports = createJestConfig(customJestConfig);

jest.setup.js の作成

// jest.setup.js
import '@testing-library/jest-dom';

// グローバルなモックを設定
global.ResizeObserver = jest.fn().mockImplementation(() => ({
  observe: jest.fn(),
  unobserve: jest.fn(),
  disconnect: jest.fn(),
}));

// Next.js Imageコンポーネントのモック
jest.mock('next/image', () => ({
  __esModule: true,
  default: (props) => {
    // eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text
    return <img {...props} />;
  },
}));

サンプルテストの作成

// __tests__/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from '@/components/ui/Button';

describe('Button', () => {
  // 基本的なレンダリングテスト
  it('renders correctly with default props', () => {
    render(<Button>Click me</Button>);

    const button = screen.getByRole('button', { name: /click me/i });
    expect(button).toBeInTheDocument();
  });

  // バリアントのテスト
  it('applies correct styles for different variants', () => {
    const { rerender } = render(<Button variant="default">Default</Button>);
    expect(screen.getByRole('button')).toHaveClass('bg-primary-600');

    rerender(<Button variant="secondary">Secondary</Button>);
    expect(screen.getByRole('button')).toHaveClass('bg-secondary-100');
  });

  // クリックイベントのテスト
  it('calls onClick handler when clicked', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);

    fireEvent.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  // ローディング状態のテスト
  it('shows loading spinner when isLoading is true', () => {
    render(<Button isLoading>Loading</Button>);

    const button = screen.getByRole('button');
    expect(button).toBeDisabled();
  });

  // disabled状態のテスト
  it('is disabled when disabled prop is true', () => {
    render(<Button disabled>Disabled</Button>);

    expect(screen.getByRole('button')).toBeDisabled();
  });
});

package.json にテストスクリプトを追加

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

CI/CD パイプラインの構築

GitHub Actions を使って、プッシュ時に自動でテストとビルドを実行する CI/CD パイプラインを構築します。

GitHub Actions ワークフローの作成

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  # リント・フォーマットチェック
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run ESLint
        run: npm run lint

      - name: Check formatting
        run: npm run format:check

  # テスト実行
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm run test:coverage

      - name: Upload coverage report
        uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          fail_ci_if_error: false

  # ビルド確認
  build:
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: build
          path: .next
          retention-days: 7

テンプレートの使用方法

作成したテンプレートを新しいプロジェクトで使用する方法を説明します。

方法1: GitHub テンプレートリポジトリとして使用

  1. GitHub でテンプレートリポジトリに設定する

    • リポジトリの Settings > General > Template repository にチェック
  2. 新しいプロジェクトを作成する際に「Use this template」をクリック

方法2: degit を使用してクローン

# degitを使ってテンプレートをコピー
npx degit your-username/my-nextjs-template my-new-project

# プロジェクトディレクトリに移動
cd my-new-project

# 依存関係をインストール
npm install

# 開発サーバーを起動
npm run dev

方法3: Git clone でコピー

# テンプレートをクローン
git clone https://github.com/your-username/my-nextjs-template.git my-new-project

# プロジェクトディレクトリに移動
cd my-new-project

# 既存のGit履歴を削除
rm -rf .git

# 新しいGitリポジトリを初期化
git init

# 依存関係をインストール
npm install

まとめ

Next.js のプロジェクトテンプレートを作成することで、以下のメリットが得られます。

テンプレート化のメリット

  1. 開発効率の向上: 毎回同じ設定を繰り返す必要がなくなる
  2. 品質の統一: チーム全体で一貫したコードスタイルと設定を維持できる
  3. オンボーディングの簡素化: 新メンバーがすぐに開発を始められる
  4. ベストプラクティスの共有: 検証済みの設定を再利用できる

次のステップ

  • Storybook を追加してコンポーネントカタログを作成
  • Husky と lint-staged で commit 時の自動チェックを導入
  • E2E テスト(Playwright や Cypress)の追加
  • 認証機能(NextAuth.js)の統合

テンプレートは一度作って終わりではなく、プロジェクトを重ねるごとに改善していくことが重要です。チームのフィードバックを取り入れながら、より良いテンプレートに育てていきましょう。

参考文献

円