🌐

React Router 7 + Supabase + Cloudflare Workersを使ってアプリを作った話

2025/01/20に公開

社内で使うちょっとしたアプリを作るために、以下のような構成でやってみました。

  • フロントエンド
    • React Router 7
    • Cloudflare Workers
  • バックエンド
    • Supabase
    • Resend

それぞれどんなものかを説明しだすと長いので、AIに「〇〇とは?」で質問してもらうことにして、この記事ではそれぞれを使ってみてどうだったかを書きたいと思います。

React Router 7

https://reactrouter.com/home

もともとはRemixでプロジェクトを作り出したのですが、途中でReact Router 7の存在を知り、マイグレーションしました。

基本的には公式の Remixからのマイグレーション方法 の通りにやるだけでスムーズに移行できましたが、もともとRemixをnode.js向けで作っていたこともあり、Cloudflare Workers対応とReact Router 7のマイグレーションの組み合わせにハマりました。

React Router 7の公式には、Cloudflare Workers向けの情報がほとんどなく、サンプルコードやテンプレートもまだReact Router 7に対応できていない状況のようで、かなり手探りになりました。最終的に、RemixのCloudflare Workers向けのテンプレート + React Router 7のRemixからのマイグレーション方法を読み解いて、なんとか動くところまで行きました。

RemixもReact Router 7も、非常に開発体験が良く、これまでのフロントエンドで必要だった「状態の設計」がほとんど不要になっているのがラクチンでした。(これは Next.js などのSSRのフレームワークはどれもそうかも)

Next.jsと比べた際のメリットとしては、以下のような感想を持ちました。

  • フレームワークが要求する制約、ルールが少ない
  • ルートモジュールに対する型の自動生成が便利で、ルートの定義を変えた場合に型チェックで対象箇所がわかるのが強い
  • 同一ファイル内に loader, action, コンポーネントを書くスタイルが思った以上に書きやすく、どれがサーバーサイドでどれがクライアントサイドかもわかりやすい
  • サーバーオンリーモジュールは .server をつければよく、混乱しがちなサーバーサイド・フロントエンドの切り分けも比較的スムーズ

一方で以下のようなデメリットも感じました。

  • React Router 7のドキュメントがまだ足りない感じがあり、Remixのドキュメントを見なきゃいけない事が多かった(これは時間が解決しそうな気はする)
  • ミドルウェアの仕組みがなく、認証チェックのような複数ルート共通のロジックの置き場所に困る

個人的には、Next.jsのSSGありきの挙動に慣れない部分が大きいため、React Router 7のようなフレームワークは非常に理解しやすく、かつWEB標準を重視する思想にも大賛成という感じです。特に、これまでMPA (Thymeleafのような技術を使って、ページごとにサーバーサイドでレンダリングする方式) での開発経験が長い人ほど、同じようなマインドセットのままReactを使ったフロントエンド開発に入れるため、メリットが大きいように感じました。

Supabase

https://supabase.com/

Supabaseは前から使いたかったものの機会がなかったため、今回初めてちゃんと使ってみました。使ったのは以下の機能です。

  • ローカル環境
  • Database
  • Authentication
  • Realtime

ローカル環境

こういうクラウドサービスを使う場合って、ローカルでの開発時はどうやるの?が疑問だったんですが、ちゃんとdockerでの開発用環境が提供されてました。(あたりまえかもですが)

// 起動
npx supabase start

// 停止
npx supabase stop

// マイグレーションファイル・シードファイルからのDB再構築
npx supabase db reset

// マイグレーションファイルと現状のDBとの差分確認
npx supabase db diff

// 差分からマイグレーションファイルを作成
npx supabase db diff --use-migra -f update_tables

本番同等のGUI、開発用メールアプリ、コードベースとの連携などいろんなものがついていて、非常に使い勝手が良く、ああこれでいいんだなという感じがしました。

認証設定、メールテンプレートなど、本番ではGUIで変更できるものを、開発向けにはあえて設定ファイルで管理するようにしてるものがあり、何が設定ファイル管理になってるのかを探すのが若干難しい感じがしました。(Supabaseのドキュメントを見ていけばちゃんと書いてあるものの、概念が理解できてない状態では探しづらく、苦労しました。)

Database

開発の流れは以下のような感じになるのですが、これがすごく体験としてよいです。

  1. ローカルのSupabase上でDBを変更する(GUI or SQL)
  2. ローカルでアプリを起動して動作確認
  3. ローカルのSupabaseで、過去のマイグレーション履歴との差分をSQLファイルに抽出
  4. アプリの変更とマイグレーション用SQLをコミット
  5. マイグレーション用SQLを本番環境へpush

通常はマイグレーションSQLを先に用意して適用すると思うんですが、Supabaseの場合はGUIから直接DBに変更をかけてから、差分を元にSQLを作ってくれるのが非常にラクチンでした。
SupabaseではRLS (Row Level Security) を有効化することが推奨されているものの、よくわからなかったりするので、GUIで色々設定して試せるってのは大きいです。

