電通総研 テックブログ

電通総研が運営する技術ブログ

AWS + React Router v7でシステム構築した際につまずいたこと

はじめに

こんにちは。エンタープライズ第一本部・テクノロジービジネスユニット・テクノロジービジネス2部の多田圭佑です。
掲題の構成でシステムを構築するにあたり、基本的な構成であるにもかかわらず思わぬところでつまずいてしまったので、将来同じような問題に遭遇した方向けに書いています。
5分程度でサクッと読めますので、お付き合いいただければ幸いです。
本記事は電通総研 Advent Calendar 2025 10日目の記事です。

つまずいたこと2つ

先に私がつまずいたことの2つを述べます。

  1. CloudFront経由だと画面遷移でエラーになる
  2. 画像が表示されない

システム構成

以下のような、AWSでは一般的な構成です。

(Internet) -> CloudFront -> ALB -> ECS

ECS上にReact Router v7(SSR。ビルドツールはVite)を使用したシステムが稼働しており、それをCloudFront経由で公開しています。
CloudFrontはこのシステム以外でも使用しており、特定のパスパターンに合致した場合のみこのシステムにルーティングするよう設定しています。

1. CloudFront経由だと画面遷移でエラーになる

事象

前提としてCloudFrontでのパスパターンは後段で維持されます。
例えば、パスパターンを「aaa/bbb」としたときは以下のように転送されます。
(HTTPSの終端はALBで、バックエンド(ECS)との通信はプライベートネットワーク内のHTTPです)

AWSサービス URL
CloudFront https://example.cloudfront.net/aaa/bbb
ALB https://hogehoge.region.elb.amazonaws.com/aaa/bbb
ECS http://localhost:8888/aaa/bbb

逆に言えば、CloudFrontを経由した場合は http://localhost:8888/aaa にはアクセスできません。
当初、私がルーティングを実装した際は以下のように routes.ts を設定しました。
こうすることで/aaa/bbbをベースとしてその配下のパスにアクセスできます。

export default [
  route('/healthcheck', 'routes/healthcheck.tsx'), // healthcheck用
  ...prefix('aaa/bbb', [
    index('routes/index.tsx'))
    route('/ccc', 'routes/ccc.ts'),
    route('/ddd', 'routes/ddd.ts'),
    ]),
] satisfies RouteConfig;

ただ、CloudFront経由だとインデックスへのアクセスは問題ないのですが、そこからの画面遷移に失敗していました。
ローカルやALB経由だと問題ありませんでした。

原因

React Router v7のルート・コードスプリット(遅延ルート)では、クライアントは起動時に /__manifest というエンドポイントから「ルートID → チャンクURL」のマップを取得します。
今回はパスパターン経由で公開しているため/__manifest にアクセスしようとするとパスパターンに合致しないため404となってしまい、その結果チャンクのURLが分からずに動的 import が失敗していました。
ローカルやALB経由だとパスパターンの制約がないため問題がありませんでした。

対応

React Router v7では、アプリケーションのベースパスを指定するbasenameという設定があります。
今回の構成では、CloudFrontから/aaa/bbbというパスパターン経由でこのWebアプリケーションにアクセスしているため、このbasenameを設定することで問題を解決しました。
具体的には、react-router.config.tsに以下のように設定を追加しました。

import type { Config } from "@react-router/dev/config";

export default {
  ssr: true,
  basename: "/aaa/bbb",
  // その他の設定...
} satisfies Config;

routes.tsは以下のようにシンプルになりました。

export default [
  route('/healthcheck', 'routes/healthcheck.tsx'), // healthcheck用。/aaa/bbb/healthcheck
  route('/ccc', 'routes/ccc.ts'),
  route('/ddd', 'routes/ddd.ts'),
] satisfies RouteConfig;

この設定により、React Routerは/__manifestへのアクセスを/aaa/bbb/__manifestとして解決するようになり、CloudFront 経由でも正しくマニフェストファイルを取得できるようになりました。
また、リンクやナビゲーションも自動的に/aaa/bbbを基準としたパスで動作するようになるため、ルーティング全体が正常に機能するようになりました。

2. 画像が表示されない

事象

ルーティングの問題は解決しましたが、今度は画像が表示されない問題が発生しました。
/images/icon.svgを取得しようとして404となっていました。

原因

原因を調査したところ、画像などの静的アセットファイルはReact Routerのルーティングとは別に扱われるため、basename の設定だけでは対応できないことがわかりました。
静的ファイルは通常publicディレクトリに配置されますが、これらのファイルへのパスも /aaa/bbbプレフィックスを考慮する必要があります。しかし、ビルド時に生成される静的アセットへの参照パスは、basenameの設定では反映されません。

解決方法

この問題を解決するために、以下の対応を行いました。

1. Vite の base 設定を追加

vite.config.tsbaseオプションを追加して、静的アセットのベースパスを明示的に設定しました。
react-router.config.tsとは若干異なるため注意です)

import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";

export default defineConfig({
  base: "/aaa/bbb",
  plugins: [reactRouter()],
  // その他の設定...
});

この設定により、ビルド時に生成される静的アセットへの参照パスに/aaa/bbbプレフィックスが自動的に付与されるようになります。

2. 画像パスの設定

コンポーネント内で画像を参照する際は、以下のように画像を直接インポートすることで、Vite が自動的に適切なパスに変換してくれます。

import iconSvg from "/images/icon.svg";

function MyComponent() {
  return <img src={iconSvg} alt="Icon" />; // <img src="/aaa/bbb/images/icon.svg" />
}

以下のように環境変数を使用して文字列ベースで解決する方法もあります。こちらは動的に画像パスを生成する必要がある場合に有効です。

function MyComponent() {
  return <img src={`${import.meta.env.BASE_URL}/images/icon.svg`} alt="Icon" />;
}

まとめ

CloudFrontのパスパターンを使用している場合はbasenameの設定が必要でした。この設定により/__manifestがprefix付きとなり、マニフェストファイルの取得やルーティングが正常に動作します。
また、静的ファイルの参照についても同様にbasenameを考慮した設定が必要になります。

これから同様の構成でシステムを構築される方の参考になれば幸いです。

私たちは一緒に働いてくれる仲間を募集しています!

電通総研 キャリア採用サイト 電通総研 新卒採用サイト

執筆:@tada.keisuke
レビュー:@kinjo.ryuki
Shodoで執筆されました