🧳

NextjsのSSGホスティングで躓いた話

2023/06/15に公開

はじめに

突然ですが、Nextjsを利用するケースってどんな背景がありますか?

  • Reactの開発環境をシンプルに整えたい。
  • Nextjsの提供するルーティングを利用したい。
  • SSRで実装したい。

いろいろあると思いますが、NextjsがないころからReactの開発をする私としては、Webpackをイチから構築するあの手間が省けるのがなによりの恩恵と思っています。

昨今、フロントエンドの技術選定でReactやNextjsが選定にあがることは普通で、開発に関する情報も非常に多く扱われていると感じます。

そんなNextjsですが、ホスティングの話になると急に情報が乏しくなっている気がします
そんなわけで、今回はフロントエンド開発にホスティングを絡めた話をしようと思います。

SSGにおけるホスティングは?

わたしはSSRよりはSSGを用いることが多いのですが、自身の経験からは以下の2択かなと思ってます。

  • 専用ホスティングサービスの利用
  • マニュアルホスティング

専用ホスティングサービスの利用

ひとつめはVercelのようなプロバイダーへのホスティングです。
大半の方はこちらを利用しているのではないか、と感じています。
https://vercel.com/

Vercelはホスティング、ルーティング、デプロイ管理を包括的におこなってくれます。
インフラに疎くても、Webコンソール上でGitHubリポジトリを指定するだけでプロダクトが完成するので、フロントエンドエンジニアには大変魅力的です。
個人利用であれば無料で開始できることも人気の要因かと思います。

AWS Amplifyというのもある

それと類似の機能を提供してくれるのがAWS Amplifyです。
バックエンドでAWSを利用している場合など、インフラが散らばらないので非常にメリットが高いです。
https://aws.amazon.com/jp/amplify/

こちらも同じくデプロイ管理までを包括するNextjsのプロダクトを自動で判別し、コードからルーティング解決も自動でおこなってくれます。

Amplifyでは指定した構成ファイルからプロダクトがNextjsを利用していることを推論し、利用があった場合はSlugに対応したルーティングを提供してくれます。
内部挙動は隠蔽されていますが、Lambda@Edgeで手続き的におこなった処理を完全に代行してくれます。

業務でSSGホスティングする際は、検討すべきツールかと思います。

マニュアルホスティング

ホスティングサービスが利用できない場合、最も基本的な構成がS3とCloudfrontによるデリバリーになると思います。Amplifyもアプリの自動構築後には内部的にcloudfrontの作成をおこなっており、おおよそ同じ構成であることがわかります。

ピュアな構成であるため、Amplifyよりも柔軟にカスタマイズが可能で、コストも若干安くはなります。
ただし、Nextjsは自力でのホスティングを考えるた途端、いろいろ厄介になります。

そう、ここからがようやく表題の「躓いた話」となります

Cloudfrontでルーティング

CloudfrontのLambda@Edgeを使って、リクエストを事前に検証して置き換えるrewriteのようなことができます。S3へのホスティングをおこなう際はこちらを用いてルーティングを設定するのが定石かと思います。

以下のようなコードを用意しました。

exports.handler = (event, _context, callback) => {
  const { request } = event.Records[0].cf;
  request.uri = request.uri
    .replace(/^([a-z0-9\-/]{1,})$/, '$1/index.html')
    .replaceAll('//', '/')
    .replace(/\/[0-9]{1,}\/index.html$/, '/[number]/index.html');

  callback(null, request);
};

解説すると、

  • URLは a-z 0-9 - の文字列を想定し、一意に index.html を付与する。
  • URLの末尾が / かどうか配慮しないかわりに、ダブってたら1つに変換する処理を実施。
  • 末尾階層のパスが数字のみの場合はSlugとみなして [numebr] という文字列に置き換える。

これで単一Slugまでなら恒久的に対応可能だ!(…と思ってましたw)

Lambda@Edgeのタイムアウト

上記のコードを実装したところ、504エラーが頻発するようになってしまいました。
内容としては、Origin Request(上のコード)のタイムアウトによるものでした。

こちらの例示であれば、URLは下記のようにrewriteされるのが期待値になります。

/user/change-mail/123/

/user/change-mail/[number]/index.html

ですが、実際はreplace処理のコストが非常に高く、上記くらいの文字数では処理がLambda@Edgeのタイムアウトに間に合いませんでした。

Lambdaのデフォルトタイムアウトは3秒ですが、上記の処理にはなんと9秒ちかく費やしていたことをインフラ対応いただいたメンバーから伺いました。

ここから以下のようなリプレイスをかけました。

  • 正規表現を極力使わない。(数字判定 /[0-9]/ だけは残念ながら利用)
  • / でスプリットしたディレクトリ群を評価するようにして、極力長い文字列を比較しない。

コードは固有のパス名が多くベタ書きされ、パフォーマンスは劇的に改善しました。
(代償として、見栄えは劇的に悪くなりました…)

react-routerをあわせて使う

上記の困難を経て、別プロダクトでおこなった事例になります。
Lambda@Edgeを如何に簡潔にするか、というところからこちらの発想に至りました。

nextjs+react-routerという邪道な組み合わせのため、口外するか迷いのあった手段ではありますが、この流れだとむしろ妥当性が見えてくるのではないかと思います。

ルーティングを一元化し、完全なSPAにする

Nextjsではコンポーネントのロード時にSSRを無効にするオプションも提供されています。
こちらを利用して、SPAなレンダリング環境を構築しましょう。

import dynamic from 'next/dynamic'
 
const DynamicHeader = dynamic(() => import('../components/header'), {
  ssr: false,
})

https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#with-no-ssr

react-routerの <BrowserRouter /> を利用する

<BrowserRouter /> はブラウザのhistory機能を利用したルーティングをReact上で再現する機能です。ただし、URLが変わってしまうとSPAのエントリーポイントのindex.htmlを実行しなくなってしまうので、こちらはサーバーによるrewriteの実施が前提となります。
https://reactrouter.com/en/main/router-components/browser-router

rewiteが簡潔に書ける

こちらでは以下のようなLambda@Edgeを用意しました。

exports.handler = (event, _, callback) => {
  const { request } = event.Records[0].cf;
  const pathArray = request.uri.split('/');

  const escapeDirs = ['_next', 'assets'];

  // 指定のディレクトリに含まれるファイルはそのまま返す
  if (escapeDirs.indexOf(pathArray[1]) !== -1) {
    callback(null, request);

    return;
  }

  request.uri = '/index.html';

  callback(null, request);
};

すべてのアクセスをおなじindex.htmlに向けるだけなので、非常にシンプルです。
当然ながら、アセットやJSファイルへのアクセスは除外しています。

こちらでもらったツッコミとしては 「Nextjsつかわないでよくない?」 でしたが、前述した通りReactの開発環境をシンプルに整える事のできる点のみでもNextjsの利用は十分魅力的で、わたしとしては ルーティングライブラリとの併用はアリ だと思っています。

総括

以上のように、Nextjsのホスティングには常に苦しんできました。
正直、キレイではないプラクティスを晒していますが、わりと現場の実情だったりします。

悩めるフロントエンドエンジニアの一助となればと思い、こちらの記録を残します。

Discussion