はじめに
新しい 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フレームワーク | 高速なスタイリング、一貫したデザイン |
| ESLint | JavaScript/TypeScriptのリンター | コード品質の維持、バグの早期発見 |
| Prettier | コードフォーマッター | 一貫したコードスタイル |
| Jest | テストフレームワーク | ユニットテストの実行 |
| React Testing Library | UIテストライブラリ | コンポーネントテスト |
| 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 テンプレートリポジトリとして使用
-
GitHub でテンプレートリポジトリに設定する
- リポジトリの Settings > General > Template repository にチェック
-
新しいプロジェクトを作成する際に「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 のプロジェクトテンプレートを作成することで、以下のメリットが得られます。
テンプレート化のメリット
- 開発効率の向上: 毎回同じ設定を繰り返す必要がなくなる
- 品質の統一: チーム全体で一貫したコードスタイルと設定を維持できる
- オンボーディングの簡素化: 新メンバーがすぐに開発を始められる
- ベストプラクティスの共有: 検証済みの設定を再利用できる
次のステップ
- Storybook を追加してコンポーネントカタログを作成
- Husky と lint-staged で commit 時の自動チェックを導入
- E2E テスト(Playwright や Cypress)の追加
- 認証機能(NextAuth.js)の統合
テンプレートは一度作って終わりではなく、プロジェクトを重ねるごとに改善していくことが重要です。チームのフィードバックを取り入れながら、より良いテンプレートに育てていきましょう。
参考文献
- Next.js 公式ドキュメント - Next.js の公式ガイド
- TypeScript 公式ドキュメント - TypeScript の学習リソース
- Tailwind CSS 公式ドキュメント - Tailwind CSS の設定ガイド
- ESLint 公式ドキュメント - ESLint のルール設定
- Prettier 公式ドキュメント - Prettier の設定オプション
- Jest 公式ドキュメント - Jest テストフレームワーク
- Testing Library 公式ドキュメント - React Testing Library のガイド
- Docker 公式ドキュメント - Docker の基本と応用
- GitHub Actions 公式ドキュメント - CI/CD パイプラインの構築