🗄️

ゼロから学ぶ React, Next.js⑭【Learn Next.js】Chapter7

2024/05/25に公開

【Chapter7】 データのフェッチ

データベースを作成してシードしたので、アプリケーションのデータをフェッチする様々な方法と、ダッシュボードの概要ページの構築について議論しましょう。

この章で扱うトピック

  • 🗄️ データフェッチのアプローチ(API、ORM、SQLなど)について学ぶ
  • 🔑 サーバーコンポーネントがバックエンドリソースへのよりセキュアなアクセスに役立つ方法
  • 🌐ネットワークウォーターフォールとは何か
  • 📳 JavaScriptパターンを使用して並列データフェッチを実装する方法

データフェッチの方法の選択

APIレイヤー

APIは、アプリケーションコードとデータベースの間の中間レイヤーです。APIを使用する場合がいくつかあります:

  • APIを提供するサードパーティのサービスを使用している場合。
  • クライアントからデータをフェッチする場合、データベースのシークレットをクライアントに公開しないように、サーバー上で実行されるAPIレイヤーが必要です。

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

データベースクエリ

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

データベースクエリを書かなければならない場合がいくつかあります:

  • APIエンドポイントを作成する際に、データベースとやり取りするロジックを書く必要があります。
  • React Server Components(サーバー上でデータをフェッチする)を使用している場合は、APIレイヤーをスキップし、データベースのシークレットをクライアントに公開するリスクなしに、データベースに直接クエリを実行できます。

クイズの時間です!
知識をテストし、学んだことを確認しましょう。

次のどのシナリオでは、データベースに直接クエリを実行すべきではありませんか?

A. クライアント上でデータをフェッチする場合
B. サーバー上でデータをフェッチする場合
C. データベースとやり取りするための独自のAPIレイヤーを作成する場合

解答

A. クライアント上でデータをフェッチする場合
データベースの秘密情報が漏えいしてしまうため、クライアント上でデータをフェッチする際に、データベースに直接クエリを実行すべきではありません。

React Server Componentsについて詳しく学びましょう。

データフェッチにServer Componentsを使用する

デフォルトでは、Next.jsアプリケーションはReact Server Componentsを使用します。Server Componentsでデータをフェッチすることは比較的新しいアプローチであり、いくつかの利点があります:

  • Server Componentsはプロミスをサポートしているため、データフェッチなどの非同期タスクをより簡単に解決できます。useEffectuseState、データフェッチライブラリを使用せずに、async/await構文を使用できます。
  • Server Componentsはサーバー上で実行されるため、高価なデータフェッチとロジックをサーバー上に置き、結果のみをクライアントに送信できます。
  • 前述のように、Server Componentsはサーバー上で実行されるため、追加のAPIレイヤーなしに、データベースに直接クエリを実行できます。

クイズの時間です!
知識をテストし、学んだことを確認しましょう。

React Server Componentsを使用してデータをフェッチすることの利点の1つは何ですか?

A. SQLインジェクションから自動的に保護してくれる。
B. 追加のAPIレイヤーなしに、サーバーからデータベースに直接クエリを実行できる。
C. APIレイヤーを使用してエンドポイントを作成する必要がある。

解答

B. 追加のAPIレイヤーなしに、サーバーからデータベースに直接クエリを実行できる。
サーバーコンポーネントを使用すると、データベースから直接データを取得できます。

SQLの使用

ダッシュボードプロジェクトでは、Vercel Postgres SDKとSQLを使用してデータベースクエリを書きます。SQLを使用する理由はいくつかあります:

  • SQLは、リレーショナルデータベースのクエリの業界標準です(例:ORMはSQLを生成します)。
  • SQLの基本的な理解は、リレーショナルデータベースの基礎を理解するのに役立ち、他のツールに知識を適用できるようになります。
  • SQLは汎用性があり、特定のデータをフェッチして操作できます。
  • Vercel Postgres SDKは、SQLインジェクションに対する保護を提供します。

SQLを使用したことがない場合でも心配しないでください。クエリは用意されています。

