事象
Reactで作成したWebアプリをAWS S3にデプロイしたところ、TOPページ以外のページでリロードした際に、404 Not Foundになりました。
404 Not Found
・ Code: NoSuchKey
・ Message: The specified key does not exist.
・ Key: ******
・ RequestId: ****************
・ HostId: ****************
ルーティングにはreact-router-domのBrowserRouterを使用しています。
原因
今回Reactで作成したWebアプリはSingle Page Application(SPA)です。
React Routerはクライアントサイドでのルーティングを提供するもので、URLに応じて新たにUIをレンダリングすることで、別ページへ遷移したかのように見せかけます。
S3上にはindex.html以外のページは存在しないため、リロードすると、S3がそのURLの先に置かれたファイルを探してしまい、404エラーが発生します。
BrowserRouterの代わりにHashRouterを使うことでも解決できるようですが、URLに#が入るのは綺麗ではありません。
解決策
方法1:エラードキュメントにindex.htmlを登録する
S3 > 該当のバケット > プロパティ > 静的ウェブサイトホスティングの編集を選択します。

エラードキュメントにindex.htmlと入力して変更の保存をクリックします。

この設定によって、エラーが発生した場合にindex.htmlが返されるようになります。
BrouserRouterはブラウザのアドレスバーに現在位置を保存し、ブラウザの履歴スタックを利用して遷移を実現しています。
index.htmlが返されても、アドレスバーに入力されているURLは変わらないため、リロード直前に表示されていたUIがレンダリングされます。
この方法は手軽な上、見かけ上は問題なく動作します。しかし、デベロッパーツールのコンソールを見てみると、依然としてリロード時に404エラーが発生しています。

そこで、次の方法も試してみました。
方法2:CloudFrontのカスタムエラーレスポンスを使用する
まず、CloudFrontのディストリビューションを作成します。
オリジンドメインには該当のS3バケットを指定してください。入力ボックスを選択すると、S3バケットの候補が表示されるので、該当のバケットを選択の後、Webサイトのエンドポイントを使用をクリックします。

デフォルトルートオブジェクトにはindex.htmlを入力します。

他の項目は適宜設定してください。
ディストリビューションの作成をクリックします。デプロイが終了するまでしばらく時間がかかります。
続いて、該当のディストリビューション > エラーページ > カスタムエラーレスポンスを作成と進みます。以下の図のように、404エラーに対してレスポンスコード200と/index.htmlを返すように設定します。

こうすることで、方法1のようにリロード時に404エラーが発生することがなくなります。
一方で、次のような問題点があるようです。
- エラーレスポンスを書き換えるため、ブラウザ側でエラーハンドリングを実装しようとすると機能しなくなる
- SPAと従来のウェブアプリケーションのハイブリッド構成の場合、意図しないレスポンスになることがある
どの方法にも一長一短がありますね。
参考