Next.js: Server Actionsでのデータ取得について

に公開
3

Next.js 13以降で導入されたServer Actionsは、クライアントからのインタラクションに応じてサーバーサイドの関数を実行できる機能です。これにより、従来必要だったAPIエンドポイントの作成なしにサーバーとのやり取りが可能となり、特定のユースケースにおいて開発効率の向上が期待できます。

しかし、Server Actionsをデータ取得(クエリ)目的で使用することについては、いくつかの考慮すべき点が存在します。本記事では、データ取得にServer Actionsを利用する際の主な注意点と、Next.jsにおける代替的なデータ取得戦略について解説します。

Server Actionsの設計思想とデータ取得

Server Actionsは、主にミューテーション(データの作成、更新、削除など、サーバーの状態を変更する操作)を簡素化することを意図して設計されています。

  • HTTPメソッド: 内部的にHTTP POSTリクエストを使用します。これは一般的に状態変更を伴う操作に用いられるメソッドであり、冪等性が保証されず、キャッシュの利用にも制約があります。データ取得に通常用いられるGETメソッドとは特性が異なります。
  • 関心の分離: Web開発においては、読み取り操作(クエリ)と変更操作(ミューテーション)を分離することが、コードの明確性や保守性の観点から推奨される場合があります。Server Actionsをデータ取得に利用すると、この境界が曖昧になる可能性があります。
// app/actions.ts
'use server';
import { db } from '@/lib/db'; // Prisma Clientなどのインポートを想定

// Server Actionsをデータ取得に使う例
export async function getItemData_NotRecommended(id: string) {
  console.warn("Warning: Using Server Action for data fetching is generally not recommended.");
  try {
    const data = await db.item.findUnique({ where: { id } });
    return data;
  } catch (error) {
    console.error("Failed to fetch data via Server Action:", error);
    return null;
  }
}

// Client Component からの呼び出し例 (※非推奨)
import { getItemData_NotRecommended } from '@/app/actions';

async function someFunction(itemId: string) {
  const data = await getItemData_NotRecommended(itemId);
  console.log(data);
}

これらの設計上の特性が、データ取得ユースケースにおける以下の考慮事項につながります。

Server Actionsによるデータ取得:検討すべき技術的・設計的側面

Server Actionsは魅力的な機能ですが、データ取得という特定の用途においては、その仕組みや設計思想に起因するいくつかの側面を慎重に評価する必要があります。これらはパフォーマンス、開発効率、そしてアプリケーションの将来性に影響を与える可能性があります。

1. 技術的な制約と効率性の問題点

Server Actionsの内部的な動作メカニズムは、データ取得処理において非効率性を生む可能性があります。

  • 逐次実行によるボトルネック: Server Actionsの呼び出しは、特に複数同時に行われた場合、順番に処理される傾向があります。これは、本来並列で実行可能なデータ取得リクエストが直列化し、全体の処理時間を不必要に長引かせる原因となり得ます(ウォーターフォール現象)。結果として、ユーザーが体感するページの表示速度が低下する可能性があります。
  • キャッシュ機構の適用外: Webパフォーマンスの要であるキャッシュ戦略が、Server Actionsには適用しにくいという制約があります。POSTリクエストが基本であるため、GETリクエストに対する標準的なHTTPキャッシュ(ブラウザ、CDN)は機能しません。また、Next.jsがFetch APIに対して提供するリクエスト単位のキャッシュや重複排除の仕組みも、Server Actionsの呼び出しには適用されません。これにより、同一データへのアクセスが繰り返されても、毎回サーバー処理が実行される非効率が生じます。
  • ネットワークオーバーヘッドの発生: クライアントからServer Actionを呼び出す際には、必ずクライアント・サーバー間のネットワーク通信が発生します。これには、通信遅延だけでなく、データの送受信に伴うシリアライズ・デシリアライズのコストも含まれます。Server Components内でデータを取得する場合など、本来この通信が不要なケースと比較すると、明らかなオーバーヘッドとなります。
  • データベース直接アクセスの限界: たとえServer Action内で直接データベースにアクセスする場合でも、これらの問題が完全に解消されるわけではありません。クライアントからのアクション呼び出しに伴う最初のネットワーク通信は避けられず、アクション呼び出し自体はキャッシュされないため、データベースへのアクセスが重複する可能性も残ります。

2. 設計原則との不整合