/app/lib/data.tsに移動すると、@vercel/postgresからsql関数をインポートしていることがわかります。この関数を使用すると、データベースにクエリを実行できます:

/app/lib/data.ts
import { sql } from '@vercel/postgres';
 
// ...

sqlはどのServer Componentの中でも呼び出すことができます。ただし、コンポーネントをより簡単にナビゲートできるようにするために、すべてのデータクエリをdata.tsファイルに保持し、コンポーネントにインポートできるようにしました。

クイズの時間です!
知識をテストし、学んだことを確認しましょう。

SQLを使用すると、データのフェッチに関してどのようなことができますか?

A. すべてのデータを無差別にフェッチする
B. 特定のデータをフェッチして操作する
C. パフォーマンス向上のためにデータを自動的にキャッシュする
D. その場でデータベーススキーマを変更する

解答

B. 特定のデータをフェッチして操作する
SQLを使用すると、特定のデータを取得し、操作するためのターゲットクエリを記述することができます。


ダッシュボードの概要ページのデータフェッチ

データをフェッチする様々な方法を理解したので、ダッシュボードの概要ページのデータをフェッチしましょう。/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は非同期コンポーネントです。これにより、awaitを使用してデータをフェッチできます。
  • データを受け取る3つのコンポーネントもあります:<Card><RevenueChart><LatestInvoices>。これらは現在コメントアウトされており、アプリケーションがエラーにならないようにしています。

<RevenueChart/>のデータフェッチ

<RevenueChart/>コンポーネントのデータをフェッチするには、data.tsからfetchRevenue関数をインポートし、コンポーネント内で呼び出します:

/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 } from '@/app/lib/data';
 
 export default async function Page() {
+  const revenue = await fetchRevenue();
   // ...
 }

次に、<RevenueChart/>コンポーネントのコメントを外し、コンポーネントファイル(/app/ui/dashboard/revenue-chart.tsx)に移動して、その中のコードのコメントを外します。localhostを確認すると、収益データを使用するチャートが表示されるはずです。

チャート追加後

さらにいくつかのデータクエリをインポートし続けましょう!


<LatestInvoices/>のデータフェッチ

<LatestInvoices />コンポーネントでは、日付順にソートされた最新の5つの請求書を取得する必要があります。

JavaScriptを使用してすべての請求書をフェッチし、ソートすることもできます。データが小さいので問題ありませんが、アプリケーションが大きくなるにつれて、各リクエストで転送されるデータ量とそれをソートするために必要なJavaScriptが大幅に増加する可能性があります。

メモリ内で最新の請求書をソートする代わりに、SQLクエリを使用して最後の5つの請求書のみをフェッチできます。例えば、data.tsファイルのSQLクエリは次のようになります:

/app/lib/data.ts
// 日付順にソートされた最後の5つの請求書をフェッチする
const data = await sql<LatestInvoiceRaw>`
  SELECT invoices.amount, customers.name, customers.image_url, customers.email
  FROM invoices
  JOIN customers ON invoices.customer_id = customers.id
  ORDER BY invoices.date DESC
  LIMIT 5`;

ページで、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/ui/dashboard/latest-invoicesにある<LatestInvoices />コンポーネント自体の関連コードのコメントも外す必要があります。

localhostにアクセスすると、データベースから最後の5つのみが返されることがわかります。データベースに直接クエリを実行する利点が理解できるようになったことを願っています!

収益チャートとともに表示される最新の請求書コンポーネント


練習:<Card>コンポーネントのデータフェッチ

次は、<Card>コンポーネントのデータをフェッチする番です。カードには以下のデータが表示されます:

  • 回収済みの請求書の合計金額。
  • 保留中の請求書の合計金額。
  • 請求書の総数。
  • 顧客の総数。

繰り返しになりますが、すべての請求書と顧客をフェッチし、JavaScriptを使用してデータを操作したくなるかもしれません。例えば、Array.lengthを使用して、請求書と顧客の総数を取得できます:

const totalInvoices = allInvoices.length;
const totalCustomers = allCustomers.length;

