アーキテクチャ設計の意思決定
アーキテクチャ設計の意思決定
Section titled “アーキテクチャ設計の意思決定”シニアエンジニアとして、Next.jsアプリケーションの適切なアーキテクチャを選択するためには、トレードオフを理解し、コンテキストに応じた判断が必要です。この章では、Next.jsアプリケーションのアーキテクチャ設計における重要な意思決定ポイントについて深く解説します。
レンダリング戦略の選択
Section titled “レンダリング戦略の選択”SSR vs SSG vs CSR vs ISR の判断
Section titled “SSR vs SSG vs CSR vs ISR の判断”各戦略の本質的な違い:
// 1. SSR (Server-Side Rendering)// リクエストごとにサーバーでHTMLを生成export default async function Page() { const data = await fetch('https://api.example.com/data', { cache: 'no-store' // キャッシュしない }); return <div>{data.title}</div>;}
// 適用範囲:// - ユーザーごとに異なるコンテンツ// - リアルタイム性が重要// - SEOが必要だが、頻繁に更新される
// トレードオフ:// - メリット: 常に最新のデータ、SEOに強い// - デメリット: サーバー負荷が高い、TTFBが遅い可能性// 2. SSG (Static Site Generation)// ビルド時にHTMLを生成export default async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: false } // キャッシュを無効化しない }); return <div>{data.title}</div>;}
// 適用範囲:// - コンテンツが静的な、または更新頻度が低い// - 最高のパフォーマンスが必要// - SEOが必要
// トレードオフ:// - メリット: 最高のパフォーマンス、サーバー負荷なし// - デメリット: 動的なコンテンツには不向き、ビルド時間が長くなる// 3. ISR (Incremental Static Regeneration)// ビルド時に生成し、定期的に再生成export default async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 3600 } // 1時間ごとに再生成 }); return <div>{data.title}</div>;}
// 適用範囲:// - 更新頻度が中程度// - パフォーマンスと最新性のバランスが必要// - 大量のページがある
// トレードオフ:// - メリット: SSGのパフォーマンス + 定期的な更新// - デメリット: 再生成までの間は古いデータが表示される可能性// 4. CSR (Client-Side Rendering)// クライアントでレンダリング'use client';
export default function Page() { const [data, setData] = useState(null);
useEffect(() => { fetch('https://api.example.com/data') .then(res => res.json()) .then(setData); }, []);
return <div>{data?.title}</div>;}
// 適用範囲:// - ユーザー固有のコンテンツ// - リアルタイム性が最重要// - SEOが不要
// トレードオフ:// - メリット: サーバー負荷が低い、インタラクティブ// - デメリット: 初期表示が遅い、SEOに弱い意思決定フレームワーク:
| 観点 | SSR | SSG | ISR | CSR |
|---|---|---|---|---|
| 更新頻度 | リアルタイム | 低い | 中程度 | リアルタイム |
| パフォーマンス | 中 | 最高 | 高 | 低(初期) |
| SEO | 強い | 強い | 強い | 弱い |
| サーバー負荷 | 高い | なし | 低い | 低い |
| ユーザー固有 | 可能 | 困難 | 困難 | 可能 |
実践的な選択例:
// 例1: ブログ記事 → SSG// 理由: 更新頻度が低く、SEOが重要、パフォーマンス優先export async function generateStaticParams() { const posts = await getPosts(); return posts.map(post => ({ slug: post.slug }));}
// 例2: ダッシュボード → SSR// 理由: ユーザーごとに異なる、リアルタイム性が必要export default async function Dashboard() { const user = await getCurrentUser(); const data = await getUserData(user.id); return <DashboardContent data={data} />;}
// 例3: 商品一覧 → ISR// 理由: 更新頻度が中程度、大量のページ、パフォーマンス重要export default async function Products() { const products = await fetch('https://api.example.com/products', { next: { revalidate: 3600 } // 1時間ごとに更新 }); return <ProductList products={products} />;}
// 例4: 管理画面 → CSR// 理由: SEO不要、リアルタイム性が最重要、認証が必要'use client';export default function AdminPanel() { // クライアントサイドでレンダリング}サーバーコンポーネント vs クライアントコンポーネント
Section titled “サーバーコンポーネント vs クライアントコンポーネント”境界の設計判断
Section titled “境界の設計判断”判断基準:
// サーバーコンポーネントを選ぶべき場合:// 1. データフェッチが必要// 2. 機密情報へのアクセス(APIキー、データベース)// 3. 大きな依存関係をサーバー側に保持// 4. SEOが重要
// app/products/page.tsx(サーバーコンポーネント)export default async function ProductsPage() { // サーバー側でデータを取得 const products = await getProducts();
return ( <div> {products.map(product => ( <ProductCard key={product.id} product={product} /> ))} </div> );}
// クライアントコンポーネントを選ぶべき場合:// 1. インタラクティブな機能(onClick、onChange)// 2. ブラウザAPIの使用(localStorage、window)// 3. 状態管理(useState、useEffect)// 4. イベントリスナー
// components/AddToCartButton.tsx(クライアントコンポーネント)'use client';
export default function AddToCartButton({ productId }: { productId: string }) { const [isAdding, setIsAdding] = useState(false);
const handleClick = async () => { setIsAdding(true); await addToCart(productId); setIsAdding(false); };
return ( <button onClick={handleClick} disabled={isAdding}> {isAdding ? '追加中...' : 'カートに追加'} </button> );}境界の設計パターン:
// パターン1: サーバーコンポーネントがデータを取得し、クライアントコンポーネントに渡すexport default async function ProductPage({ params }: { params: { id: string } }) { const product = await getProduct(params.id);
// サーバーコンポーネントからクライアントコンポーネントにpropsで渡す return ( <div> <ProductDetails product={product} /> {/* サーバーコンポーネント */} <AddToCartButton productId={product.id} /> {/* クライアントコンポーネント */} </div> );}
// パターン2: クライアントコンポーネントが必要なデータを取得// 問題のあるパターン: サーバーコンポーネントの境界を越えてデータを取得'use client';export default function ProductList() { const [products, setProducts] = useState([]);
useEffect(() => { // 問題: クライアント側でデータを取得(SSRの利点を失う) fetch('/api/products').then(res => res.json()).then(setProducts); }, []);
return <div>{/* ... */}</div>;}
// 解決: サーバーコンポーネントでデータを取得export default async function ProductList() { const products = await getProducts(); // サーバー側で取得
return ( <ClientProductList products={products} /> // クライアントコンポーネントに渡す );}状態管理の設計判断
Section titled “状態管理の設計判断”状態管理ライブラリの選択
Section titled “状態管理ライブラリの選択”判断基準:
// 1. サーバー状態 vs クライアント状態
// サーバー状態: React Query / SWR// - APIから取得したデータ// - キャッシュ、再取得、同期が必要import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) { const { data, isLoading } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), staleTime: 5 * 60 * 1000, // 5分間は新鮮とみなす });
if (isLoading) return <Loading />; return <div>{data.name}</div>;}
// クライアント状態: Zustand / Jotai / Redux// - UIの状態(モーダルの開閉、フォームの入力値)// - グローバルなUI状態import { create } from 'zustand';
interface CartStore { items: CartItem[]; addItem: (item: CartItem) => void;}
const useCartStore = create<CartStore>((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })),}));状態管理ライブラリの選択判断:
| ライブラリ | 適用範囲 | 学習コスト | ボイラープレート | デバッグ |
|---|---|---|---|---|
| React Query | サーバー状態 | 低い | 少ない | 容易 |
| Zustand | クライアント状態 | 非常に低い | 非常に少ない | 容易 |
| Redux | 複雑な状態管理 | 高い | 多い | 優秀 |
| Jotai | アトミックな状態 | 低い | 少ない | 容易 |
実践的な選択例:
// 小規模プロジェクト: Zustand + React Query// - シンプルで学習コストが低い// - ボイラープレートが少ない
// 中規模プロジェクト: Zustand + React Query + Context API// - 機能ごとに状態を分離// - グローバル状態はZustand、機能固有はContext
// 大規模プロジェクト: Redux Toolkit + React Query// - 厳格な状態管理が必要// - タイムトラベルデバッグが必要// - 複雑な状態遷移があるデータフェッチング戦略
Section titled “データフェッチング戦略”データフェッチングパターンの選択
Section titled “データフェッチングパターンの選択”パターン1: サーバーコンポーネントでのフェッチ(推奨)
// メリット: サーバー側で実行、SEOに強い、パフォーマンスが良いexport default async function ProductsPage() { const products = await fetch('https://api.example.com/products', { next: { revalidate: 3600 } }).then(res => res.json());
return <ProductList products={products} />;}パターン2: Route Handlerでのフェッチ
export async function GET() { const products = await getProductsFromDB(); return Response.json(products);}
// メリット: APIキーを隠蔽できる、データ変換が可能// デメリット: 追加のネットワークリクエストパターン3: クライアントコンポーネントでのフェッチ
'use client';export default function ProductsPage() { const { data } = useQuery({ queryKey: ['products'], queryFn: () => fetch('/api/products').then(res => res.json()), });
return <ProductList products={data} />;}
// メリット: リアルタイム更新、キャッシュ制御// デメリット: 初期表示が遅い、SEOに弱い判断基準:
// サーバーコンポーネントでフェッチを選ぶべき場合:// 1. 初回表示が重要// 2. SEOが必要// 3. データがユーザー固有でない
// Route Handlerでフェッチを選ぶべき場合:// 1. APIキーを隠蔽する必要がある// 2. データ変換が必要// 3. 複数のデータソースを統合
// クライアントコンポーネントでフェッチを選ぶべき場合:// 1. リアルタイム更新が必要// 2. ユーザー操作に応じてデータを取得// 3. SEOが不要キャッシング戦略の選択
Section titled “キャッシング戦略の選択”Next.jsのキャッシングレイヤー
Section titled “Next.jsのキャッシングレイヤー”キャッシングの階層:
1. Request Memoization (同一リクエスト内での重複リクエストを防ぐ)2. Data Cache (fetchの結果をキャッシュ)3. Full Route Cache (レンダリング結果をキャッシュ)4. Router Cache (クライアント側のルーターキャッシュ)実践的な設定:
// 1. 常に最新のデータが必要 → キャッシュしないexport default async function Dashboard() { const data = await fetch('https://api.example.com/dashboard', { cache: 'no-store' // キャッシュしない }); return <Dashboard data={data} />;}
// 2. 定期的に更新 → ISRexport default async function Products() { const data = await fetch('https://api.example.com/products', { next: { revalidate: 3600 } // 1時間ごとに再検証 }); return <ProductList products={data} />;}
// 3. ビルド時に生成 → SSGexport default async function BlogPost({ params }: { params: { slug: string } }) { const post = await fetch(`https://api.example.com/posts/${params.slug}`, { next: { revalidate: false } // キャッシュを無効化しない }); return <Article post={post} />;}
// 4. 動的キャッシュ → タグベースexport default async function Product({ params }: { params: { id: string } }) { const product = await fetch(`https://api.example.com/products/${params.id}`, { next: { revalidate: 3600, tags: ['products'] // タグでキャッシュを管理 } }); return <ProductDetails product={product} />;}
// タグベースの再検証// app/api/revalidate/route.tsexport async function POST(request: Request) { const { tag } = await request.json(); revalidateTag(tag); // 特定のタグのキャッシュを無効化 return Response.json({ revalidated: true });}Next.jsアプリケーションのアーキテクチャ設計において重要なポイント:
- レンダリング戦略: SSR、SSG、ISR、CSRの使い分け
- サーバー/クライアントコンポーネント: 適切な境界の設計
- 状態管理: サーバー状態とクライアント状態の分離
- データフェッチング: 最適なフェッチパターンの選択
- キャッシング: 多層的なキャッシング戦略の活用
シニアエンジニアとして考慮すべき点:
- コンテキストに応じた選択: プロジェクトの要件に応じて最適な戦略を選択
- トレードオフの理解: パフォーマンス、SEO、開発体験のバランス
- 計測に基づく判断: 実際のパフォーマンスデータに基づいて最適化
- 長期的な保守性: 短期的な最適化ではなく、長期的に保守可能な設計
適切なアーキテクチャの選択は、アプリケーションの成功に大きく影響します。コンテキストを理解し、トレードオフを分析した上で、最適な選択を行いましょう。