Authentication

はじめは独自で認証機能を作り込んでいたんですが、将来的な拡張性を見越してSupabaseでやるように変えました。Supabaseの色んな機能はAuthenticationと緊密に連携するようになっているため、今から思うとはじめからやっておくべきだったなと思います。

メール送信機能も統合されてるし、ドキュメントも豊富だし、アプリを作るうえでは全く不足はないですね。

Realtime

DBへの変更をリアルタイムに画面に反映させるために使ってみました。こういうのって自前実装すると非常にめんどくさいんですが、Supabaseはすごく簡単で良いです。

SSRフレームワークでは、基本的にDBアクセスはサーバーサイドでやることになるものの、Realtimeはクライアントサイドで使う必要があるため、認証情報をどう引き継ぐかが課題でしたが、以下のような方法で乗り切れました。

export const loader = ({ context }: Route.LoaderArgs) => {
  // サーバーサイドでclientを作る
  const supabase = createServerClient(...);
  // client経由でセッションを取得
  const result = await supabase.auth.getSession();

  if (result.error || !result.data.session) {
    throw data("Unauthorized", { status: 401 });
  }

  // クライアントサイドでclientを作り、セッション維持するために必要な情報を渡す
  return data({
    supabaseUrl: context.cloudflare.env.SUPABASE_URL,
    supabaseAnonKey: context.cloudflare.env.SUPABASE_ANON_KEY,
    accessToken: result.data.session.access_token,
    refreshToken: result.data.session.refresh_token,
  });
};

export default function PageComponent({
  loaderData: {
    supabaseUrl,
    supabaseAnonKey,
    accessToken,
    refreshToken,
  },
}: Route.ComponentProps) {
  // サーバーから受け取った情報を元に、クライアントサイドでclientを作る
  const supabase = useMemo(() => {
    const supabase = createClient(supabaseUrl, supabaseKey);
    // アクセストークン、リフレッシュトークンを渡してセッションが維持されるようにする
    supabase.setSession({
      access_token: accessToken,
      refresh_token: refreshToken,
    });
    return supabase;
  }, [supabaseUrl, supabaseAnonKey, accessToken, refreshToken]);

  useEffect(() => {
    // Realtime機能を使う(サーバーのセッションを引き継いでアクセスできる)
    const channel = supabase.channel("db-channel")
      .on('postgres_changes', { event: '*', schema: '*' }, payload => {
        console.log('Change received!', payload)
      })
      .subscribe();

    return () => {
      channel.unsubscribe();
    };
  }, [supabase]);
}

Resend

https://resend.com/

アプリからの通知メール用に使いました。メール用にドメインが必要だったので、そのためにドメインも購入しました。SMTPサーバーを自前で用意するのはめんどくさいし、SPF, DKIM, DMARCとか、迷惑メール扱いされない設定を入れるのもめんどくさいので、その辺を指示通り進めるだけでいいというのは楽ですね。

SupabaseのカスタムSMTP設定との連携もあって、ラクチンです。

ちなみに、メールテンプレートの作成にはReact Emailを使いました。これも開発者向けにはおすすめ。

https://react.email/

Cloudflare Workers

https://www.cloudflare.com/ja-jp/developer-platform/products/workers/

Cloudflare Pagesは使ったことありましたが、Workersは初です。
wranglerを使ってローカル環境で確認、デプロイもしました。環境変数をどこで書くのか?がよくわからず苦戦しましたが、以下のような形で理解しました。

  • ローカル環境用:.dev.vars ファイル
  • 本番環境用(秘匿情報じゃないもの):wrangler.toml ファイル
  • 本番環境用(秘匿情報):Cloudflare WorkersのGUI設定

GUIで設定したものは、タイプを「シークレット」にしておかないと、次回デプロイ時に消えてしまうっぽい。

せっかく独自ドメイン取得したため、アプリも独自ドメインで配信しようとして、色々ハマりました。
最終的には以下のような形でうまくいきました。

  • ネームサーバーをCloudflare DNSに変更
  • サブドメインをCNAMEとして設定、ターゲットをルートドメインにする
  • Workersルートの設定で、サブドメイン配下すべてをアプリのWorkerに転送するように設定

これで無事にサブドメインでアクセスできるようになりましたが、うまく行かなかった原因がなんなのか切り分けできてないため、実は不要なものもある気がします。

まとめ

いろいろクラウドサービスを活用してアプリ公開までやりましたが、いろんなものが開発体験としてはすごく良く、アプリの中身の開発に集中できるようになってることを感じました。

一方で、SupabaseもCloudflare Workersも、ロックインされちゃいそうな独自のところが色々あるなーという感じです。そのサービスの強みの部分でもあるのである程度はいいですが、別のものに切り替えるハードルが結構ありそう。アプリの作りとして、個別サービス依存の部分をいかに減らすか、コアから追い出すかというのが重要そうに感じました。

Discussion