ただし、SQLを使用すると、必要なデータのみをフェッチできます。Array.lengthを使用するよりも少し長くなりますが、リクエスト中に転送する必要のあるデータが少なくなります。SQLの代替は次のとおりです:

/app/lib/data.ts
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;

インポートする必要がある関数はfetchCardDataと呼ばれています。関数から返される値を分割する必要があります。

ヒント:

  • カードコンポーネントを確認して、どのデータが必要かを確認します。
  • data.tsファイルを確認して、関数が何を返すかを確認します。

準備ができたら、トグルを展開して最終的なコードを確認してください:

解答
/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 {
+   numberOfInvoices,
+   numberOfCustomers,
+   totalPaidInvoices,
+   totalPendingInvoices,
+  } = await fetchCardData();
 
  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>
  );
}

素晴らしい!ダッシュボードの概要ページに必要なすべてのデータをフェッチしました。ページは次のようになっているはずです:

データがすべてフェッチされたダッシュボードページ
ただし...注意すべき点が2つあります:

  1. データリクエストは意図せずお互いをブロックし、リクエストウォーターフォールを作成しています。
  2. デフォルトでは、Next.jsはパフォーマンスを向上させるためにルートをプリレンダリングします。これは静的レンダリングと呼ばれます。したがって、データが変更された場合、ダッシュボードには反映されません。

この章では1番目について説明し、次の章で2番目について詳しく見ていきましょう。


リクエストウォーターフォールとは?

「ウォーターフォール」とは、以前のリクエストの完了に依存するネットワークリクエストのシーケンスを指します。データフェッチの場合、各リクエストは、前のリクエストがデータを返した後にのみ開始できます。

順次データフェッチと並列データフェッチの時間を示す図

例えば、fetchLatestInvoices()が実行を開始する前に、fetchRevenue()の実行が完了するのを待つ必要があります。以下同様です。

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

このパターンは必ずしも悪いわけではありません。次のリクエストを行う前に条件が満たされることを望む場合があります。例えば、最初にユーザーのIDとプロフィール情報をフェッチしたい場合があります。IDを取得したら、友達のリストをフェッチすることができます。この場合、各リクエストは前のリクエストから返されたデータに依存しています。
ただし、この動作は意図しないものであり、パフォーマンスに影響を与える可能性もあります。

クイズの時間です!
知識をテストし、学んだことを確認しましょう。
いつウォーターフォールパターンを使用したいと思いますか?
A. 次のリクエストを行う前に条件を満たすため
B. すべてのリクエストを同時に行うため
C. 一度に1つのフェッチを行うことでサーバーの負荷を減らすため

解答

A. 次のリクエストを行う前に条件を満たすため
例えば、まずユーザーのIDとプロフィール情報を取得して、IDを取得したら、次に友達のリストを取得したい場合など。


並列データフェッチ

ウォーターフォールを回避する一般的な方法は、すべてのデータリクエストを同時に(並行して)開始することです。
JavaScriptでは、Promise.all()またはPromise.allSettled()関数を使用して、すべてのプロミスを同時に開始できます。例えば、data.tsでは、fetchCardData()関数でPromise.all()を使用しています:

/app/lib/data.js
 export async function fetchCardData() {
   try {
     const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
     const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
     const invoiceStatusPromise = sql`SELECT
          SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
          SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
          FROM invoices`;
 
+    const data = await Promise.all([
+      invoiceCountPromise,
+      customerCountPromise,
+      invoiceStatusPromise,
+    ]);
    // ...
  }
}

このパターンを使用すると、次のことができます:

  • すべてのデータフェッチを同時に実行開始し、パフォーマンスを向上させることができます。
  • ライブラリやフレームワークに適用できるネイティブのJavaScriptパターンを使用できます。

ただし、このJavaScriptパターンのみに依存することには1つ欠点があります:1つのデータリクエストが他のすべてのリクエストよりも遅い場合はどうなりますか?


次の章

https://zenn.dev/gunjo/articles/44b7a4cd0def7c

Discussion