この記事は DeNA 23 新卒 Advent Calendar 2022 の9日目の記事です。
はじめに
フロントエンドをメインで書いていると、簡単にバックエンドを用意したくてFirebaseを使うことも多いと思います。サクッとAuthやDBなどが使えるのは、特に個人開発では魅力的ですよね!
FirebaseはSDKもあり、ドキュメントもしっかりしているためかなり扱いやすいのですが、使っていて「これってどうなんだろう…?」と思うところがあったので、今回はそんなポイントについて考えていきたいと思います!
環境
今回はReactのNext.js
でFirebaseを扱うことを考えます。
他のライブラリやフレームワークだとまた話が変わってくるかもしれませんのでご注意を。
また、全体としてFirebaseが提供しているデータベースサービスのFirestoreの話が中心になります。
Firebaseの2つのSDK
Firebaseには2種類のSDKが存在します。1つはクライアントライブラリのFirebase SDK
で、もう1つがサーバーライブラリのFirebase Admin SDK
です。そう、これらはクライアントとサーバーそれぞれで使うという明確な違いがあり、一見わかりやすいように見えるんですよね…。
しかし、Next.jsではgetServerSideProps
やgetStaticProps
などでサーバーサイドのコードも触れたりするため、クライアントとサーバーの両方のコードを触ることがあり、どちらのSDKも扱う必要がある場面が発生します。こうなると、意識しないとごちゃごちゃしてくるので注意です。
セキュリティルールとSDK
Firebase SDKではデータベースを使用する際などにセキュリティルールというのを設定することで、ユーザーごとのアクセス権限などを設定できます。これにより、運営側の都合の良い形でセキュリティを設定できとても便利です。
その一方で、Firebase Admin SDKはあらかじめローカルに保持しているアクセスキーを使って API を呼び出すので、セキュリティルールを無視したアクセス権を持つことができます。
これらを必要に合わせてうまく使いこなす必要があるのも迷いポイントだと感じます。
書き方が違う2つのSDK
Firebase SDKはv8までとv9以降ではWebでの書き方が大きく書き方が変わっています。Firestoreの公式ドキュメントを例に見てみます。
例えば、1つのデータの取得ではv8までは以下のように書きます。
//DBの参照情報
const docRef = db.collection("cities").doc("SF")
//データ取得実行
const docSnap = docRef.get()
これがv9以降では関数ベースに切り替わっています。
//関数をインポートして使う
import { doc, getDoc } from "firebase/firestore"
//DBの参照情報
const docRef = doc(db, "cities", "SF")
//データ取得実行
const docSnap = getDoc(docRef)
SDKのバージョンアップで書き方が変わったこと自体は別にいいんです。
気にするべきところは、現状Firebase SDKはv9以降の書き方、Firebase Admin SDKはv8までの書き方をする必要があるというところです。
書き方が変わった理由としてバンドルサイズを最小限にするためらしく、サーバー側はバンドルサイズを気にする必要がないのでわざわざ変えていないということみたいです。ややこしいのでご注意を。
状況別でSDKの使い分けを考えてみる
ここからは、それぞれの状況を想定してSDKをどう使うのかを見ていきます。
SSR、SSG、ISRでデータ取得
これらの場合はめちゃくちゃ単純で、getServerSideProps
やgetStaticProps
での処理(サーバー側の処理)ですので、Firebase Admin SDKを使いましょう。
export const getServerSideProps: GetServerSideProps<Props> = async () => {
const docRef = db.collection("cities").doc("SF")
const docSnap = await docRef.get()
return { props: { data: docSnap.data() } }
}
//または
export const getStaticProps: GetStaticProps<Props> = async () => {
const docRef = db.collection("cities").doc("SF")
const docSnap = await docRef.get()
return { props: { data: docSnap.data() } }
}
クライアントサイドでデータ取得(useEffect使用)
ReactのhookであるuseEffect
を使用してデータ取得を行う場合はFirebase SDKを使った方が手っ取り早くて良いのかなと思います。
useEffect(async () => {
const docRef = doc(db, "cities", "SF")
const docSnap = await getDoc(docRef)
if (docSnap.exists()) {
//データがあったらstateなどにデータを保持
setState(docSnap.data())
} else {
//データがなかった時の処理
console.log("データが存在しません");
}
}, [])
ログインしていたらデータが見れるなど、状況に適したセキュリティルールの作成も忘れずに行いましょう!
クライアントサイドでデータ取得(useSWR使用)
上記のようにuseEffectでもクライアントサイドでのデータフェッチは可能なのですが、Next.jsを開発するVercel社は同社が開発したSWRと呼ばれるデータ取得用のライブラリを使用することを強く推奨しています。詳しくは公式ドキュメントを参照してください。
ただ、FirebaseとSWRを使う時には注意が必要です。
SWRは以下のようにして、importしたuseSWRフックの第1引数にURL、第2引数にデータフェッチを行う処理を持つfetcher関数を設定することで、取得したデータやエラーを返すことができます。
import useSWR from 'swr'
const { data, error } = useSWR('/api/data', fetcher)
通常のAPIを叩く時は何の問題もないのですが、僕は初めてFirebaseと使う時に思いました。
「"URL"ってなに…?」
そう、Firebaseは基本的にSDKを使っていたため、そもそもこの形式に当てはまらなかったのです。(もしいいやり方がありましたらぜひ教えてください!)
そこで、うまいことやるために使ったのがNext.jsのAPI Routes
です!
これはNext.js内でAPIを作成することができる超絶便利な機能です。こちらも詳細は公式ドキュメントを参照してください。
API Routesでデータ取得用のエンドポイントを作ることで、FirebaseのSDKを使いながらuseSWRをいい感じに使うことができます。
ただしここで重要なのが、API側での記述になるのでサーバーサイドの処理になる、つまりFirebase Admin SDKを使うことになるということを忘れずに!
API側のイメージ
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if(req.method === 'GET') {
const docRef = db.collection("cities").doc("SF")
const docSnap = await docRef.get()
res.status(200).json(docSnap.data())
}
}
フロント側のイメージ
const fetcher = async (url) => {
const response = await fetch(url)
return response.json()
}
const { data, error } = useSWR('/api/data',fetcher)
データ操作(ログインあり)
ユーザーのログイン機能があり、データ操作をする場合はFirebase SDKを使って、セキュリティルールを設定する方針で良いと思います。
例えばデータ作成ならこんな感じでログイン情報からセキュリティルールを設定できます。
allow create: request.auth != null && userId == request.auth.uid;
データ操作(ログインなし)
誰でも投稿できるようなものを作るときです。あまりない状況かもしれませんが、個人的にこれをしないといけない状況(自由投稿できるフォーム)に出会ったので、ありえないこともないかもしれません。
この状況は非常に難しいのですが、セキュリティルールフリーでwrite処理を実行できるようにするのもどうかと思ったので、「クライアントサイドでデータ取得(useSWR使用)」のようにAPIを経由させてFirebase Admin SDKを使う選択肢を自分は取りました。
もちろんFirebase SDK経由ではデータ操作はできないようにセキュリティルールを設定します。
allow write: if false;
ロジックをまとめたい
Firestoreを操作している部分のロジックまとめてコードもスッキリ見やすくしたいですよね。自分features
というディレクトリにまとめることが多いのですが、その際SDKごと(フロントとバック)で別のディレクトリにまとめるようにしましょう。
ここを混ぜてしまうとおそらく Module not found: Can't resolve 'fs'
というエラーに怒られると思うのでご注意を。自分は最初やってました…。
プロジェクトごとに変わりますが、大体よく使うディレクトリ構成が以下のような感じです。
libsでSDKごとに設定ファイルを作ってますが、これをいい感じにまとめている人も見かけたので試行錯誤は続けたいと思ってます。
src/
├─ components/
├─ features/
│ ├─ frontend/
│ │ ├─ converter/
│ │ ...
│ └─ backend/
│ ├─ converter/
│ ...
├─ hooks/
├─ libs/
│ ├─firebase.ts
│ └─firebaseAdmin.ts
├─ pages/
│ ├─ api/
│ ...
├─ styles/
└─ types/
さいごに
ここまで、自分が詰まった記憶を元にSDKで迷ったところをまとめてみました。
慣れている人には当たり前かもしれませんが、初見だとつまずくこともあると思うので参考になったら幸いです。
もっといい方法だったり、見落としているところがあったらぜひコメントで教えてください!
再度告知ですが、この記事は DeNA 23 新卒 Advent Calendar 2022 の9日目の記事です。他の記事もぜひ読んでみてください!!