梨のコンポート

React Router DOMでSPAをS3+CloudFront環境に展開する際の注意事項

Cover Image for React Router DOMでSPAをS3+CloudFront環境に展開する際の注意事項
最終更新日時:

React RouterでSPAにルーティングを作成したところ、ローカルのデバッグ環境では所定のパスにアクセスできたものの、AWS環境に展開した後同じパスにアクセスできないトラブルが発生した。この記事では、その原因と解決策を紹介する。

React Routerとは

React Routerの解説ではないので簡単に説明する。

React RouterはReactで作成したWebアプリケーションにルーティング(パスの移動。たとえばURLに/foobarと指定して、そのページに移動することなど)を導入するためのパッケージである。具体的には、URLと表示するReactコンポーネントを対応付けることで、ブラウザのURLバーの変化に応じて画面を切り替えるためのライブラリである。

通常のWebページで同様のことを行うには、ドキュメントルート内にフォルダを作成し、その中にページを配置すればよい。一方、Reactでは一般的に1つのHTMLファイル上にSPA(Single Page Application)としてアプリケーションを構築する。そのため、URLごとに別々のHTMLページを用意するのではなく、React Routerを使って1つのSPAの中に仮想的な複数ページを定義するのが一般的である。

対策としてNext.js等のフレームワークを活用する方法もあるが、シンプルなアプリを作りたいだけの場合にはやや大袈裟なことも多い。パスが2つ程度で良いシンプルなアプリであれば、React RouterによりSPA内に仮想的に別個のページを用意するといった形で対応するのがよい。

本記事では、このReact Routerを使ったSPAをS3+CloudFrontで配信する際に起きる問題と、その対策を扱う。

想定読者と前提

本記事は以下の経験を前提とする。

  • Reactを触ったことがある
  • S3+CloudFront で静的サイトをホスティングしたことがある

準備

使用したパッケージのバージョンは以下の通り。pnpm listの結果より抜粋。

react 19.1.1
react-dom 19.1.1
react-router-dom 7.9.4
vite 7.1.4

src配下のmain.tsxに以下の実装を行う。

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import App from './App.tsx'
import { Contact } from './pages/Contact'
import { ErrorPage } from './pages/ErrorPage'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />} />
        <Route path="/contact" element={<Contact />} />
        <Route path="*" element={<ErrorPage />} />
      </Routes>
    </BrowserRouter>
  </StrictMode>,
)

/でメインのアプリにルーティングし、/contactで連絡先を記載したページにルーティングする。他のパスにアクセスするとエラーページにルーティングする。

インフラ構成は、S3+CloudFrontによりS3配下のHTMLファイルを静的ページとして配信するものである。

発生事象

vite buildコマンドで生成したdist/以下のファイルをS3バケットに配置し、/contactにアクセスしたところ、連絡先ページではなく以下のページが表示され、ステータスコードとして403を返した。

<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
</Error>

原因

このエラーページはReactが返しているのではなく、S3が返しているものである。前述の通り、React Routerは実際のファイルとしてページを作成するのではなく、SPA内に仮想的なページを定義する仕組みである。そのため、ファイルとしては/contactパスに対応するページは存在しない。結果として、/contactのパスにアクセスした際は、ページを開くことができず、S3側のファイル不在エラーが返される。

すなわち、/contactのパスのページを開こうとすると、SPA内のReact Routerの実装で指定したページを開くより前に、S3側のルーティングが実行されてしまい、ページが不在であるという旨のエラーが表示される。

対策

任意のパスについて、S3のルーティングよりもReact Routerのルーティングを優先させるように設定する。これを実現するためには、S3のルーティングでステータスコード403などを返した場合でもSPAのindex.htmlページを返し、React Routerのルーティングを強制すればよい。

AWSの管理画面より当該ディストリビューションを開き、エラーページタブから「カスタムエラーレスポンスを作成」をクリックする。HTTP error codeには403を指定し、TTLは短めの時間(例: 300秒ほど)を設定する(あまり長いと新規ページ作成時に更新が反映されないので注意する)。Customize error responseYesとし、Response page pathindex.html(200を返すときと同じページ)とし、HTTP Response codeは200に設定する。

エラーコード404に対しても同様の設定を行った際のスクリーンショットを下に示す。

CloudFrontディストリビューションに設定した例

対策の限界

この方法では、/(ルートページ)と/contact以外のページ(Router設定ではErrorPageとしたページ)へアクセスした場合でも、ステータスコードは200を返す。 これはSEO的に問題がある。そのため、ある程度以上高機能なサイトを実装したい場合はNext.jsを利用するのがよい。あくまで簡便なツール等を作る用途に留めるべきである。

まとめ

S3+CloudFront環境でReact Routerが適切にルーティングしない問題の原因と、その対策を紹介した。