既存のNext.js製アプリをFirebase Hostingにデプロイしてみた（ISRは動きません）

# はじめに

この記事を開いた人の中には「いやVercelにデプロイしろよ」って思われた方もいるかもしれませんが、大人の事情でデプロイ先を指定されることもあるかと思います。

Next.jsで作ったのであれば私も絶対にVercelがいいと思いますが、やむなくFirebaseにせざるを得なくなった人たちのために、苦労話としてまとめます。

# 先に結果を書いておきます

デプロイ自体はできました。その手順は下に書いていきます。
ただ**ISRは機能しませんでした。**

原因はこの解説がわかりやすかったです。

https://zenn.dev/catnose99/articles/0b601c1f62019b

>Next.jsのISRはキャッシュしたHTMLをファイルシステムに書き込む仕様になっているようです
それゆえに
AWS Lambda / Cloud Functions / GAE → ファイルシステムへの書き込みに未対応
AWS Fargate / GKE / Cloud Run → コンテナごとにキャッシュが分散してしまう
といった問題にぶつかります。

そうなんです。実際に私もCloud Functionのログで`[Error: EROFS: read-only file system・・・`という「書き込みが出来ないよ」というエラーを確認しました。

https://twitter.com/hmktsu/status/1288710861708640258

同様のツイートもありました。
そしてこの方がNext.jsのDiscussionにも投稿されていたのですが、その中に一つとても興味深い回答がありました。

https://github.com/vercel/next.js/discussions/15671

![スクリーンショット 2021-03-24 23.50.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631600/3949cc78-0ae8-caf8-3799-4d29f5b97b8b.png)
>いくつかテストをしてみたところ、Firebaseは異なる形式のNext.jsプリレンダリングのキャッシュ設定をサポートしているようです。