ソフトウェア設計やWebの標準的な考え方との間に、いくつかのギャップが存在します。

  • クエリとミューテーションの役割: アプリケーションの堅牢性や予測可能性を高めるためには、データを読み取る操作(クエリ)とデータを変更する操作(ミューテーション)を明確に区別することが推奨されます。Server Actionsは主にミューテーションのために設計されており、「アクション」という名称もそれを反映しています。これをクエリに転用することは、役割の境界を曖昧にし、コードの意図を不明瞭にする可能性があります。
  • HTTPセマンティクスとの乖離: Webの基本的な通信プロトコルであるHTTPでは、GETメソッドがデータの取得、POSTメソッドがデータの送信や状態変更という役割分担(セマンティクス)が定められています。Server ActionsはPOSTを使用するため、これをデータ取得に使うことは、この標準的なセマンティクスから逸脱します。この逸脱は、キャッシュ利用の制約など、具体的な技術的課題にもつながっています。

3. 開発・運用上の具体的な困難

上記の技術的制約や設計上の不整合は、実際の開発や運用において、以下のような具体的な困難を引き起こす可能性があります。

  • デバッグ・テストの難易度上昇: Server ActionsはNext.jsの内部的な仕組みと連携して動作するため、問題発生時の原因究明が複雑になることがあります。エラーの追跡やパフォーマンスボトルネックの特定が、通常のAPI経由の処理に比べて難しくなる可能性があります。また、テストにおいても、依存関係の管理や実行環境の準備に手間がかかることが考えられます。
  • コードの保守性低下のリスク: クエリとミューテーションの責務が混在すると、コードの可読性が低下し、将来的な変更や機能追加が困難になるリスクがあります。特定の関数が何をするのか、副作用はあるのかといった点が不明確になりがちです。
  • 将来的な柔軟性の欠如: データ取得ロジックをServer Actionsに強く依存させると、Next.js/React環境以外での再利用が難しくなり、技術的なロックインを招く可能性があります。将来的なアーキテクチャ変更やシステム移行の際の制約となることも考慮すべき点です。
  • チーム開発とスケーリングの課題: 役割分担が不明確になることで、チーム内での認識齟齬や開発効率の低下を招く可能性があります。また、アプリケーションが大規模化するにつれて、パフォーマンスや保守性の問題が顕在化し、システム全体のスケーラビリティを損なう恐れがあります。

これらの側面を総合的に評価すると、Server Actionsは強力なツールである一方で、データ取得という用途においては、他のより適切な手段を検討することが望ましいと言えます。

Next.jsにおける代替データ取得戦略

