Next.jsでFirestoreを使う時のSDK迷いポイント

この記事は DeNA 23 新卒 Advent Calendar 2022 の9日目の記事です。

https://qiita.com/advent-calendar/2022/dena-23-shinsotsu

# はじめに

フロントエンドをメインで書いていると、簡単にバックエンドを用意したくて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も扱う必要がある場面が発生します。こうなると、意識しないとごちゃごちゃしてくるので注意です。

https://firebase.google.com/docs/web/setup?hl=ja

https://firebase.google.com/docs/admin/setup

## セキュリティルールとSDK

Firebase SDKではデータベースを使用する際などにセキュリティルールというのを設定することで、ユーザーごとのアクセス権限などを設定できます。これにより、運営側の都合の良い形でセキュリティを設定できとても便利です。
その一方で、Firebase Admin SDKはあらかじめローカルに保持しているアクセスキーを使って API を呼び出すので、セキュリティルールを無視したアクセス権を持つことができます。
これらを必要に合わせてうまく使いこなす必要があるのも迷いポイントだと感じます。

## 書き方が違う2つのSDK

Firebase SDKはv8までとv9以降ではWebでの書き方が大きく書き方が変わっています。Firestoreの公式ドキュメントを例に見てみます。

https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja

例えば、1つのデータの取得ではv8までは以下のように書きます。

```typescript
//DBの参照情報
const docRef = db.collection("cities").doc("SF")
//データ取得実行
const docSnap = docRef.get()
```

これがv9以降では関数ベースに切り替わっています。

```typescript
//関数をインポートして使う
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を使いましょう。

```typescript
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を使った方が手っ取り早くて良いのかなと思います。

```typescript
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](https://swr.vercel.app/ja)と呼ばれるデータ取得用のライブラリを使用することを強く推奨しています。詳しくは公式ドキュメントを参照してください。

https://nextjs-ja-translation-docs.vercel.app/docs/basic-features/data-fetching/client-side

ただ、FirebaseとSWRを使う時には注意が必要です。
SWRは以下のようにして、importしたuseSWRフックの第1引数にURL、第2引数にデータフェッチを行う処理を持つfetcher関数を設定することで、取得したデータやエラーを返すことができます。

```typescript
import useSWR from 'swr'

const { data, error } = useSWR('/api/data', fetcher)
```

通常のAPIを叩く時は何の問題もないのですが、僕は初めてFirebaseと使う時に思いました。

**「"URL"ってなに…？」**

そう、Firebaseは基本的にSDKを使っていたため、そもそもこの形式に当てはまらなかったのです。（もしいいやり方がありましたらぜひ教えてください！）

そこで、うまいことやるために使ったのがNext.jsの`API Routes`です！
これはNext.js内でAPIを作成することができる超絶便利な機能です。こちらも詳細は公式ドキュメントを参照してください。

https://nextjs-ja-translation-docs.vercel.app/docs/api-routes/introduction

API Routesでデータ取得用のエンドポイントを作ることで、FirebaseのSDKを使いながらuseSWRをいい感じに使うことができます。
ただしここで重要なのが、API側での記述になるのでサーバーサイドの処理になる、つまり**Firebase Admin SDKを使うことになる**ということを忘れずに！

API側のイメージ

```typescript
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())
  }
}
```

フロント側のイメージ

```typescript
const fetcher = async (url) => {
  const response = await fetch(url)
  return response.json()
}
const { data, error } = useSWR('/api/data',fetcher)
```

## データ操作（ログインあり）

ユーザーのログイン機能があり、データ操作をする場合はFirebase SDKを使って、セキュリティルールを設定する方針で良いと思います。

例えばデータ作成ならこんな感じでログイン情報からセキュリティルールを設定できます。

```javascript
allow create: request.auth != null && userId == request.auth.uid;
```

## データ操作（ログインなし）

誰でも投稿できるようなものを作るときです。あまりない状況かもしれませんが、個人的にこれをしないといけない状況（自由投稿できるフォーム）に出会ったので、ありえないこともないかもしれません。
この状況は非常に難しいのですが、セキュリティルールフリーでwrite処理を実行できるようにするのもどうかと思ったので、「クライアントサイドでデータ取得（useSWR使用）」のようにAPIを経由させてFirebase Admin SDKを使う選択肢を自分は取りました。

もちろんFirebase SDK経由ではデータ操作はできないようにセキュリティルールを設定します。

```javascript
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日目の記事です。他の記事もぜひ読んでみてください！！

https://qiita.com/advent-calendar/2022/dena-23-shinsotsu