Next.jsのチュートリアルをやってみる【データの取得編】

前回

Next.jsのチュートリアルをやってみる【データベースの設定編】

はじめに

前回はデータベースの作成と初期データの追加をいたしました。今回は、アプリケーションのデータを取得する方法について学習していきます。

データの取得方法

APIレイヤー

APIはアプリケーションコードとデータベースの仲介レイヤーです。APIを利用するケースは以下の通りです。

  • APIを提供するサードパーティーのサービスを利用する場合
  • クライアントからデータを取得する場合にデータベースの秘密がクライアントに公開されるのを避けるために、サーバー上で動作するAPIレイヤーを用意したい場合

Next.jsではルートハンドラーを使用してAPIエンドポイントを作成することができます。

データベースクエリ

フルスタックのアプリケーションを作成するときは、データベースとやり取りをするロジックを書く必要があります。Postgresのようなリレーショナルデータベースの場合は、SQLやORMを使用することもできます。

以下の場合はクエリを書かなければなりません。

  • APIエンドポイントを作成する場合は、データベースとやり取りするためのロジックを必要とする場合
  • React Server Components(サーバー上でデータを取得する)を使用している場合は、APIレイヤーをスキップして、データベースの秘密をクライアントに公開するリスクを冒すことなく、データベースに直接問い合わせることができるという場合

サーバーコンポーネントを使用してデータを取得

基本的には、Next.jsアプリケーションはReact Server Componentsを使用します。Server Componentsを使用したデータ取得を利用するとこのようなメリットがあります。

  • サーバーコンポーネントはpromiseをサポートしており、データ取得などの非同期タスクをシンプルに解決することができます。useEffectuseState、データ取得ライブラリを使用することなく、async/await構文を使用することができます。
  • サーバーコンポーネントはサーバー上で実行されるため、データ取得やロジックをサーバー上に保持し、その結果のみをクライアントに送信することができます。
  • サーバーコンポーネントはサーバー上で実行されるため、APIレイヤーを追加することなくデータベースに直接問い合わせることができます

SQLを使用

今回のチュートリアルで作成しているダッシュボードのプロジェクトではVercel Postgres SDKとSQLを使用してデータベースクエリを記述しています。

  • SQLはリレーショナルデータベースをクエリするための業界標準です(ORMはフードの下でSQLを生成しています)。
  • SQLの基本的な理解を持つことで、リレーショナルデータベースの基礎を理解することができ、ほかのツールに知識を適用することができます。
  • SQLは汎用性があり、特定のデータを取得したり操作したりすることができます
  • Vercel Postgres SDKはSQLインジェクションからの保護を提供してくれます

/app/lib/data.tsを確認すると@vercel/postgresからsql関数インポートしていることが分かります。この関数を使用することでデータベースに問い合わせることができます。

どのサーバーコンポーネント内でもsqlを呼び出すことができます。しかし、より簡単にコンポーネントをナビゲートできるように、すべてのデータクエリをdata.tsに保持し、コンポーネントにインポートできるようにしています。

ダッシュボードプロジェクトの概要ページデータをフェッチする

データフェッチの様々な方法を学んだところで、ダッシュボードの概要ページのデータをフェッチしてみます。

/app/dashboard/page.tsxに移動して以下のコードを追加してみましょう。

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
 
export default async function Page() {
  return (
    <main>
      <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
        Dashboard
      </h1>
      <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
        {/* <Card title="Collected" value={totalPaidInvoices} type="collected" /> */}
        {/* <Card title="Pending" value={totalPendingInvoices} type="pending" /> */}
        {/* <Card title="Total Invoices" value={numberOfInvoices} type="invoices" /> */}
        {/* <Card
          title="Total Customers"
          value={numberOfCustomers}
          type="customers"
        /> */}
      </div>
      <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
        {/* <RevenueChart revenue={revenue}  /> */}
        {/* <LatestInvoices latestInvoices={latestInvoices} /> */}
      </div>
    </main>
  );
}

Page関数にはasyncが付いているため、非同期コンポーネントとなります。また、<Card><RevenueChat><LatestInvoices>というデータを受け取るコンポーネントが3つあります(現在はエラー防止でコメントアウト)。

<RevenueChart/>コンポーネントのデータを取得する

<RevenueChart/>コンポーネントのデータを取得するには、data.tsからfetchRevenue関数をインポートしてコンポーネント内で呼び出します。

/page/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  // ...
}

コードを追加・変更したら<RevenueChart>コンポーネントのコメントを解除して、コンポーネントファイルである/app/ui/dashboard/revenue-chart.tsxのコメントも解除しましょう。

コメントの解除が終わったら、サーバーが起動されていることを確認し、http://localhost:3000/dashboardにアクセスします。

以下のように収益データを使用したチャート画面が表示されたら、データ取得の成功です。

<LatestInvoices/>コンポーネントのデータを取得する

<LatestInvoices/>コンポーネントでは、日付順に並べ替えられた最新の5件の請求書を取得する必要があります。すべての請求書を取得して、JavaScriptを使って並べ替えることもできます。しかし、アプリケーションの規模が大きくなるにつれて、リクエスト毎に転送されるデータ量と、ソートするJavaScriptの量が大幅に増加する可能性があります。ソートして最新の請求書を使用するのではなく、SQLクエリを使用して直近の5件の請求書だけを取得しましょう。

/app/dashboard/page.tsxファイルを開き、以下の様にfetchLatestInvoices関数をインポートして追加します。

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue, fetchLatestInvoices } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  // ...
}

