Skip to content

✍️ 필사 모드: TypeScript & Next.js 実践ガイド — 型安全なフルスタック開発

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

目次

  1. TypeScript型システム詳解
  2. ユーティリティ型完全攻略
  3. 型ガードと型の絞り込み
  4. Next.js 15 App Router
  5. Server Components vs Client Components
  6. Server Actions
  7. データフェッチング戦略
  8. ミドルウェア
  9. デプロイ戦略
  10. パフォーマンス最適化

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 --from=deps /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 --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

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型システム詳解)

작성 글자: 0원문 글자: 22,370작성 단락: 0/925