まったくわからないなりにAWSのS3上にReactのアプリをデプロイした話

## はじめに
S3上にreact-routerを使ってルーティングしたReactアプリをデプロイするのにめちゃくちゃ苦労したので、どのように実現したかを記録します。

なお、使用していたはクラウドのサービスはAWSのS3、そしてCloudFrontです。
アプリはviteを使って構築し、React、TypeScriptで構成。react-routerを使ってルーティングをしています。

## やりたかったこと
作っていたのは以下のようにTopページと、各機能のページに遷移できるボタンがあるような簡易的なWebアプリです。
<img width='40%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/fe27cf7c-b90d-4e34-a818-1ebe1d631b54.png'>

一部省略していますが、コードのイメージとしては以下のような形でした。
```react
// ルーティングの設定
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`が正しいことになります。

よって、コードは以下の通りになりました。
```diff_jsx
// ルーティングの設定
const MainRoutes = [
 { path: "/", element: <Top />},
+  { path: "/index.html", element: <Top />},
  { path: `/pageA`, element: <PageA />},
  { path: `/pageB`, element: <PageB />},
  { path: `/pageC`, element: <PageC />}
];
```
`/`はローカル環境で動かす際に必要なので残しています。

今回はこのようにして解決しましたが、実はこの問題は「静的ウェブサイトホスティング」を利用することで解消することができます。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/HostingWebsiteOnS3Setup.html

しかし、私のやっていたプロジェクトではこの対応をなぜか採用していなかったため、静的ウェブサイトホスティングを利用せず、ルートパスが`/index.html`になるものとして対応をしました。

### ルーティングの理解
:::note warn
この部分に関しては、現時点でも完全に理解が及んでいないので不正確な部分があるかもしれません。

:::

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の詳細な設定方法は省略しますが、以下のような処理を作成しました。
```javascript
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`です。

```diff_jsx
// 画面描画
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上に配置したアプリを正しく閲覧できるようになりました。
ただ、本当にいろいろ試した結果できるようになったので、もしかするとやったけど書ききれていない設定があるかもしれません。

業務でデッドラインが決められている中、知見がある人もおらず、手探りで作業をするのはかなり苦しく、辛いものがありますが、やり遂げられたときの達成感はなんとも言えません。

ただ、反省点として作業記録を細かくつけられなかったので、今後に活かせるようにするためにも、焦らずやったことを逐一まとめていきたいと思いました。