「これからデカいプロジェクトでSPAガッツリ使うぞ」だった時の自分に伝えたい、技術選択としてのSPA観

# 表題について

ここ2年弱ほど、フルスクラッチのWebシステム開発プロジェクトにてVue.jsによるフロントエンド開発のリード枠として稼働を続けておりました。
それまでのフロントエンド経験は業務用パッケージ製品のWebUIカスタマイズ（その製品が用意しているAPIにjQueryを一つまみ程度）をしばらく -> リリース済みのVue.jsアプリ保守を1年弱、という経歴だったので、今回が一番ガッツリVue.jsに向き合って開発をする機会でした。
しかし、長い開発期間の中で実装の複雑さがどんどん膨れ上がっていき、プロジェクトも終わりに近づくにつれて複雑化かつ肥大化したソースコードと大量に発生したバグに悩まされる日々が続くことになりました。

そこに至った経緯は色々とあるのですが、特に技術面や実装面でどうすればこの複雑さを回避できたのか、について答えがなかなか自分の中で出せないモヤモヤを抱え、「SPAなんもわからん…」「フロントエンドなんもわからん…」という若干逃げ腰なモヤモヤをしばらく抱えてました。

そんな折、X（Twitter）で流れてきた下記の記事が目に留まりました。

https://devlog.mescius.jp/htmx-quickstart/

https://qiita.com/tsmd/items/0d07feb8e02cfa213cc4