ソースを見に行ってみたところ、どうやら[Custom Server](https://nextjs.org/docs/advanced-features/custom-server)を使ってこの問題の解決を図ろうとしているようです。

https://github.com/FSaldanha/nextjs-cache-purging

>カスタムサーバー（このリポジトリのserver.jsを参照）を設定することで、Next.jsにpurge=1というパラメータを追加するだけで、特定のURLのプリレンダリングファイルとLRUキャッシュのパージを削除するように指示することができます。これにより、ドキュメントには反対のことが書かれていますが、自動静的最適化が維持されるようです。

残念ながらこの説明を参考にCustom Serverを設定してみたのですが解決できず、
まだテスト段階だからできなかったのか、私のアプリ側の問題だったのかはわかりませんが、
このリポジトリは今後もウォッチしていきたいと思います。

ということで本題に入りますが、
ここから下の手順はISRを取り入れていないアプリ（例えば単純にSSRだけなど）をFirebase Hostingにデプロイする方法として参考にしてください。

~~ISRを取り入れているアプリはFirebaseでのデプロイではなく、
一番上のリンクを参考にGAE + Cloud CDNなどの方法で試してみてください。
（2回目ですがVercelが一番だと思っています）~~
### 追記：2021/03/28
別の方法でISRっぽい動きを実現できました。

https://qiita.com/centerfield/items/a7e4964d6c1963b70bbb

# 環境

- macOS Catalina 10.15.6
- Next js 10.0.8

# 前提

- Firebaseのプロジェクトが作成されていること
- Firebase CLI がグローバルにインストールされていること ※1
- Firebase CLI からプロジェクトを作成したアカウントでログインしていること
- Blazeプランへのアップグレードをしていること ※2
- /srcディレクトリを作成し、その中にNext.jsアプリが格納されていること ※3

※1 [Firebase CLI リファレンス](https://firebase.google.com/docs/cli?hl=ja)
※2 Next.js製のアプリをデプロイするにはCloud Functionを利用することになりますが、現在はBlazeプラン（従量課金）でのみ利用可能となっています。（2021年3月現在）
※3 後述しますがVercelのサンプルコード内にあるREADMEにこの指定が記載されています。srcについてはこちらを[Next.js公式](https://nextjs.org/docs/advanced-features/src-directory)



#やっていく

Vercelは本当に良い会社で、Firebase Hostingへのデプロイを想定したサンプルコードまで用意してくれています。
ここからはVercelのGitHubを覗きながら自分のコードと比較して足りないところを補っていくという手法で進めていきたいと思います。

https://github.com/vercel/next.js/blob/canary/examples/with-firebase-hosting/README.md

## package.jsonを編集する

package.jsonを開き、まず`"main": "firebaseFunctions.js",`の一文を追加します。

```/package.json
{
  "name": "★★★",
  "version": "0.1.0",
  "private": true,
  "main": "firebaseFunctions.js", ← この１行を追加
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },

--省略--

```

その下に`"engines"`でNode.jsのバージョンを指定します。
Vercelのサンプルコードにはないですが、Cloud Functionのドキュメントでは必須とされています。
今回はドキュメント内で推奨されている12を選択します。
[関数のデプロイとランタイム オプションを管理する](https://firebase.google.com/docs/functions/manage-functions?hl=ja)

```/package.json
{
  "name": "★★★",
  "version": "0.1.0",
  "private": true,
  "main": "firebaseFunctions.js",
  "engines": {
   "node": "12"
  },
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },

--省略--

```

続いて`"scripts"`も追加します。
デフォルトだと既に`"dev"`, `"build"`, `"start"`があると思いますが、
その下に`"serve"`, `"shell"`, `"deploy"`, `"logs"`を追加します。コピペでOKです。

```/package.json
{
  "name": "★★★",
  "version": "0.1.0",
  "private": true,
  "main": "firebaseFunctions.js",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "serve": "npm run build && firebase emulators:start --only functions,hosting",
    "shell": "npm run build && firebase functions:shell",
    "deploy": "firebase deploy --only functions,hosting",
    "logs": "firebase functions:log"
  },

--省略--

```

##必要なパッケージを追加する

↓これはvercelのサンプルコードにあるpackage.jsonです。

```(vercelのサンプルコード)/package.json
{
 --省略--

 "dependencies": {
     "firebase-admin": "^9.4.2",
     "firebase-functions": "^3.13.1",
     "next": "latest",
     "react": "^17.0.1",
     "react-dom": "^17.0.1"
   },
   "devDependencies": {
     "firebase-functions-test": "^0.2.3",
     "firebase-tools": "^9.3.0"
   },
}
```

これを見る限りFirebase関連で必要なパッケージは
`"firebase-admin"` 
`"firebase-function"`
`"firebase-functions-test"`
`"firebase-tools"`
の4つのようです。

"firebase-functions-test"は今回は不要そうな空気も出ていますが、
サンプルコードに従いこの4つを`npm`または`yarn`で追加します。

※ "firebase-functions-test"と"firebase-tools"は`dev`オプションをつけて`devDependencies`に入るようにしましょう。

## firebase init コマンドで初期化

みんな大好きであろうこの画面が出るやつです。
![スクリーンショット 2021-03-23 23.52.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631600/70752f45-b84a-a2fe-6b6f-7ebd6fe7964e.png)
設問はこのように進めました。
・Configure and deploy Cloud Functionsを選択
・既存のプロジェクトを選択
・TS or JS の問いはJSを選択（まだTSを選ぶ自信がなかった）
・ES Lintどうこう〜はNo
・npmの依存関係どうこうもNo

完了するとプロジェクト直下に`.firebaserc`と`firebase.json`が作成されているはずです。

## firebase.jsonを編集する

自動作成された`.firebaserc`は初めから内容が記載され、サンプルコードとも一致していたので問題なかったのですが、`firebase.json`はほぼ空だったのでサンプルコードからコピペします。
※ ✳️がたくさんあるので何か書き換えを求められているのか？と思っちゃいそうですがコピペでOKです。

```/firebase.json
{
  "hosting": {
    "public": "public",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [
      {
        "source": "**",
        "function": "nextjsFunc"
      }
    ]
  },
  "functions": {
    "source": ".",
    "predeploy": [
      "npm --prefix \"$PROJECT_DIR\" install",
      "npm --prefix \"$PROJECT_DIR\" run build"
    ],
    "runtime": "nodejs12"
  }
}
```

### 補足："runtime"について
サンプルコードの`"runtime"`は

```(Vercelのサンプルコード)/firebase.json
"runtime": "nodejs10"
```
とありましたが、今回は`"runtime": "nodejs12"`としています。
理由はpackage.jsonでnode.jsのバージョンを12と指定したからです。
バージョンを揃えないと`Your requested "node" version "10" doesn't match your global version "12"`というエラーが流れます。

## firebaseFunctions.js を作成する

プロジェクト直下に`firebaseFunctions.js`を作成します。
おそらくこれが肝となるファイルなのでしょう。
サンプルコードを見る限りそんな空気がプンプンしますが全くわからないのでコピペします。

```firebaseFunctions.js
const { join } = require('path')
const { https } = require('firebase-functions')
const { default: next } = require('next')

const nextjsDistDir = join('src', require('./src/next.config.js').distDir)

const nextjsServer = next({
  dev: false,
  conf: {
    distDir: nextjsDistDir,
  },
})
const nextjsHandle = nextjsServer.getRequestHandler()

exports.nextjsFunc = https.onRequest((req, res) => {
  return nextjsServer.prepare().then(() => nextjsHandle(req, res))
})
```

## next.config.js を作成する

`src`の直下に`next.config.js`を作成します。
**※ 作成場所に注意です。`src`の直下です。**

```/src/next.config.js
module.exports = {
  distDir: "../.next",
};
```

これが何のファイルなのかはさっぱりわかりません。
ただREADMEには必須のファイルだと記載されています。
![スクリーンショット 2021-03-24 0.18.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631600/b3069c19-3f57-e3fa-67b5-71707984e781.png)

※ ちなみにここにNext.jsのアプリは`src/`ディレクトリに入れてねってことが書かれています。

## いざ、デプロイ。

の前に、もしかしたら先にFirebaseのコンソール上からHostingとFunctionの「開始する」をクリックして画面を進めておく必要があるのかもしれません。
私はもうやっちゃってたので必要かどうかはわからず、もしかしたら必要ないのかもしれませんが2,3クリックの話なので先に進めておくことをおすすめします。

ではデプロイしていきます。

```
npm run deploy

//または

yarn run deploy
```

後は待ちます。
実はこれめっちゃ時間かかります。5分くらいかかった気がします。
（Vercelならこんなに待つことないのになーとか考えながら待ちます）

## デプロイされたアプリを確認

デプロイが完了したらFirebaseコンソールのHostingを開き、表示されているURLをクリックします。
![スクリーンショット 2021-03-24 0.47.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631600/331840c9-97c3-3f29-d6a3-c14f05e7f501.png)

問題なく作成したアプリが表示されていたら成功です。

#おわりに
ISRの件はとても残念でした。
~~大人の事情でVercelが使えないのですが、GAE + Cloud CDNなどで試してみたいと思います。~~
追記：2021/03/28　[Firebase HostingでNext.jsのISRっぽいことを実現する](https://qiita.com/centerfield/items/a7e4964d6c1963b70bbb)

あと最後に補足で、
サンプルコードのREADMEの一番上に書かれていたのですがテンプレートも用意されているみたいです。
もし初めからFirebaseにデプロイするとわかっている場合は

```
npx create-next-app --example with-firebase-hosting with-firebase-hosting-app
# or
yarn create next-app --example with-firebase-hosting with-firebase-hosting-app
```
で作成するのが楽だと思います。

-------
他に参考になった記事：https://blog.euxn.me/xsymsnvwjbkaaaaaaacrug