はじめに
Next.js で Firestore を使う際に、データを CSR で取得したり SSR で取得したり、
ページによって取得方法が変わったりすると思いますが、
初期化方法を間違えてちょいちょいハマったりするので記事にまとめます。
この記事に書かれていること
- Firestore とのやり取りを CSR と SSR の両方でおこなう場合、初期化はそれぞれ分けて書く必要があるということを Vercel 社が用意しているサンプルコードを使ったりしながら説明しています。
※ 便宜上 SSR とだけ書いていますが SSG でも同じです。
CSR の場合
CSR する際に必要になる初期化
コンソールにログインし、プロジェクトの設定
→ 全般
からAPIキーが表示されている画面を開きます。
↑ の情報を .env に貼り付けます。
NEXT_PUBLIC_FIREBASE_API_KEY="***************"
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="***************"
NEXT_PUBLIC_FIREBASE_PROJECT_ID="***************"
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="***************"
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="***************"
NEXT_PUBLIC_FIREBASE_APP_ID="***************"
そして ↓ こんな感じで初期化します。
import firebase from 'firebase/app';
import 'firebase/firestore';
const credentials = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};
if (!firebase.apps.length) {
firebase.initializeApp(credentials);
}
export default firebase;
これだけですね。あとは必要な場所でインポートして使います。
注意点としては環境変数のプレフィクスにはNEXT_PUBLIC_FIREBASE_
が必要ということです。
SSR の場合
先に書いておくと、セキュリティルールのことを何も考えなければ上に書いた初期化でも動きます。
しかし実際にはセキュリティルールを書くことは必須になるはずです。
例えば以下のようなセキュリティルールを設定したとします。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
Firebaseのドキュメントにも載っているオーソドックスなルールです。
認証されたユーザーでのみアクセスできます。
認証されたユーザーであれば OK という、匿名ユーザーでもアクセスできてしまうゆるいルールですが、
このままでは SSR ではアクセスできません。通常、認証情報はクライアント側が持っているからです。
SSR する際に必要になる初期化
firebase-adminを使います。
firebase-admin は特権環境から Firebase を操作するためルールに関係なくサーバー側から Firestore にアクセスできます。
firebase-admin を使用するにはまずプロジェクトの設定
のサービスアカウント
から新しい秘密鍵の生成
をクリックし、秘密鍵(json ファイル)をダウンロードします。
※このファイルは絶対に外部に漏れてはいけないやつです。上に書いたAPIキーは漏れたところでルールを強固しておけば問題ない話ですが、このファイルが漏れたら事故です。
ダウンロードした json ファイルの中にproject_id
, client_email
, private_key
があるので、それを.env
に追加します。
# for client-side
NEXT_PUBLIC_FIREBASE_API_KEY="***************"
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="***************"
NEXT_PUBLIC_FIREBASE_PROJECT_ID="***************"
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="***************"
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="***************"
NEXT_PUBLIC_FIREBASE_APP_ID="***************"
# for firebase-admin
FIREBASE_PROJECT_ID="***************"
FIREBASE_CLIENT_EMAIL="***************"
FIREBASE_PRIVATE_KEY="***************"
※NEXT_PUBLIC_
は不要です。このプレフィクスはブラウザ側のjsにバンドルさせるために必要なものです。
※NEXT_PUBLIC_FIREBASE_PROJECT_ID
とFIREBASE_PROJECT_ID
の中身は同じなのでNEXT_PUBLIC_FIREBASE_PROJECT_ID
をそのまま流用することもできますが、それぞれが何を必要としているかわかりやすくするために敢えて追記しています。
.
以下のようにして初期化します。
import * as admin from 'firebase-admin';
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
}),
});
}
export default admin;
これで初期化のファイルが2つできましたので、
あとは CSR, SSR が必要な場所でそれぞれインポートして使います。
Next.js のサンプルコードを使って Firestore とのやり取りをしてみる
初期化の方法だけ並べられてもイメージが付きにくいと思うので、
Vercel 社が用意している Firebase との連携用のサンプルコードを使って、実際にエラーなども出しながら説明していきたいと思います。
これはこのサンプルコードをよりシンプルにアレンジしたものです。
クライアント側でデータを登録し、それを SSR で画面に表示します。
具体的には以下のような流れです。
-
ページを開いたタイミングで匿名認証のログイン
-
名前とメッセージを入力し、「Firestoreにデータを作成」をクリックするとクライアント側の処理で Firestore にデータが登録される。
-
「Go to SSR Page」をクリックすると先ほど登録したデータが SSR で画面に表示される。
↓ 実際にデプロイしたものがこちらです
クライアント側の処理
Firestore のルールは上で書いた例と同じように認証ユーザーであれば OK としています。
import Head from 'next/head';
import Link from 'next/link';
import { useState, useEffect } from 'react';
// ↓ CSR 用として初期化してあるものインポート
import firebase from '../firebase/clientApp';
export const Home = () => {
const [name, setName] = useState('');
const [message, setMessage] = useState('');
const data = { name, message };
useEffect(() => {
firebase.auth().onAuthStateChanged(async (user) => {
// 匿名ユーザーを作成する
if (!user) {
firebase.auth().signInAnonymously();
}
});
});
// Firestore にデータを登録する関数
const createData = async () => {
if (!name || !message) {
alert('名前とメッセージを入力してください');
return;
}
const db = firebase.firestore();
await db.collection('profile').doc(name).set(data);
alert('Firestoreにデータを作成できました!');
};
return (
<div className="container">
<Head>
<title>Next.js / Firestore</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<h1 className="title">Next.js / Firebase CSR</h1>
<p className="description">名前とメッセージを入力してください。</p>
<div className="labelBox">
<label>
名前:
<input value={name} onChange={(e) => setName(e.target.value)} />
</label>
<label>
メッセージ:
<input value={message} onChange={(e) => setMessage(e.target.value)} />
</label>
</div>
<button onClick={createData}>Firestoreにデータを作成</button>
<Link href={`/profile/${data.name}`} passHref>
<a>Go to SSR Page</a>
</Link>
</main>
//これより下はただのスタイルのため省略...
注意すべきことは CSR 用に初期化したファイルをインポートすることです。
もし誤って SSR 用に初期化した admin を使って firestore を呼んでもエラーになります。
実際にやってみましょう。
import Head from 'next/head';
import Link from 'next/link';
import { useState, useEffect } from 'react';
import firebase from '../firebase/clientApp';
// ↓ SSR 用に初期化した admin をインポート
import admin from '../firebase/nodeApp';
// Firestore にデータを登録する関数
const createData = async () => {
if (!name || !message) {
alert('名前とメッセージを入力してください');
return;
}
// db を admin から作成
const db = admin.firestore();
await db.collection('profile').doc(name).set(data);
alert('Firestoreにデータを作成できました!');
};
この状態でアプリを起動すると
Module not found: Can't resolve 'fs'
というエラーや、
Module not found: Can't resolve 'child_process'
のようなエラーが出ます。
node.js 特有のコードがブラウザ側で読まれてしまっていることが原因です。
サーバー側の処理
次に SSR の処理を書いていきます。
// admin をインポート
import admin from '../firebase/nodeApp';
export const getProfileData = async (name) => {
// admin から db を作成
const db = admin.firestore();
const profileCollection = db.collection('profile');
const profileDoc = await profileCollection.doc(name).get();
if (!profileDoc.exists) {
return null;
}
// 取得したデータを返す
return profileDoc.data();
};
import Head from 'next/head';
// ↓ 上に書いた関数をインポート
import { getProfileData } from '../../fetchData/getProfileData';
const SSRPage = ({ data }) => {
const { name, profile } = data;
return (
<div className="container">
<Head>
<title>Next.js / Firestore</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<h1 className="title">Next.js / Firebase SSR</h1>
<h2>{name}</h2>
<p>{profile.message}</p>
</main>
</div>
);
};
export const getServerSideProps = async ({ params }) => {
const { name } = params;
// getServerSideProps 内で getProfileData() を実行する
const profile = await getProfileData(name);
if (!profile) {
return { notFound: true };
}
return { props: { data: { name, profile } } };
};
export default SSRPage;
注意すべきことは SSR 用に初期化した admin をインポートすることです。
もし誤って CSR 用に初期化した方をインポートするとエラーになります。
こちらも実際にやってみましょう。
// CSR 用に初期化した方をインポートします
import firebase from '../firebase/clientApp';
export const getProfileData = async (name) => {
// ここも admin から firebase に書き換えます。
const db = firebase.firestore();
const profileCollection = db.collection('profile');
const profileDoc = await profileCollection.doc(name).get();
if (!profileDoc.exists) {
return null;
}
return profileDoc.data();
};
この状態でアプリを起動してみます。
データの登録までは問題なくできますが、いざ SSR のページをクリックすると
Missing or insufficient permissions.
というエラーが表示されます。
これは Firestore のルールに引っかかったときに表示されるエラーです。
クライアント側では匿名認証がされていますが、サーバー側ではされていないので弾かれてしまいます。
試しに Firestore のルールを緩めてみましょう。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
誰でもアクセスできる状態にしました。
これで再度アプリを実行してみます。
読み込めました。
世に公開しないものであれば、この方法でも問題ないでしょう。
まとめ
以上、初期化方法の違いについてでした。
このサンプルコードでもある通り、
初期化のファイルは分けてわかりやすく管理することが大事だと思いました。