その後<LatestInvoices/>コンポーネントのコメントを外しましょう。また、/app/dashboard/latest-invoicesにある<LatestInvoices/>コンポーネントの関連コードもコメント解除する必要があります。

サーバーが起動されていることを確認したら再度、http://localhost:3000/dashboardにアクセスして5件の請求書データが表示されていることを確認しましょう。以下のようなが目になればデータ取得の成功です。

<Card/>コンポーネントのデータを取得する

続いて<Card/>コンポーネントのデータを取得します。

カードには以下のデータが表示されています。

  • 収集された請求書の合計金額
  • 未決済の請求書の合計金額
  • 請求書の合計数
  • 顧客の合計数

ここでもすべてのデータを取得してJavaScriptを使用しデータを操作したくなるかもしれません。ですがその方法では転送するデータが多くなってしまう等の問題が出てきます。

SQLを使用すると必要なデータを取り出すことができます。以下のようにコードを変更してみましょう。

/app/dashboard/page.tsx
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import {
  fetchRevenue,
  fetchLatestInvoices,
  fetchCardData
} from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  const {
    numberOfCustomers,
    numberOfInvoices,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
  // ...
}

以下のような画面が表示されたらカードの追加成功です。

注意点

これまででダッシュボードにコンポーネントを追加してきました。しかし注意すべき点が以下の二点あります。

  1. データリクエストは意図せず互いにブロックし合い、リクエストのウォーターフォールを生み出しています。
  2. デフォルトではNext.jsはパフォーマンスを向上させるためにルートをプリレンダリングします(スタティックレンダリング)。そのため、データが変更されてもダッシュボードには反映されません。

ウォーターフォールとは

注意点で説明された「ウォーターフォール」について学んでいきましょう。

ウォーターフォールというのは、前のリクエストの完了に依存する一連のネットワーク・リクエストのことです。データ・フェッチの場合、各リクエストは前のリクエストがデータを返して初めてフェッチの処理を開始できるようになります。

先ほど追加したフェッチ処理を確認してみましょう。ここではfetchRevenue関数が完了してからfetchLatestInvoices関数を実行し、fetchLatestInvoices関数が完了してからfetchCardData関数が実行されます。

/app/dashboard/page.tsx
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // fetchRevenue関数が完了するのを待機する
const {
  numberOfInvoices,
  numberOfCustomers,
  totalPaidInvoices,
  totalPendingInvoices,
} = await fetchCardData(); // fetchLatestInvoices関数が完了するのを待機する

ウォーターフォールパターンが悪というわけではありません。「最初にIDとプロフィール情報を取得する必要がある」など、適切に使用されれば問題ないのです。

今回は前の処理を待つ必要はありません。この場合はパフォーマンスに影響が出るため並列に実行することが望ましいです。

並列にデータを取得する

JavaScriptではPromise.all()Promise.allSettled()関数を使用してすべての処理を同時に開始することができます。

このパターンを使用することで以下のようなメリットが生まれます。

  • すべてのデータ取得を同時に実行し始めることで、パフォーマンスを向上させることができる
  • ネイティブのJavaScriptパターンを使用することで、どのようなライブラリやフレームワークにも適用できる

しかし、このJavaScriptのパターンだけに頼るとデメリットが生まれます。それは、複数のデータ取得のうち、一つのデータリクエストがほかのリクエストより遅い場合、遅いデータリクエストが完了するまで、UIが表示されなくなってしまいます。

次回はこの問題を解決していきます。

まとめ

  • Next.jsはデフォルトでプリレンダリングされているため、データに変更があってもUIに反映されない。
  • ウォーターフォールになってしまうのを避けてJavaScriptのpromiseを使用すると、すべてのデータ取得が終わるのを待つため、一つのデータ取得が遅れるとUIの表示も遅くなってしまう。
データ取得方法ポイント
APIレイヤー・APIを提供するサードパーティーのサービスを利用する場合。

・クライアントからデータを取得する場合にデータベースの秘密がクライアントに公開されるのを避けるために、サーバー上で動作するAPIレイヤーを用意したい場合。
データベースクエリ・APIエンドポイントを作成する場合は、データベースとやり取りするためのロジックを必要とする場合。

・React Server Components(サーバー上でデータを取得する)を使用している場合は、APIレイヤーをスキップして、データベースの秘密をクライアントに公開するリスクを冒すことなく、データベースに直接問い合わせることができるという場合。
サーバーコンポーネントを使用して取得・サーバーコンポーネントはpromiseをサポートしており、データ取得などの非同期タスクをシンプルに解決することができます。useEffectuseState、データ取得ライブラリを使用することなく、async/await構文を使用することができます。

・サーバーコンポーネントはサーバー上で実行されるため、データ取得やロジックをサーバー上に保持し、その結果のみをクライアントに送信することができます。

・サーバーコンポーネントはサーバー上で実行されるため、APIレイヤーを追加することなくデータベースに直接問い合わせることができます
SQL・SQLはリレーショナルデータベースをクエリするための業界標準です(ORMはフードの下でSQLを生成しています)。

・SQLの基本的な理解を持つことで、リレーショナルデータベースの基礎を理解することができ、ほかのツールに知識を適用することができます。

・SQLは汎用性があり、特定のデータを取得したり操作したりすることができます

・Vercel Postgres SDKはSQLインジェクションからの保護を提供してくれます
データの取得方法

次回

Next.jsのチュートリアルをやってみる【レンダリング編】

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です