サーバーFW経験者がRemix(React Router v7) でSSRやると幸せになれるかもよという話

本記事は、[KDDIアジャイル開発センター（KAG)Advent Calendar 2024](https://qiita.com/advent-calendar/2024/kag)1日目の記事です。
また、念の為こちらの記事で書いたことは、現職での経験ではなく過去のものであることにあらかじめご留意いただけますと幸いです。

## 概要
みなさんは技術選定をするときどんなことを考えていますか？
どんなに悩みきっても結局のところ唯一絶対の正解など存在しないもの。それが技術選定かと思います。

この記事では、一人のバックエンドエンジニアが、ひょんなことからフロントエンドメタフレームワークの技術選定をすることになって考えたことや、採用したRemixのどこが気に入ったかなどを書いていきたいと思います。


## 選定の背景
当時の選定の背景を簡単に書くと以下のような状況でした。

### 作りたかったアプリケーションの特徴
1. 業務用ではなく不特定多数の顧客提供用のアプリケーション
2. 多くのリクエストが集中することはあまりない
3. 利用ターゲットが広く設定されているかつ都市圏で利用される想定ではなく、十分なネットワークおよびハードウェア要件について、通常のアプリケーションより多少シビアに考える必要がある


### 当時のチームスタックとモチベーション
- チームメンバーの技術スタック
    - 自分含めバックエンドFW経験者がほとんどの現場
    - Reactは触ったことがあるが社内プロジェクトの簡単なSPAアプリ程度

- モチベーション
    - 積極的にモダンなフロントエンドUIコンポーネント(React等)を採用していきたいが、いきなりのメンタルチェンジは厳しいと感じる(例えば、複数あるレンダリングパターンを理解して使いこなしたり、適切な粒度でのコンポーネント設計をするにはまだハードルがある)
    - やりたいこととしては、バックエンドFWを使っていた頃のメンタルモデルは保ちつつ、描画にまつわるロジックをコンポーネントに凝集し、UI部分だけモダンにしていきたいぜくらいの気持ち(jQuery辛い)

### 「開発者体験」より「顧客体験」
特に「作りたかったアプリケーションの特徴」の3つ目の項目に関して「何を使いたいか」が先行して顧客にとって使いづらいものになることは避けたかったので、慎重になりました。

慎重になった背景として自分の頭の中にあったのは、普段家で見ている時はサクサク動くアプリケーションが、電車内など少し通信状態が悪い環境になった途端、いつまで経っても使えるような状態にならず、そっ閉じしてしまう実経験でした。

また、少し前に話題となったAddy Osmani氏の「The Cost Of JavaScript」[^1]を読んでいたこともあって、クライアント側に多くの責務を担わせることには慎重になるべきで、開発者の高スペックな環境で正しく動作することが、顧客の良質な利用体験を保証するわけではないということを知っていました。

https://medium.com/dev-channel/the-cost-of-javascript-84009f51e99e


このような背景から、純粋なSPAのアプリケーションではなく、SSRを具備したフレームワークの採用を検討しました。

## 選定にあたって候補になったもの
まず前提として、SSRを具備したフロントエンドメタフレームワークはいくつかありますが、元々チームでReactを使っていたこともあり、Reactを使用したものに限定しました。
そして、選定の中で以下が候補にあがりました。

- Next.js(Pages Router)
- Next.js(App Router)
- Remix(現: React Router v7)

以下それぞれのフレームワークに対して、選定判断で特に決め手になったポイントです。

|  フレームワーク| 印象 |
|-----|-----|
| Next.js(Pages Router) | 求めていたことは実現できそうだが、選定時期にApp Routerが盛り上がってきたこともあって、積極的に採用しづらい。また、いくつかの観点でRemixの方に優位性を感じた。例を挙げるとForm送信一つとった時にRemixの方がよりシンプルに実現できそうに感じた。|
| Next.js(App Router) | SSRを容易に実現し、最低限のパフォーマンスを保証するという観点において、まだまだ自チームにハードルを感じた。例えば、SSRを積極的に有効化するには、use clientの境界を小さく狭められるような上手なコンポーネント設計が必要と感じ、それをやり切れる自信があまり無かった。また、現状バックエンドで柔軟にキャッシュ制御する要件もなく、アプリケーション要件に対してオーバースペックに感じた。|
| Remix | Next.jsと比べた時のコミュニティの小ささは多少気になる[^2]ものの、App Routerほどシビアにコンポーネント境界を区切ることができなくても、ルートごとに用意されたloaderとactionにサーバーサイドで実行されるべきコードを記述していき、特に境界を意識せずコンポーネントを定義しても、自然とSSRが実現できることに魅力を感じた。 |

このような評価から、Remixを採用することに決めました。

## Remix SSRのお気に入りポイント
以下、個人的にRemix SSRの好きな点をご紹介していきます。

### 1. フルスタックデータフローの馴染みやすさ
Remixが掲げる「フルスタックデータフロー」[^3]という特徴があり、そのレールに乗って開発を進めることで、開発者は煩雑なクライアントサイドでの状態管理から解放され、それでいてSPAアプリケーションと同等のUI体験を顧客に提供することができます。

![スクリーンショット 2024-11-30 12.13.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/714305/10df9dab-7e94-c9c4-e027-8136a713d809.png)

以下、コード内に各関数の役割などを簡単に補足させていただきました。
```tsx:route.tsx
import type {
  ActionFunctionArgs,
  LoaderFunctionArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData, Form } from "@remix-run/react";

// loader: ページ遷移時のAPIエンドポイントのようなものでサーバーサイドで実行される
export async function loader({
  request,
}: LoaderFunctionArgs) {
  const user = await getUser(request);
  return json({
    displayName: user.displayName,
    email: user.email,
  });
}

export default function Component() {
  // 初回遷移時はloaderで返したデータをもとにこのコンポーネントがSSRされ、HTMLとして返却される
  // 以降遷移してきた時は、jsonデータのみが返り、CSRが行われる。
  
  const user = useLoaderData<typeof loader>();
  // useLoaderData: loaderで返したjsonデータを受け取るhooks

  return (
    // RemixのFormコンポーネントやLinkコンポーネントはSSRした時には通常の<form>や<a>タグとして
    // 動作し、後からhydrateされるようになっているので、HTMLが届いた時点でページとして機能させることができる(プログレッシブエンハンスメント)
    <Form method="post" action="/account">
      <h1>Settings for {user.displayName}</h1>

      <input
        name="displayName"
        defaultValue={user.displayName}
      />
      <input name="email" defaultValue={user.email} />

      <button type="submit">Save</button>
    </Form>
  );
}

// action: postメソッドが送られた際に実行されるAPIエンドポイントのようなものでサーバーサイドで実行される
// 実行後、デフォルトでは「データが更新された」と判断され、loaderが再度自動で走るようになっている。
// 補足として、loaderの再実行が不要な場合もその設定が可能である(https://remix.run/docs/ja/main/route/should-revalidate)
export async function action({
  request,
}: ActionFunctionArgs) {
  const formData = await request.formData();
  const user = await getUser(request);

  await updateUser(user.id, {
    email: formData.get("email"),
    displayName: formData.get("displayName"),
  });

  return json({ ok: true });
}

```

RESTでものを考えるということをそれなりにやってきたエンジニアにとって、こうしたベージ単位でのデータ取得・UI更新の流れは非常に理解がしやすく、馴染みやすいものでした。
実際に、採用した際他のチームメンバーもそれほどキャッチアップの時間をかけずに、開発に参加できていたように感じます。


### 2. 容易にSSRや適切なバンドルができるようになっている
先述したようにRemixのレールに従ってコードを書いていると、勝手に初回のSSRができるような状態になっています。

さらにコンポーネント境界をあまり意識しないでいいという点に関して、Remix(Vite版)はビルド時にbuild/server, build/clientというフォルダにそれぞれ以下のようなルールでビルドしたものを入れ込みます。

- build/server
    - exportしたloaderとaction及びその中で使用したパッケージ類(.server拡張子や/serverフォルダに置くことで強制も可能)
    - コンポーネントをjsx()でReactエレメント化→HTML文字列化する準備をしたjsファイル
- build/client
    - ブラウザ固有の部分(reactのhooks等)を含んだjsファイル
    - ルートごとに取得リソースを最適化するために必要なassetが記されたmanifestファイル
 
セキュリティ的な観点でガードレールもあり、あとは***loaderやactionの返り値さえ気をつければ***、クライアントサイドに意図しない秘匿情報が出ていってしまうことも基本的にはないので、事故も起こりにくいかと思います。


## 最後に
いかがでしたでしょうか。
技術選定の際に考えることは今回挙げた観点以外にたくさんあると思いますが、こういった観点もあるという一つのきっかけにしていただけたら幸いです。

またRemixは最近、React Routerとして生まれ変わったことで、既存のSPA利用者も吸収し、これからより大きくなっていくのだろうなという感じがします。SPAモードも注目され始め、盛り上がってきているところです。
個人的にも推し技術であるため、これからも積極的にその良さを伝えていけたらなと思います。

お読みいただきありがとうございました。

## おまけ
いつも実装の際に参考にさせてもらっている資料やリポジトリを貼っておきます。

https://remix.run/

https://remix-docs-ja.techtalk.jp/

https://reactrouter.com/

https://react-router-docs-ja.techtalk.jp/

https://github.com/epicweb-dev/epic-stack

https://github.com/remix-run/examples

https://github.com/coji/remix-spa-example

[^1]:最初に指摘されたのは7年前ですが、2023年に同氏が講演を行ったものがあったりする
(https://www.youtube.com/watch?v=ZKH3DLT4BKw)


[^2]: とはいえ、全く知見がないわけではもちろんなく、むしろRemix開発チームのkentcdodds氏中心にメンテナンスされている[epic-stack](https://github.com/epicweb-dev/epic-stack)であったり、コミュニティ有志によって作成されている[examples](https://github.com/remix-run/examples)を参照することで、多くのことを学ぶことができる。また、日本でも[Remix ドキュメントの日本語訳](https://remix-docs-ja.techtalk.jp/)をしている方がいたり、実装に困らないくらいの知見は既に十分あるように感じる。

[^3]: https://remix.run/docs/zh/main/discussion/data-flow