Next.jsを使ってCloud FunctionsとFirebase HostingでSSRする

# Next.js 9を使ってCloud FunctionsとFirebase HostingでSSRする

皆さんこんにちは、[nori](https://twitter.com/1amageek)です。
[FirebaseSummit](https://firebase.google.com/summit?hl=ja)のレジストレーションがオープンしましたね。僕は今年も参加しますよ！!
今年はスペインの開催ですねー
楽しみ。

最近フロントの環境設定をする機会があったので、登場したばかりの[Next.js 9](https://nextjs.org/)をCloud Functionsにのせてみました。

Cloud Functionsにのせるところまでは、すぐだったんですが[React](https://ja.reactjs.org/)の扱いが初めてだった僕にとってつまりどころがあったので、同じことをしようとしている人たちのためにこの記事を書くことにしました。

先にサンプルコードを置いておきます。

https://github.com/1amageek/ballcap.ts/tree/master/examples

## Firebase HostingとしてNext.jsを動かす

まず今回やったことを図として示します。

<img width="802" alt="Next.js on Firebase Hosting, Cloud Functions" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80287/e52a6f28-fef6-89ef-4cf9-db112916a1f8.png">

ポイントは以下の2つです。

- Next.jsをCloud FunctionsにのせてSSR
- REST APIもCloud Functionsを利用する

## Next.jsをCloud Functionsにのせる

実はこれはNext.jsのGitHubにサンプルが公開されているので全く難しくありません。
こちらのREADMEに従えば問題なく動作します。

https://github.com/vercel/next.js/tree/canary/examples/with-firebase-hosting

※公開当日は動かなかったんですが、僕がPRを送ろうとした頃にすでに日本の方がコミットされてました。🤩
先こされた。。
https://github.com/zeit/next.js/commit/cdf4f0a20d71b365e69828942ca7bf69498fa825

手順を簡単に説明します。

### Next.jsからテンプレートを取得

次のコマンドでサンプルコードを取得することができます。
このサンプルコードはそのままプロダクトに転用可能なので今回はこれを使いました。

npmの方はこちら

```
npx create-next-app --example with-firebase-hosting-and-typescript your-app
```

yarnの方はこちら

```
yarn create next-app --example with-firebase-hosting-and-typescript your-app
``` 

テンプレートを準備した時点でプロジェクトを実行可能になってます。次のコマンドで実行してみましょう。

```
npm run dev
```

このコマンドは、Next.jsを開発モードで起動するコマンドです。任意のブラウザが起動し、サンプルコードが起動するのが確認できればOKです。

### Cloud Functionsにデプロイ

プロジェクトはすでに起動することが確認できたのでデプロイすれば動くはずですね。
早速Firebase側の設定をやってみましょう。

`.firebaserc`を変更してFirebase プロジェクトの設定をしましょう。
この設定をせずにデプロイするともちろんエラーが出ます。

```JSON
{
  "projects": {
    "default": "YOUR_PROJECT"
  }
}
```

次のコマンドも忘れないように

```
firebase use default
```

これでFirebaseの設定も整いました。早速デプロイしてみましょう。

```
npm run deploy
```

Firebaseでは通常`firebase deploy`でCloud Functionsへデプロイしますが、Next.jsを利用する場合は`npm run`を利用した方が良さそうです。
`package.json`の`scripts`の定義を見ると、それぞれのコマンドの前に色々と処理が入っていることがわかります。

例えば、`serve`の実行前される、`preserve`では次の処理が行われています。

- clean
- build-public
- build-functions
- build-app
- copy-deps
- install-deps

```JSON
"scripts": {
    "dev": "next src/app",
    "preserve": "npm run clean && npm run build-public && npm run build-functions && npm run build-app && npm run copy-deps && npm run install-deps",
    "serve": "cross-env NODE_ENV=production firebase serve",
    "deploy": "npm run clean && firebase deploy",
    "clean": "rimraf \"dist\"",
    "build-app": "next build \"src/app\"",
    "build-app-permission": "chmod 755 dist/functions/next/static/media/*.svg",
    "build-public": "cpx \"src/public/**/*.*\" \"dist/public\" -C",
    "build-functions": "tsc --project src/functions",
    "lint-app": "tslint --project src/app",
    "typecheck-app": "tsc --project src/app",
    "lint-functions": "tslint --project src/functions",
    "copy-deps": "cpx \"*{package.json,package-lock.json,yarn.lock}\" \"dist/functions\" -C",
    "install-deps": "cd \"dist/functions\" && npm i"
  },
```

他のコマンドの実行に関してもスクリプトを経由した方が安全かと思います。
デプロイに関しては`firebase.json`に以下のように同じスクリプトが定義されているので実は安全に使えますが、念のためコマンドは入力方針は統一した方がいいでしょう。

```JSON
"functions": {
    "source": "dist/functions",
    "predeploy": [
      "npm run lint-functions",
      "npm run lint-app",
      "npm run typecheck-app",
      "npm run build-functions",
      "npm run build-app",
      "npm run copy-deps",
      "npm run install-deps",
      "npm run build-app-permission"
    ]
  },
```

デプロイしてFirebase HostingのURLを開ばNext.jsが動作していることが確認できるはずです。

#### Cloud FunctionsとFirebase Hosting

ここでCloud FunctionsとFirebase Hostingの関係性を説明しておきます。
Firebase Hostingは通常、静的コンテンツの配信を行う__Static Server__として機能します。
Firebase Hostingで動的コンテンツの配信を行う場合は`firebase.json`で設定の変更が必要です。

```JSON
"hosting": {
    "public": "dist/public",
    "rewrites": [
      {
        "source": "**/**",
        "function": "nextApp"
      }
    ],
```

テンプレートを利用してる時点で設定は終わっているので今回はこのまま利用可能です。

また、Cloud Functionsでは次のようになっています。

```typescript
import * as functions from 'firebase-functions'
import next from 'next'
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev, conf: { distDir: 'next' } })
const handle = app.getRequestHandler()

export const nextApp = functions.https.onRequest((req, res) => {
   console.log('File: ' + req.originalUrl)
   return app.prepare().then(() => handle(req, res))
})
```
これからわかるように、`https`を通したリクエストは全てNext.jsにコントロールが奪われていることがわかります。

### Cloud FunctionsをREST APIにも対応させる

Cloud Functionsでは[express.js](https://expressjs.com/ja/)を利用して__Path__をコントロールすることができます。
例えば次のように

```typescript
const express = require('express');
const cors = require('cors');

const app = express();

// Automatically allow cross-origin requests
app.use(cors({ origin: true }));

// Add middleware to authenticate requests
app.use(myMiddleware);

// build multiple CRUD interfaces:
app.get('/:id', (req, res) => res.send(Widgets.getById(req.params.id)));
app.post('/', (req, res) => res.send(Widgets.create()));
app.put('/:id', (req, res) => res.send(Widgets.update(req.params.id, req.body)));
app.delete('/:id', (req, res) => res.send(Widgets.delete(req.params.id)));
app.get('/', (req, res) => res.send(Widgets.list()));

// Expose Express API as a single Cloud Function:
export const widgets = functions.https.onRequest(app);
```
https://firebase.google.com/docs/functions/http-events?hl=ja

__express__を使えばうまくいきそうですが、`export`に注目すると

```typescript
export const nextApp = functions.https.onRequest((req, res) => {
   return app.prepare().then(() => handle(req, res))
})
```

```typescript
export const widgets = functions.https.onRequest(app);
```

`functions.https.onRequest`をいずれも占拠してしまっているため、共存できなさそうです。

今回は次のように設定しました。

__index.ts__

```typescript
import API from 'api'

const dev = process.env.NODE_ENV !== 'production'
const nextApp = next({ dev, conf: { distDir: 'next' } })
const handle = nextApp.getRequestHandler()

export const hosting = functions.https.onRequest(async (req, res) => {
  await nextApp.prepare()
  const app = express()
  app.use('/_', API)
  app.get('*', async (req, res) => {
    console.log('File: ' + req.originalUrl)
    await handle(req, res)
  })
  app(req, res)
})
```
__app.ts__

```typescript
const app = express()
app.post('/', (req, res, next) => {
  res.status(200).send("REST API")
})
export default app
```

こうすることで次のことができるようになります。

1. `/`で始まるパスのリクエストはNext.jsがコントロールする
2. `/_/`で始まるパスのリクエストはREST APIがコントロールする

これでNext.jsとREST APIをCloud Functionsに共存できるようになりました✨