Next.jsでは、データ取得のための複数のアプローチが提供されています。Server Actionsの代替として、以下の方法が利用可能です。

  1. Server Componentsでのデータ取得:

    • App Routerにおける基本的なデータ取得方法です。
    • async/awaitを用いてコンポーネント内で直接データフェッチを行います。
    • サーバーサイドレンダリング(SSR)または静的サイト生成(SSG)時にデータが取得され、初期ロードパフォーマンスに優れます。
    • Next.jsによるFetch APIのキャッシュおよびリクエスト重複排除機能を利用可能です。
    • 主な利用シーン: ページの初期表示に必要なデータ、SEOが重要なコンテンツ。
    // app/items/[id]/page.tsx (Server Component)
    import { db } from '@/lib/db';
    import ItemDetailView from '@/components/ItemDetailView'; // 表示用コンポーネント (仮)
    
    async function getItemData(id: string) {
      try {
        // Server Component内でDBアクセスやAPI呼び出し
        const data = await db.item.findUnique({ where: { id } });
        // fetchならNext.jsキャッシュ活用可: await fetch(url, { cache: 'force-cache' });
        return data;
      } catch (error) {
        console.error(`Failed to fetch data for item ${id} in Server Component:`, error);
        return null;
      }
    }
    
    export default async function ItemPage({ params }: { params: { id: string } }) {
      const item = await getItemData(params.id);
    
      if (!item) {
        return <div>Item not found or failed to load.</div>;
      }
      // 取得データをコンポーネントに渡す
      return <ItemDetailView item={item} />;
    }
    
  2. Route Handlers (API Routes):

    • クライアントサイドからの動的なデータ取得要求に対応するためのAPIエンドポイントを作成します(例: app/api/.../route.ts)。
    • クライアントからはFetch APIやデータ取得ライブラリを用いて、主にGETメソッドでアクセスします。
    • HTTPメソッドの適切な使用、キャッシュヘッダー(Cache-Control)による細やかな制御が可能です。
    • APIとして独立しているため、テストや監視が容易です。
    • 主な利用シーン: ユーザー操作に応じたデータ取得・更新、外部提供用API。
    // app/api/items/[id]/route.ts
    import { db } from '@/lib/db';
    import { NextResponse } from 'next/server';
    
    export async function GET(
      request: Request,
      { params }: { params: { id: string } }
    ) {
      try {
        const item = await db.item.findUnique({ where: { id: params.id } });
    
        if (!item) {
          return NextResponse.json({ message: 'Item not found' }, { status: 404 });
        }
        // 必要ならキャッシュヘッダー等を設定
        // return NextResponse.json(item, { headers: { 'Cache-Control': 'public, max-age=3600' } });
        return NextResponse.json(item);
      } catch (error) {
        console.error(`API Route error for item ${params.id}:`, error);
        return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
      }
    }
    
    
    // Client Componentからの呼び出し例
    useEffect(() => {
      fetch(`/api/items/${itemId}`)
        .then(res => {
          if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
          return res.json();
        })
        .then(data => { /* データ処理 */ })
        .catch(error => { /* エラー処理 */ });
    }, [itemId]);
    
  3. クライアントサイドデータ取得ライブラリ (SWR / TanStack Query):

    • クライアントサイドでのデータキャッシュ、状態管理、自動再検証、ローディング/エラー状態のハンドリングなどを高度に行うためのライブラリです。
    • Route Handlersと組み合わせて使用されることが多いです。
    • 複雑なクライアントの状態管理を宣言的に記述でき、開発体験を向上させます。
    • 主な利用シーン: 頻繁に更新されるデータ、複数コンポーネント間でのデータ共有、高度なUI/UX(オプティミスティックUIなど)。
    // Client ComponentでのSWR利用例
    'use client';
    
    import useSWR from 'swr';
    
    // APIからデータを取得する関数
    const fetcher = async (url: string) => {
      const res = await fetch(url);
      if (!res.ok) {
        const errorInfo = await res.json().catch(() => ({ message: res.statusText }));
        throw new Error(errorInfo.message || 'An error occurred while fetching the data.');
      }
      return res.json();
    };
    
    function ItemDisplay({ itemId }: { itemId: string }) {
      // useSWRフックでデータ取得を実行
      const { data: item, error, isLoading } = useSWR(`/api/items/${itemId}`, fetcher);
    
      if (error) return <div>Error loading item: {error.message}</div>;
      if (isLoading) return <div>Loading item...</div>;
      if (!item) return <div>Item not found.</div>;
    
      // 取得したデータを使ってUIをレンダリング
      return (
        <div>
          <h2>{item.name}</h2>
          <p>{item.description || 'No description available.'}</p>
        </div>
      );
    }
    
    export default ItemDisplay;
    
  4. その他の方法:

    • Pages Router: getServerSideProps, getStaticProps が引き続き利用可能です。
    • Parallel / Intercepting Routes: 特定のUIパターン(モーダル、タブなど)における遅延読み込みや事前読み込みに活用できます。

これらの代替手段は、データ取得というタスクに対し、Web標準や関心の分離の原則に沿った、より予測可能で管理しやすいアプローチを提供します。

まとめ

Server Actionsは、Next.jsにおいて特にミューテーション処理を簡潔に実装するための有効な機能です。

一方で、データ取得の目的でServer Actionsを利用する際には、パフォーマンス、キャッシュ、保守性などの観点からいくつかの考慮事項が存在します。Next.jsが提供するServer Components、Route Handlers、クライアントサイドライブラリといった代替的なデータ取得戦略を理解し、ユースケースに応じて適切な方法を選択することが、堅牢でスケーラブルなアプリケーション構築のために重要となります。

それぞれの技術の特性とトレードオフを考慮し、プロジェクトの要件に最も適したデータアクセスパターンを採用することが重要と考えます。

Discussion

Honey32Honey32

Workspace API というものは耳にしたことが無いですが、どういったものですか…?

akkyakky

ご指摘ありがとうございます。こちらFetchAPIの誤記になります🙇‍♀️
(生成AIでの清書で誤りが発生してしまいました)