カテゴリー: FrontEnd

[Next.js]FirestoreのデータをServer Componentから取得する

はじめに

最近、Next.jsでFirestoreをバックエンドとしたPublicサイトの開発を進めていて、どのようにFirestoreのデータを取得するか、色々と調査検討してみたので、その結果をまとめます。

開発の方針は以下の通りです。

  • 1日1回、Firestoreの配信データが更新され、そのデータを閲覧できるサイト (多少ラグがあって良い)
  • Firestoreは極力無料枠に収めたい。そのためにリクエスト回数を抑制したい。
  • Publicサイトのため、SEOを考慮しNext.jsのサーバサイドレンダリングを活用する。

Next.jsでFirestoreからデータを取得する方法

Next.jsでFirestoreからデータを取得する方法は、いくつか考えられます。

  1. Client Componentで直接Firestoreから取得
  2. Route Handlerを使ってAPI化し、Server Componentからfetchする。
  3. Server Componentで直接Firestoreから取得

今回は、この中で「3. Server Componentで直接Firestoreから取得」を選択しました。それぞれの選択した/しなかった理由をこれから書いていきます。

Client Componentで直接Firestoreから取得

以下のような理由で選択しませんでした。

  • Client ComponentでのレンタリングはSEO的に不利
  • 更新頻度に対してFirestore側へのAPIコール数が過剰になってしまう
  • セキュリティルールの設定が必須

Firestoreのリアルタイム性を活用した機能を実装するのであれば、この選択が良いと思います。

Route Handlerを使ってAPI化し、Server Componentからfetchする。

Client Componentから直接取得する方法に比べ、

  • Server Componentでレンダリングすることにより、SEO的に有利
  • Server Componentで閉じているため、Admin SDKを使用することができ、セキュリティルールは全てのread writeを無効化
  • fetchにrevalidateを指定することで、Firestore側へのAPIコールを減らすことができる

といった点で、当初はこの方法を選択しようと思いましたが、以下のような理由で選択しませんでした。

  • APIは公開されるため、API Key等による認証が必要になる
  • APIごとにキャッシュ時間をカスタムできるのが利点だが、今のところそこまで細かく制御したいニーズがない

Server Componentで直接Firestoreから取得

Pages RouterのISRと同じような概念として、App Routerには Server ComponentのConfigに revalidateがあります。初回アクセス時にレンダリングされた内容はキャッシュされ、 revalidateに指定した秒数が経過して初めてのリクエスト時にキャッシュが更新されます。(ただし、すぐ最新化される訳ではなく、2回目以降のリクエストから最新化されます。)
revalidateはそのページ全体をキャッシュします。複数のコンポーネントでrevalidateが指定されている場合は、それらの中で一番短い時間が使用されます。(参考)
なお、デフォルトでは falseとなっていて、一度レンダリングされると、ずっとキャッシュが維持されます。

今回はシンプルな一覧と詳細がメイン機能であるため、この方法を選択しました。
(ちなみに、ページ単位のrevalidateのみでは柔軟性に欠けますが、これとfetchのrevalidationも組み合わせることができ、その場合は特定コンポーネントのみrevalidation間隔を短くすることができます。)

実際に組み込んでみる

Firestoreのセットアップ

詳細な説明は省きますが、Web Appの追加と、Firestoreへの初期データ登録までは完了させておきます。
また、サービス アカウントの認証情報を含む構成ファイル(JSON)をダウンロードし、src/の直下に置いておきます。

Firebase Adminの組み込み

まずは、firebase-adminをインストールします。

% pnpm add firebase-admin

次に、firebaseの初期化処理を書くため、 app/lib/firestore.tsを作成します。

import admin from "firebase-admin";
import serviceAccount from "@/yourprojectname-firebase-adminsdk-abcde-01234567.json";
import { getFirestore } from "firebase-admin/firestore";

const init = () => {
  if (!admin.apps.length) {
    admin.initializeApp({
      credential: admin.credential.cert({
        projectId: serviceAccount.project_id,
        privateKey: serviceAccount.private_key,
        clientEmail: serviceAccount.client_email,
      }),
    });
  }
};

ここでは、先ほど配置した構成ファイル(JSON)をimportし、Admin SDKの初期化時に認証情報を渡しています。

Firestoreからデータを取得する部分の実装

先ほどと同じファイルに追記していきます。

type Meeting = {
  id: string;
  title: string;
  agenda: string;
  summary: string;
  tags: string[];
  heldAt: Date;
};

export const getMeetings = async () => {
  init();
  const db = getFirestore();
  const meetingsRef = db.collection("meetings");
  const snapshot = await meetingsRef.orderBy("heldAt", "desc").get();

  return snapshot.docs.map((doc) => ({
    id: doc.id,
    ...doc.data(),
    heldAt: doc.data().heldAt.toDate(),
  })) as Meeting[];
};

ここでは、議事録の一覧を取得して返しています。

page.tsxの実装

最後に、先ほど作ったgetMeetings()をpages.tsxから呼び、実際に表示させます。また、 export const revalidateを宣言し、適切な値を設定することで、定期的にこのページを更新しつつ、Firestoreへのリクエストを抑えます。

import { getMeetings } from "@/app/lib/firestore";
import { FormattedDate } from "@/components/formattedDate";
import Link from "next/link";

export const revalidate = 43200;  // 12 hours

export default async function Home() {
  const meetings = await getMeetings();
  return (
    <main>
      {meetings.map((meeting) => (
        <div key={meeting.id}>
          <Link href={`/meetings/${meeting.id}`}>
            <h2>{meeting.title}</h2>
          </Link>
          <p>{meeting.agenda}</p>
          <p>{meeting.summary}</p>
          <p>{meeting.tags.join(", ")}</p>
          <p>
            <FormattedDate date={meeting.heldAt} />
          </p>
        </div>
      ))}
    </main>
  );
}

このように、Server Component内でFirestoreへのリクエストを完結させることで、クライアント側にその部分を隠蔽することができます。

さいごに

Server Componentのrevalidateを使用することで、Pages Router時代のISRと同じような挙動を作り出すことができました。

おすすめ書籍

カイザー

シェア
執筆者:
カイザー
タグ: Next.js

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

3週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

1か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前