🛠️

React SPA の環境設定をビルド後にデプロイ環境ごとに変える

2024/12/18に公開

この記事は、Magic Moment Advent Calendar 2024 13 日目の記事です。

こんにちは!

Magic Moment でソフトウェアエンジニアをやっている Miyake です。

今回は React アプリケーションのデプロイに関する内容を書かせていただきます。Tips 的な内容なので、気楽に読んでいただけたら幸いです。

前提: React アプリケーションにおける環境変数の扱い

様々なフレームワークやビルドツールがある React ですが、基本的にローカル環境においては環境変数を用いてランタイムで設定や挙動を変えることができます。

一方でデプロイ時 React アプリケーションをビルドする際には、各環境ごとに設定した環境変数がソースコードに埋め込まれるため、各環境ごとにアプリケーションをビルドする必要があります。

本記事では、同一アーティファクトの React アプリケーションで、環境変数用いて環境ごとに設定を変える方法について説明します。

※今回話す弊社のフロントエンドアプリケーションは、Vite を開発環境とビルドツールに用いた React SPA です。

モチベーション

なぜ環境ごとにビルドせず同一アーティファクトを利用したいかは、弊社のデリバリーパイプラインの設計が関係します。

弊社のサーバーアプリケーションは全てコンテナ化されており、リリースタグが切られた時点でビルドしたコンテナイメージを development -> staging -> production と各リリースステージに昇格していく形を取っています。

今回の話の背景を端的に言うと、このデリバリーパイプラインの形をフロントアプリケーションでも適用させたい、というものになります(同一アーティファクトを各リリースステージに昇格させていきたい)。

実現方法

簡単に言うと、配信用の Web サーバーアプリケーションを構築し、そのサーバーアプリケーションの環境変数で動的に値を変えるというものです。React アプリケーションの配信とは別に、環境変数を window オブジェクトにセットしたスクリプトファイルを配信します。

以下、簡略化していますが具体的な実装コードとなります。

// env.ts (client / server 共用ファイル)
export const envs = {
  API_URL: process.env.VITE_API_URL || window.API_URL,
};
// server.ts(Web サーバー)
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
import { Hono } from 'hono'

import { envs } from '~/path/to/env';

const app = new Hono()

app.use('/assets/*', serveStatic({ root: './dist' }))

app.get('/config.js', (c) => {
  c.header('Content-Type: text/javascript');

  let script = '';
  
  for (const [key, value] of Object.entries(envs)) {
    script += `window.${key} = '${value}';`;
  }

  return c.text(script);
});

app.get('*', serveStatic({ path: './dist/index.html' }))

serve({
  fetch: app.fetch,
  port: 8080,
}, (info) => {
  console.log(`listen on ${info.address}:${info.port}`);
});
<!-- index.html(エントリーポイントファイル) -->
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React Application</title>
    <script type="text/javascript" src="/config.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/assets/app.js"></script>
  </body>
</html>

ポイントとしては、環境変数を設定している env.ts です。

export const envs = {
  API_URL: process.env.VITE_API_URL || window.API_URL,
};

このファイルの処理はローカル開発環境とプロダクション環境で変わります。

まず、Web サーバーアプリケーションについては、アプリケーションそのものをローカル開発時は使いません。プロダクション環境では環境変数に設定した VITE_API_URL の値が process.env から参照できるので、その値を window オブジェクトにセットしたスクリプトファイル(config.js)をフロント側に配信します。

そして フロント(React)アプリケーションについては、ローカル開発時は VITE_API_URL を環境変数にセットすることで process.env.VITE_API_URL の値が使われます。一方で、プロダクションビルド時には環境変数を設定せずにビルドします。それにより process.env.VITE_API_URL の値は空となるため window.API_URL が使われるようになり、Web サーバーから配信されたスクリプトファイルの値が使われる形となります。

最後にリリースに関してですが、弊社では Web サーバーアプリケーションと、ビルドした React アプリケーションをコンテナイメージにパッケージングして、サーバーサイドと同様のデリバリーパイプラインに乗せる形で運用しています。

最後に

Next.js だったり Server-Side Runtime が使えるフレームワークを利用していれば、他にも色々手段があると思うのですが、シンプルな React SPA のプロダクションビルドにて環境変数を利用する方法についてあまり情報がなかったので、紹介させていただきました。

利用しているプラットフォームやリリース運用方法によっては本記事の内容はマッチしない可能性はありますが、弊社と似たような運用している方に参考になれば幸いです。


次の弊社のアドベントカレンダー記事もお楽しみに!

次回のアドベントカレンダーは otsuka さん の「Cloud Profilerを使ってメモリリークを特定したい!」です。
お楽しみに!

Discussion