はじめに
S3上にreact-routerを使ってルーティングしたReactアプリをデプロイするのにめちゃくちゃ苦労したので、どのように実現したかを記録します。
なお、使用していたはクラウドのサービスはAWSのS3、そしてCloudFrontです。
アプリはviteを使って構築し、React、TypeScriptで構成。react-routerを使ってルーティングをしています。
やりたかったこと
作っていたのは以下のようにTopページと、各機能のページに遷移できるボタンがあるような簡易的なWebアプリです。

一部省略していますが、コードのイメージとしては以下のような形でした。
// ルーティングの設定
const MainRoutes = [
{ path: "/", element: <Top />},
{ path: `/pageA`, element: <PageA />},
{ path: `/pageB`, element: <PageB />},
{ path: `/pageC`, element: <PageC />}
];
// ルーティング用のコンポーネント生成
const Routes = () => {
return useRoutes(MainRoutes);
};
// 画面描画
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<Routes />
</BrowserRouter>
</React.StrictMode>
);
ボタンを押下したときに、それぞれのurlのページに飛ばし、react-routerをつかってそれぞれのパスに応じたコンポーネントを表示させる、というような作りです。
ローカルの環境ではこれでも問題なく動いていました。
しかし、これをS3の環境に上げたときまったく想定通りの挙動にならず、調査に途方もない時間がかかったので、一つずつ紐解いていきます。
S3上でのhtmlファイルの扱いへの理解不足
ルートパスの理解
AWSのS3では「バケット」と呼ばれる入れ物にファイルをアップロードしていきます。
そして、そのファイルへのアクセスを許可することで、簡単な静的ウェブサイトならそのまま公開することができます。
ただ、あくまでもただのファイルを公開しているだけなので、ファイルパスがそのままURLになります。
例えば通常のウェブサイトであれば
https://hogehoge.jp/react-app
というURLにアクセスすることで、用意しているindex.htmlが表示されるようになっています。
ところが、S3の場合にはindex.htmlそのものへのアクセスになるため、
https://hogehoge.jp/react-app/index.html
というURLの形式になります。
※S3のバケットのURLをhttps://hogehoge.jpというドメインに結びつけ、react-appというフォルダ形式のオブジェクトを作成し、その下にファイルを配置しているものとします。
このとき、ルートパスは/ではなく/index.htmlになるのです。
そのため、TOPページのコンポーネントのルーティング対象のパスは/ではなく、/index.htmlが正しいことになります。
よって、コードは以下の通りになりました。
// ルーティングの設定
const MainRoutes = [
{ path: "/", element: <Top />},
+ { path: "/index.html", element: <Top />},
{ path: `/pageA`, element: <PageA />},
{ path: `/pageB`, element: <PageB />},
{ path: `/pageC`, element: <PageC />}
];
/はローカル環境で動かす際に必要なので残しています。
今回はこのようにして解決しましたが、実はこの問題は「静的ウェブサイトホスティング」を利用することで解消することができます。
しかし、私のやっていたプロジェクトではこの対応をなぜか採用していなかったため、静的ウェブサイトホスティングを利用せず、ルートパスが/index.htmlになるものとして対応をしました。
ルーティングの理解
この部分に関しては、現時点でも完全に理解が及んでいないので不正確な部分があるかもしれません。
react-routerはフロント側のみでルーティング処理を行ってくれるモジュールです。
冒頭で示したアプリのようにページAボタンを押下して、https://hogehoge.jp/react-app/pageAのURLに飛ぶことを考えます。
バックエンドで処理をする場合、pageA.htmlを探して、その中身を表示します。
react-routerの場合はindex.htmlを表示したまま、設定したパスに紐づくコンポーネントをJavaScriptの処理で表示させます。
これが、フロント側のみでルーティングができる仕組みの一端です。
しかし、S3上では話が変わってきます。
Aボタン押下でhttps://hogehoge.jp/react-app/pageAのURLを呼ぶと、S3上でreact-appオブジェクトの下にあるpageAオブジェクトを探しに行きます。
つまり、index.htmlから離れてしまうのです。
これでは、困るのでどんな場合でもindex.htmlを返すようにする必要があります。
ここで登場するのがCloudFrontFunctionsです。
CloudFrontはAWSでコンテンツを配信するためのサービスで、そこに紐づけて様々な処理を実施できるのが、CloudFrontFunctionsになります。
冗長になるのでCloudFrontFunctionsの詳細な設定方法は省略しますが、以下のような処理を作成しました。
function handler(event) {
var request = event.request;
var uri = request.uri;
if (uri.endsWith('/')) {
request.uri += 'index.html';
} else if (!uri.includes('.')) {
request.uri = '/index.html';
}
return request;
}
端的に言えば、URLの末尾にindex.htmlを付与するような処理です。
これにより、https://hogehoge.jp/react-app/pageAを呼び出した際、pageAのオブジェクトを探しに行くのではなく、常にindex.htmlを返すようにできます。
そうすることで、react-routerの処理も正常に働き、PageAのコンポーネントを表示することができます。
ベースURLの理解不足
react-routerを使うにあたって、ベースとなるURLを理解しておく必要があります。
遷移先のパスの指定時に/や/pageAと書きますが、このパスより前に指定されるパスが何なのか、という話です。
今回index.htmlを配置したのはhttps://hogehoge.jp/react-appです。
basenameを指定しなかった場合、直前のパスで想定されているのはhttps://hogehoge.jpになります。
よって、/pageAとして遷移対象になるのはhttps://hogehoge.jp/pageAが来たときになってしまいます。
これでは困るので、そのときに指定するのがreact-routerのbasenameです。
// 画面描画
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
- <BrowserRouter>
+ <BrowserRouter basename="/react-app">
<Routes />
</BrowserRouter>
</React.StrictMode>
);
今回はS3のメインのバケットではなくそのひとつ下のオブジェクトであるreact-appの下に配置しています。
このようにサブディレクトリがある場合は、それをbasenameに指定します。
すると、https://hogehoge.jp/react-app/までが補完されることになり、正しく遷移することができるようになります。
まとめ
いろいろ調べながらやって、苦しみ抜いた末に、なんとかS3上に配置したアプリを正しく閲覧できるようになりました。
ただ、本当にいろいろ試した結果できるようになったので、もしかするとやったけど書ききれていない設定があるかもしれません。
業務でデッドラインが決められている中、知見がある人もおらず、手探りで作業をするのはかなり苦しく、辛いものがありますが、やり遂げられたときの達成感はなんとも言えません。
ただ、反省点として作業記録を細かくつけられなかったので、今後に活かせるようにするためにも、焦らずやったことを逐一まとめていきたいと思いました。