SPAと全く違うアプローチでいわゆる「モダン」なUIを組み立てられる[htmx](https://htmx.org/)の存在を知り、「WebシステムでSPAを選択する理由」や「SPAとそうでないフロントエンドとで、システム全体のアーキテクチャにどのような差が生まれるのか」について何も説明できない自分がいることに気づきました。
そしていざフロントエンドについて改めて調べなおしてみた際に、SPAを選択したことに起因する「複雑さのタネ」のようなものについて、少しずつ言語化ができるようになってきました。

SPAの基礎的な話は以下のように既にわかりやすくまとめられている記事が何件も存在しているので、
この記事では上記の「複雑さのタネ」をもとに、「このプロジェクトに入ったころの自分に「SPA使うんならこの辺意識しておきな」って言いたいこと」を自分なりにまとめようと思います。

https://qiita.com/shinkai_/items/79e539b614ac52e48ca4

# SPAを選定する際に意識しておきたい観点

:::note
※SPAフレームワークを利用していないWebシステムについての言及も多くなりますが、「SPAでないWebフロントエンド」の共通の呼び方があまりないので、記事中では便宜上「MPA（マルチ・ページ・アプリケーション）」と記載します。
:::

## ページ遷移の仕組みの上書き

SPAでは、基本的にMPAのような完全なページ遷移は存在していません。
その代わりとして、SPAのフレームワークが持っているルーター（[Vue Router](https://v3.router.vuejs.org/ja/) や [React Router](https://reactrouter.com/en/main)など）が、Webシステムにおけるページ遷移を代替する様々な処理を担います。

- アプリ内でのページ遷移用のリンク生成
- リンク選択時のイベント発行
- URLの変更、読み取り
- 描画するリソースの決定とページ移動時の切り替え
- ページ遷移前後のイベント管理
- ページ遷移履歴の操作（ブラウザが用意している [History API](https://developer.mozilla.org/ja/docs/Web/API/History_API) 経由）

語弊のある言い方をすると、MPAを利用しているときに我々が当然のように利用しているリンク操作や戻る/進むボタンでの移動などは、SPAにおいてはロード時に読み込んだ1枚のキャンパスの上で疑似的に再現されているに過ぎません。
その都合で、ルーターを操作すれば（例えMPAらしくない、Webシステムらしくないページ遷移であろうと）割と自由にページ遷移や履歴の操作ができてしまいます。
（例えばある画面で「戻る」をした時に、1つ前に開いていた画面ではなくある特定の画面を開くような）

このようにWebらしからぬ動きを作れてしまう、あるいは実装時に意図せずらしからぬ動きがついてしまう、という可能性は心に留めておきましょう。[^1]

[^1]: 画面遷移はルーター側の実装に依存する、という見方もできます。筆者は [Vue RouterのこのIssue](https://github.com/vuejs/vue-router/issues/974) がずっと閉じていないのを最近見つけて頭を抱えました。

## APIとの密結合

SPAとバックエンドAPIとの間でデータをやり取りする際、javascriptとの親和性の高さからしばしばJSONが利用されます。
SPA側はAPIからJSONで受け取ったデータを解釈・加工・変換してユーザ向けの表示や入力フォームの作成をしたり、逆にユーザによるデータ入力などの操作を受けて、必要なデータをJSON化してAPIに渡す、といったことを行います。

フロントエンドとバックエンドがそれぞれ別でリソースを保持してAPIを介してやり取りをする構造上、両者はAPIが持つJSONスキーマなどによる依存関係にあります。
JSONスキーマを始めとしたデータ受け渡しのルールはAPI側（＝バックエンド側）に定義される以上、どちらかと言えばフロントエンドがバックエンドに依存している、と言えるでしょう。
フロントエンド側はバックエンド側で用意しているAPIの種類、仕様、リクエストやレスポンスのボディ定義等を知る必要があり、バックエンド側に変更があればフロントエンド側にはそれに追従する責務が発生します。

-----

ところで、フロントエンドとバックエンドをかなり切り分けて話を進めていますが、これ自体にあまり違和感のあるWeb技術者はそこまでいないのではないかと思います。
技術記事や求人情報を見てもこの両者の単語はよく見ますし、「フロントエンドエンジニア」「バックエンド経験あり」と言ったように技術スタックもこの単位で分けて語られることも珍しくありません。
実際のところ最初で話に出したプロジェクトでも、フロントエンドとバックエンドはチームとして完全に分かれていて、分業体制で開発が進んでいました。

しかし、チームとしてフロントエンドとバックエンドが別体制で動く場合、技術スタックや担当領域として両者が分かれることは自然なことですが、Webシステム全体として達成するべき仕様については双方で理解する必要性が出てきます。
開発の過程で両者は密接に連携を取り、仕様の達成に向けてそれぞれが何を開発すればいいか、その切り分けの結果どのようなAPI構成が良いかについて一緒になって考えることが望ましいです。

つまり、「俺らでAPI作ったから、あとは画面でいい感じにやってね」の関係性ではいけません。

## 状態管理、リアクティビティ

上述の通り、APIとSPAとのデータのやり取りはJSON形式を介して行われることが多いです。

SPA側はJSONで受け取ったデータをオブジェクトとして保持します。
保持したオブジェクトを監視することで、そのデータをもとに画面表示を行ったり、ユーザ操作を受けてそのデータを変更したり、といったことを行います。いわゆる「リアクティブ」な動きです。

リアクティブなデータを保持する、とは、言い換えるとそのデータを常に「状態」として保持することです。
Vueであれば`reactive`や`ref`、あるいは [pinia](https://pinia.vuejs.org/) などがその役割を担うでしょう。

データを「状態」として保持する場合、そのデータに対して「いつ取得、更新、破棄されるか」「何のイベントで更新するべきか」「データの変更が何に波及するか」といった状態管理の設計を、コンポーネント単位、画面単位、あるいはアプリ単位で考える必要があります。
実装が多くなっていくと、短期的には楽そうじゃん、と言って管理する「状態」を増やしがち（特に簡単なフラグとか…）ですが、状態を増やすことは状態管理を増やすことであり、状態管理は複雑かつ後から見返した時の追跡が難しくなりがちです。

いかにフロントエンドが抱える状態をシンプルにするか、は常に意識するべきです。（筆者はいまだに試行錯誤中です）

## バックエンドとフロントエンドの責任分離

ここまでで記載した通り、SPAを使うことは「javascript/typescriptでJSONのデータを受け取って画面描画のために加工をする」ことがメジャーであり、常に何かしらの処理をjavascriptで実装することが前提です。
SPAは開いている画面（あるいはそれと関係ない何か）のデータを常に保持することになるため、そのデータに対する様々な検証が容易に行えます。
HTMLだけでも行えるようなフォーマットチェック等々にとどまらず、複数データ間の整合性検証、逐次的なAPI呼び出し等々もそこまで難しくないでしょう。むしろそれを支援する手法はSPAのフレームワークやその周辺のライブラリを見ればあふれるほど存在します。
そのため、API側に移譲していい処理さえも、SPA側では容易に実装するための土壌があり得ます。

しかし、状態管理の項に記載した通り、フロントエンド側に状態を持たせることはアプリの複雑性を上げることにつながります。
状態に依存した処理を実装したり、あるいは実装を進めるために新規で状態を用意したりすると、その分だけフロントエンドは複雑になっていきます。

バックエンドのAPIであれば、リクエストを受けてレスポンスを返すまでの間で変数等のスコープは原則閉じています。
しかし、フロントエンドでは処理外の状態を参照・操作することが容易なため、同等の処理を行う場合でもデータの状態に対する考慮が発生する分バックエンドより複雑になりがちです。

エンジニアはその事実を踏まえて、フロントエンドとバックエンドを通した責任分離の指針決定と設計を行うべきでしょう。
SPAはjavascript中心で様々な処理を行うため、過大に言ってしまえば「何でも」できますが、システム全体をシンプルにするためには「やるべきことだけやる」「やるべきでないことはやらない」という切り分けの選択が必要です。

# あとがき

ここまでに色々と記載してから気付いたことですが、最初に記載したプロジェクトでの筆者の反省点の1つは「フロントエンドとバックエンドがうまく分業と協業をできていなかったこと」です。
これ自体はSPAをアーキテクチャとして選択したことが原因ではないのですが、SPAであるが故にフロントエンドに生まれた高い自由度と容易に上がっていく複雑さについて、筆者が無頓着だったのかもしれない、とも考えています。
それを踏まえた上で、今フロントエンドを開発するなら、という理由でSPAを採用するのではなく、SPAの性質についてより見識を深めてSPAをあくまで「選択肢」の1つとして捉えられるようになることを目標として今回の記事を作成しました。

記事の最初で名前を出しましたが、この記事を書くにあたって、参考文献に記載したhtmxのドキュメントからかなりインスピレーションを受けてます。
htmxのエッセイを見ると正直かなり尖ったことも書かれていますが、筆者が漠然と感じていた様々な違和感や疑問点について、言語化の助けとなってくれました。
この記事自体は自分の考えをまとめるために書いた毛色が強いですが、もしこの記事を読んで感じることがあれば、エッセイに目を通してみることをお勧めします。

# 参考文献

- [Hypermedia Systems](https://hypermedia.systems/)
- [\</> htmx - high power tools for html](https://htmx.org/)