目次
- TypeScript型システム詳解
- ユーティリティ型完全攻略
- 型ガードと型の絞り込み
- Next.js 15 App Router
- Server Components vs Client Components
- Server Actions
- データフェッチング戦略
- ミドルウェア
- デプロイ戦略
- パフォーマンス最適化
1. TypeScript型システム詳解
TypeScriptは単にJavaScriptに型を追加したものを超え、強力な型レベルプログラミング言語です。この章では、実務でよく使われる高度な型パターンを解説します。
1.1 ユニオン型とインターセクション型
ユニオン型は複数の型のいずれかを表し、インターセクション型は複数の型をすべて満たす型を表します。
// ユニオン型: 複数の型のいずれか
type Status = 'idle' | 'loading' | 'success' | 'error'
type ApiResponse =
| { status: 'success'; data: unknown }
| { status: 'error'; message: string }
// インターセクション型: 複数の型を結合
type Timestamped = { createdAt: Date; updatedAt: Date }
type SoftDeletable = { deletedAt: Date | null }
type BaseEntity = Timestamped & SoftDeletable
interface User extends BaseEntity {
id: string
name: string
email: string
}
判別可能なユニオン(Discriminated Union)は共通フィールド(判別子)を使って型を区別するパターンで、複雑な状態管理に非常に有用です。
// Discriminated Unionパターン
type Action =
| { type: 'SET_USER'; payload: User }
| { type: 'SET_ERROR'; payload: string }
| { type: 'RESET' }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_USER':
// action.payloadは自動的にUser型
return { ...state, user: action.payload, error: null }
case 'SET_ERROR':
// action.payloadは自動的にstring型
return { ...state, error: action.payload }
case 'RESET':
return initialState
}
}
1.2 ジェネリクス (Generics)
ジェネリクスは型をパラメータ化して再利用可能なコンポーネントを作るための中核ツールです。
// 基本的なジェネリック関数
function identity<T>(value: T): T {
return value
}
// ジェネリック制約
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
// ジェネリックインターフェース
interface Repository<T extends { id: string }> {
findById(id: string): Promise<T | null>
findAll(): Promise<T[]>
create(data: Omit<T, 'id'>): Promise<T>
update(id: string, data: Partial<T>): Promise<T>
delete(id: string): Promise<void>
}
// ジェネリッククラス
class ApiClient<TResponse> {
constructor(private baseUrl: string) {}
async get(path: string): Promise<TResponse> {
const response = await fetch(`${this.baseUrl}${path}`)
return response.json() as Promise<TResponse>
}
}
1.3 条件付き型 (Conditional Types)
条件付き型は入力型に応じて出力型が決定される強力なパターンです。
// 基本的な条件付き型
type IsString<T> = T extends string ? true : false
type A = IsString<string> // true
type B = IsString<number> // false
// inferキーワードで型を抽出
type UnpackPromise<T> = T extends Promise<infer U> ? U : T
type Resolved = UnpackPromise<Promise<string>> // string
// 配列要素の型を抽出
type ElementOf<T> = T extends (infer E)[] ? E : never
type Item = ElementOf<string[]> // string
// 関数の戻り値型を抽出
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
1.4 マッピング型 (Mapped Types)
既存の型を変換して新しい型を作成します。
// すべてのプロパティを読み取り専用に
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
// すべてのプロパティをオプショナルに
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
// 特定のキーだけ選択
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
// 実践例: フォーム状態型の自動生成
type FormFields<T> = {
[K in keyof T]: {
value: T[K]
error: string | null
touched: boolean
}
}
interface LoginData {
email: string
password: string
}
type LoginForm = FormFields<LoginData>
// 結果:
// {
// email: { value: string; error: string | null; touched: boolean }
// password: { value: string; error: string | null; touched: boolean }
// }
1.5 テンプレートリテラル型 (Template Literal Types)
文字列リテラル型を組み合わせて新しい文字列型を生成します。
// 基本的なテンプレートリテラル型
type EventName = `on${Capitalize<string>}`
// 具体的な組み合わせを生成
type Color = 'red' | 'blue' | 'green'
type Size = 'sm' | 'md' | 'lg'
type ClassName = `${Color}-${Size}`
// 'red-sm' | 'red-md' | 'red-lg' | 'blue-sm' | ...
// CSSプロパティの自動生成
type CSSProperty = 'margin' | 'padding'
type Direction = 'top' | 'right' | 'bottom' | 'left'
type SpacingProp = `${CSSProperty}-${Direction}`
// 'margin-top' | 'margin-right' | ... | 'padding-left'
// API ルートの型安全性
type ApiRoutes = `/api/${'users' | 'posts' | 'comments'}`
type ApiRouteWithId = `${ApiRoutes}/${string}`
2. ユーティリティ型完全攻略
TypeScriptはよく使われる型変換のための組み込みユーティリティ型を提供しています。
2.1 基本ユーティリティ型
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
createdAt: Date
}
// Partial: すべてのプロパティをオプショナルに
type UpdateUserDto = Partial<User>
// Required: すべてのプロパティを必須に
type StrictUser = Required<User>
// Pick: 特定のプロパティだけ選択
type UserPreview = Pick<User, 'id' | 'name'>
// Omit: 特定のプロパティを除外
type CreateUserDto = Omit<User, 'id' | 'createdAt'>
// Record: キーと値のペア型
type UserMap = Record<string, User>
type RolePermissions = Record<User['role'], string[]>
2.2 高度なユーティリティ型
// ReturnType: 関数の戻り値型を抽出
function createUser(name: string, email: string) {
return { id: crypto.randomUUID(), name, email, createdAt: new Date() }
}
type CreatedUser = ReturnType<typeof createUser>
// Parameters: 関数のパラメータ型を抽出
type CreateUserParams = Parameters<typeof createUser>
// [name: string, email: string]
// Awaited: Promiseをアンラップ
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`)
return res.json()
}
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>
// User
// ExtractとExclude
type Status = 'active' | 'inactive' | 'pending' | 'banned'
type ActiveStatus = Extract<Status, 'active' | 'pending'>
// 'active' | 'pending'
type InactiveStatus = Exclude<Status, 'active' | 'pending'>
// 'inactive' | 'banned'
// NonNullable
type MaybeUser = User | null | undefined
type DefiniteUser = NonNullable<MaybeUser>
// User
2.3 カスタムユーティリティ型の作成
// DeepPartial: ネストされたプロパティもすべてオプショナルに
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// DeepReadonly: ネストされたプロパティもすべて読み取り専用に
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}
// RequiredKeys: 特定のキーだけ必須に
type RequiredKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
type UserWithRequiredEmail = RequiredKeys<Partial<User>, 'email'>
// Prettify: 型を展開して読みやすく
type Prettify<T> = {
[K in keyof T]: T[K]
} & {}
3. 型ガードと型の絞り込み
ランタイムで型を安全に絞り込む方法です。
3.1 typeof ガード
function formatValue(value: string | number | boolean): string {
if (typeof value === 'string') {
return value.toUpperCase()
}
if (typeof value === 'number') {
return value.toFixed(2)
}
return value ? 'Yes' : 'No'
}
3.2 instanceof ガード
class ApiError extends Error {
constructor(
message: string,
public statusCode: number
) {
super(message)
}
}
class ValidationError extends Error {
constructor(
message: string,
public fields: Record<string, string>
) {
super(message)
}
}
function handleError(error: unknown): string {
if (error instanceof ApiError) {
return `API Error ${error.statusCode}: ${error.message}`
}
if (error instanceof ValidationError) {
return `Validation Error: ${Object.values(error.fields).join(', ')}`
}
if (error instanceof Error) {
return error.message
}
return 'Unknown error'
}
3.3 in演算子ガード
interface Dog {
bark(): void
breed: string
}
interface Cat {
meow(): void
color: string
}
function makeSound(animal: Dog | Cat): void {
if ('bark' in animal) {
animal.bark()
} else {
animal.meow()
}
}
3.4 ユーザー定義型ガード (isキーワード)
// カスタム型ガード
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
)
}
// 配列フィルタリングで型ガードを活用
const items: (User | null)[] = [user1, null, user2, null]
const users: User[] = items.filter((item): item is User => item !== null)
3.5 satisfies演算子
TypeScript 4.9で導入されたsatisfiesは、型チェックと型推論を同時にサポートします。
type Route = {
path: string
element: React.ReactNode
}
// satisfiesを使うと型チェックしながらリテラル型を維持
const routes = {
home: { path: '/', element: '<Home />' },
about: { path: '/about', element: '<About />' },
blog: { path: '/blog', element: '<Blog />' },
} satisfies Record<string, Route>
// routes.home.pathの型は '/' (リテラル)
// as constを使うとreadonlyになり変更不可
type ColorConfig = Record<string, string | string[]>
const palette = {
primary: '#007bff',
secondary: ['#6c757d', '#adb5bd'],
danger: '#dc3545',
} satisfies ColorConfig
// palette.primaryはstring型
// palette.secondaryはstring[]型 (配列メソッド利用可能)
4. Next.js 15 App Router
Next.js 15のApp Routerは、React Server Componentsをベースとした新しいルーティングシステムです。
4.1 appディレクトリ構造
App Routerはファイルシステムベースのルーティングを使用します。各フォルダはURLセグメントに対応し、特殊なファイル名がそれぞれの役割を担います。
app/
layout.tsx # ルートレイアウト (必須)
page.tsx # ホームページ (/)
loading.tsx # ローディングUI
error.tsx # エラーUI
not-found.tsx # 404 UI
global-error.tsx # グローバルエラーUI
blog/
page.tsx # /blog
[slug]/
page.tsx # /blog/some-post
loading.tsx # このルートのローディング
dashboard/
layout.tsx # ダッシュボードレイアウト
page.tsx # /dashboard
settings/
page.tsx # /dashboard/settings
(marketing)/ # ルートグループ (URLに影響なし)
about/
page.tsx # /about
contact/
page.tsx # /contact
api/
users/
route.ts # APIルート
4.2 LayoutとPage
// app/layout.tsx - ルートレイアウト
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
default: 'My App',
template: '%s | My App',
},
description: 'A modern full-stack application',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ja">
<body>
<header>
<nav>{/* ナビゲーション */}</nav>
</header>
<main>{children}</main>
<footer>{/* フッター */}</footer>
</body>
</html>
)
}
// app/blog/[slug]/page.tsx - 動的ルート
import { notFound } from 'next/navigation'
interface PageProps {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: PageProps) {
const { slug } = await params
const post = await getPost(slug)
if (!post) return {}
return {
title: post.title,
description: post.summary,
}
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params
const post = await getPost(slug)
if (!post) {
notFound()
}
return (
<article>
<h1>{post.title}</h1>
<time dateTime={post.date}>{post.date}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
// 静的生成するパスを指定
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
4.3 Loading、Error、Not Found
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-3/4 mb-4" />
<div className="h-4 bg-gray-200 rounded w-full mb-2" />
<div className="h-4 bg-gray-200 rounded w-5/6" />
</div>
)
}
// app/blog/error.tsx
'use client' // Errorコンポーネントは必ずクライアントコンポーネント
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
}, [error])
return (
<div>
<h2>問題が発生しました</h2>
<button onClick={() => reset()}>再試行</button>
</div>
)
}
// app/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div>
<h2>ページが見つかりません</h2>
<p>リクエストされたページは存在しません。</p>
<Link href="/">ホームに戻る</Link>
</div>
)
}
5. Server Components vs Client Components
Next.js 15で最も重要な概念の一つは、サーバーコンポーネントとクライアントコンポーネントの区別です。
5.1 Server Components (デフォルト)
App Routerではすべてのコンポーネントがデフォルトでサーバーコンポーネントです。サーバーでのみ実行されるため、バンドルサイズに含まれません。
// サーバーコンポーネント - デフォルト
// async関数で直接データをフェッチ可能
import { db } from '@/lib/db'
export default async function UserList() {
// DBに直接アクセス可能 (サーバーでのみ実行)
const users = await db.user.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
})
return (
<ul>
{users.map((user) => (
<li key={user.id}>
<span>{user.name}</span>
<span>{user.email}</span>
</li>
))}
</ul>
)
}
5.2 Client Components
インタラクティブな操作が必要な場合、ファイルの先頭に'use client'を宣言します。
'use client'
import { useState, useTransition } from 'react'
interface SearchProps {
onSearch: (query: string) => Promise<void>
}
export default function SearchBar({ onSearch }: SearchProps) {
const [query, setQuery] = useState('')
const [isPending, startTransition] = useTransition()
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
startTransition(() => {
onSearch(query)
})
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="検索ワードを入力..."
disabled={isPending}
/>
<button type="submit" disabled={isPending}>
{isPending ? '検索中...' : '検索'}
</button>
</form>
)
}
5.3 境界設定パターン
サーバーコンポーネントとクライアントコンポーネントの組み合わせパターンです。
// サーバーコンポーネントがクライアントコンポーネントをラップするパターン
// app/dashboard/page.tsx (サーバーコンポーネント)
import { db } from '@/lib/db'
import DashboardClient from './DashboardClient'
export default async function DashboardPage() {
const data = await db.analytics.getOverview()
return <DashboardClient initialData={data} />
}
// app/dashboard/DashboardClient.tsx (クライアントコンポーネント)
'use client'
import { useState } from 'react'
interface Props {
initialData: AnalyticsOverview
}
export default function DashboardClient({ initialData }: Props) {
const [data, setData] = useState(initialData)
const [timeRange, setTimeRange] = useState('7d')
return (
<div>
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
>
<option value="7d">7日間</option>
<option value="30d">30日間</option>
<option value="90d">90日間</option>
</select>
<Chart data={data} />
</div>
)
}
サーバーコンポーネントとクライアントコンポーネントの選択基準は以下の通りです。
サーバーコンポーネントを使う場合:
- データをフェッチする時
- バックエンドリソースに直接アクセスする時
- 機密情報(APIキー、トークン)を使用する時
- 大きな依存関係をサーバー側に保持したい時
クライアントコンポーネントを使う場合:
- インタラクション(イベントリスナー)が必要な時
- useState、useEffectなどのフックを使用する時
- ブラウザ専用APIを使用する時
- カスタムフックに状態が含まれる時
6. Server Actions
Server Actionsはサーバーで実行される非同期関数で、フォーム送信とデータ変更を処理します。
6.1 基本的なServer Action
// app/actions/user.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'
import { db } from '@/lib/db'
const CreateUserSchema = z.object({
name: z.string().min(2, '名前は2文字以上必要です'),
email: z.string().email('有効なメールアドレスを入力してください'),
role: z.enum(['admin', 'user']),
})
export async function createUser(formData: FormData) {
const rawData = {
name: formData.get('name'),
email: formData.get('email'),
role: formData.get('role'),
}
const validatedData = CreateUserSchema.safeParse(rawData)
if (!validatedData.success) {
return {
errors: validatedData.error.flatten().fieldErrors,
}
}
await db.user.create({ data: validatedData.data })
revalidatePath('/users')
redirect('/users')
}
6.2 フォームと組み合わせて使用
// app/users/new/page.tsx
import { createUser } from '@/app/actions/user'
export default function NewUserPage() {
return (
<form action={createUser}>
<div>
<label htmlFor="name">名前</label>
<input type="text" id="name" name="name" required />
</div>
<div>
<label htmlFor="email">メールアドレス</label>
<input type="email" id="email" name="email" required />
</div>
<div>
<label htmlFor="role">役割</label>
<select id="role" name="role">
<option value="user">ユーザー</option>
<option value="admin">管理者</option>
</select>
</div>
<button type="submit">ユーザー作成</button>
</form>
)
}
6.3 クライアントコンポーネントでServer Actionを使用
'use client'
import { useActionState } from 'react'
import { createUser } from '@/app/actions/user'
export default function UserForm() {
const [state, formAction, isPending] = useActionState(createUser, null)
return (
<form action={formAction}>
<input type="text" name="name" placeholder="名前" />
{state?.errors?.name && (
<p className="text-red-500">{state.errors.name}</p>
)}
<input type="email" name="email" placeholder="メールアドレス" />
{state?.errors?.email && (
<p className="text-red-500">{state.errors.email}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? '処理中...' : '作成'}
</button>
</form>
)
}
6.4 セキュリティ考慮事項
Server Actionsを使用する際に必ず守るべきセキュリティ事項です。
'use server'
import { auth } from '@/lib/auth'
import { rateLimit } from '@/lib/rate-limit'
export async function deletePost(postId: string) {
// 1. 認証確認
const session = await auth()
if (!session?.user) {
throw new Error('認証が必要です')
}
// 2. 権限確認
const post = await db.post.findUnique({ where: { id: postId } })
if (post?.authorId !== session.user.id) {
throw new Error('権限がありません')
}
// 3. レートリミット
const limiter = await rateLimit(session.user.id)
if (!limiter.success) {
throw new Error('リクエストが多すぎます。しばらくしてから再試行してください。')
}
// 4. 入力バリデーション
const validatedId = z.string().uuid().parse(postId)
// 5. 実際の操作を実行
await db.post.delete({ where: { id: validatedId } })
revalidatePath('/posts')
}
7. データフェッチング戦略
Next.js 15でのデータフェッチングは、サーバーコンポーネントと共にさらに強力になりました。
7.1 サーバーコンポーネントでのfetch
// 基本的なfetch - デフォルトでキャッシュ (Next.js 14)
// Next.js 15からはデフォルトがno-store
async function getPost(slug: string) {
const res = await fetch(
`https://api.example.com/posts/${slug}`,
{
next: {
revalidate: 3600, // 1時間ごとに再検証
tags: ['posts'], // キャッシュタグ
},
}
)
if (!res.ok) throw new Error('Failed to fetch post')
return res.json()
}
// キャッシュしないリクエスト
async function getCurrentUser() {
const res = await fetch('https://api.example.com/me', {
cache: 'no-store',
headers: {
Authorization: `Bearer ${getToken()}`,
},
})
return res.json()
}
7.2 unstable_cache (サーバー専用キャッシュ)
import { unstable_cache } from 'next/cache'
import { db } from '@/lib/db'
// DBクエリ結果をキャッシュ
const getCachedPosts = unstable_cache(
async (category: string) => {
return db.post.findMany({
where: { category, published: true },
orderBy: { createdAt: 'desc' },
take: 20,
})
},
['posts-by-category'], // キャッシュキー
{
revalidate: 600, // 10分ごとに再検証
tags: ['posts'], // 手動再検証用タグ
}
)
// 使用例
export default async function BlogPage() {
const posts = await getCachedPosts('technology')
return <PostList posts={posts} />
}
7.3 キャッシュ無効化
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
// 特定パスのキャッシュを無効化
export async function publishPost(postId: string) {
await db.post.update({
where: { id: postId },
data: { published: true },
})
// 特定パスの再検証
revalidatePath('/blog')
revalidatePath(`/blog/${postId}`)
// またはタグベースの再検証
revalidateTag('posts')
}
7.4 並列データフェッチング
// 順次実行 (遅い)
export default async function Dashboard() {
const user = await getUser()
const posts = await getPosts() // user完了後に開始
const analytics = await getAnalytics() // posts完了後に開始
return <DashboardView user={user} posts={posts} analytics={analytics} />
}
// 並列実行 (速い)
export default async function Dashboard() {
const [user, posts, analytics] = await Promise.all([
getUser(),
getPosts(),
getAnalytics(),
])
return <DashboardView user={user} posts={posts} analytics={analytics} />
}
// Suspenseを活用したストリーミング
import { Suspense } from 'react'
export default async function Dashboard() {
const user = await getUser() // 必須データ
return (
<div>
<UserProfile user={user} />
<Suspense fallback={<PostsSkeleton />}>
<PostsSection />
</Suspense>
<Suspense fallback={<AnalyticsSkeleton />}>
<AnalyticsSection />
</Suspense>
</div>
)
}
8. ミドルウェア
ミドルウェアはリクエストが完了する前にコードを実行し、認証、リダイレクト、国際化などを処理します。
8.1 基本的なミドルウェア
// middleware.ts (プロジェクトルート)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 認証トークンの確認
const token = request.cookies.get('auth-token')?.value
// 保護されたルートの確認
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('callbackUrl', request.nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
}
// レスポンスヘッダーの追加
const response = NextResponse.next()
response.headers.set('x-request-id', crypto.randomUUID())
return response
}
export const config = {
matcher: [
// 静的ファイルとAPIルートを除外
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
8.2 国際化ミドルウェア
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
const locales = ['ja', 'en', 'ko']
const defaultLocale = 'ja'
function getLocale(request: NextRequest): string {
const negotiatorHeaders: Record<string, string> = {}
request.headers.forEach((value, key) => {
negotiatorHeaders[key] = value
})
const languages = new Negotiator({
headers: negotiatorHeaders,
}).languages()
return match(languages, locales, defaultLocale)
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
// パスに既にロケールがあるか確認
const pathnameHasLocale = locales.some(
(locale) =>
pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (pathnameHasLocale) return
// 適切なロケールにリダイレクト
const locale = getLocale(request)
request.nextUrl.pathname = `/${locale}${pathname}`
return NextResponse.redirect(request.nextUrl)
}
8.3 レートリミットミドルウェア
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const rateLimit = new Map<string, { count: number; resetTime: number }>()
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api')) {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown'
const now = Date.now()
const windowMs = 60 * 1000 // 1分
const maxRequests = 60
const current = rateLimit.get(ip)
if (!current || now > current.resetTime) {
rateLimit.set(ip, { count: 1, resetTime: now + windowMs })
} else if (current.count >= maxRequests) {
return NextResponse.json(
{ error: 'Too Many Requests' },
{ status: 429 }
)
} else {
current.count++
}
}
return NextResponse.next()
}
9. デプロイ戦略
9.1 Vercelデプロイ
VercelはNext.jsの開発元であり、最も最適化されたデプロイ環境を提供します。
# Vercel CLIのインストールとデプロイ
npm i -g vercel
vercel
# プロダクションデプロイ
vercel --prod
# 環境変数の設定
vercel env add DATABASE_URL production
環境変数管理のための設定ファイル例です。
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
// 環境変数の検証
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
},
// イメージ最適化
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
},
],
},
}
export default nextConfig
9.2 Dockerデプロイ
# Dockerfile
FROM node:20-alpine AS base
# 依存関係のインストール
FROM base AS deps
WORKDIR /app
COPY package.json yarn.lock* ./
RUN yarn install --frozen-lockfile
# ビルド
FROM base AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN yarn build
# プロダクション
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY /app/public ./public
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]
// next.config.ts - standalone出力設定
const nextConfig: NextConfig = {
output: 'standalone',
}
9.3 セルフホスティング
# PM2によるプロセス管理
npm install -g pm2
# ビルド
npm run build
# PM2で実行
pm2 start npm --name "nextjs-app" -- start
# クラスターモード (CPUコア数分)
pm2 start npm --name "nextjs-app" -i max -- start
# 自動再起動設定
pm2 startup
pm2 save
Nginxリバースプロキシ設定:
# /etc/nginx/sites-available/nextjs
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# 静的ファイルのキャッシュ
location /_next/static {
proxy_pass http://localhost:3000;
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
10. パフォーマンス最適化
10.1 Image最適化
import Image from 'next/image'
// 基本的なイメージ最適化
export function HeroImage() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={630}
priority // LCPイメージに使用
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
)
}
// レスポンシブイメージ
export function ResponsiveImage() {
return (
<Image
src="/photo.jpg"
alt="Responsive photo"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover"
/>
)
}
10.2 Font最適化
// app/layout.tsx
import { Inter, Noto_Sans_JP } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
const notoSansJP = Noto_Sans_JP({
subsets: ['latin'],
weight: ['400', '500', '700'],
display: 'swap',
variable: '--font-noto-sans-jp',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html
lang="ja"
className={`${inter.variable} ${notoSansJP.variable}`}
>
<body>{children}</body>
</html>
)
}
10.3 Bundle分析
# @next/bundle-analyzerのインストール
npm install @next/bundle-analyzer
// next.config.ts
import withBundleAnalyzer from '@next/bundle-analyzer'
const nextConfig: NextConfig = {
// その他の設定
}
export default withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
})(nextConfig)
# バンドル分析の実行
ANALYZE=true npm run build
10.4 Lazy Loading
import dynamic from 'next/dynamic'
// コンポーネントの遅延ロード
const DynamicChart = dynamic(() => import('@/components/Chart'), {
loading: () => <p>チャート読み込み中...</p>,
ssr: false, // クライアントでのみレンダリング
})
// 条件付きロード
const DynamicEditor = dynamic(
() => import('@/components/RichTextEditor'),
{ ssr: false }
)
export default function PostEditor() {
const [showEditor, setShowEditor] = useState(false)
return (
<div>
<button onClick={() => setShowEditor(true)}>
エディタを開く
</button>
{showEditor && <DynamicEditor />}
</div>
)
}
10.5 総合パフォーマンスチェックリスト
プロダクションデプロイ前に確認すべきパフォーマンス最適化項目です。
イメージ:
- next/imageコンポーネントの使用
- LCPイメージにpriority属性を追加
- 適切なsizes属性の設定
- WebP/AVIFフォーマット自動変換の活用
コード分割:
- dynamic importで大きなコンポーネントを遅延ロード
- ルートごとの自動コード分割を活用
- 不要な依存関係の削除
キャッシュ:
- 適切なrevalidate値の設定
- キャッシュタグを活用したきめ細かな無効化
- CDNキャッシュの活用
サーバーコンポーネント:
- 可能な限りサーバーコンポーネントを活用
- クライアントコンポーネントの境界を最小化
- Suspenseでストリーミングレンダリングを実装
バンドル最適化:
- bundle-analyzerでバンドルサイズを分析
- tree-shakingの最適化
- barrel exportパターンに注意 (必要なものだけimport)
まとめ
TypeScriptとNext.jsは現代のWeb開発の標準となりました。TypeScriptの強力な型システムはランタイムエラーをコンパイル時にキャッチし、Next.jsのApp RouterとServer Componentsは最適なユーザーエクスペリエンスとデベロッパーエクスペリエンスを同時に提供します。
重要ポイント:
- TypeScriptのジェネリクス、条件付き型、マッピング型を活用すれば、型安全で柔軟なコードが書けます
- Server Componentsをデフォルトで使用し、インタラクションが必要な部分だけClient Componentsとして分離しましょう
- Server Actionsはフォーム処理とデータ変更の新しい標準です (認証と入力バリデーションを忘れずに)
- データフェッチングは並列化し、Suspenseを活用して段階的なレンダリングを実装しましょう
- イメージ、フォント、バンドル最適化でCore Web Vitalsを改善しましょう
このガイドで解説した内容を基に、型安全でパフォーマンスに優れたフルスタックアプリケーションを構築してください。
현재 단락 (1/925)
1. [TypeScript型システム詳解](#1-typescript型システム詳解)