App Router 移行に伴う GraphQL 利用アプリのデータフェッチ再考
今回の記事では、Server Component(以下、SC)とClient Component(以下、CC)の略称を使用します。
App Routerとデータフェッチにおける課題
App Router が stable になったことで、App Router への移行事例や Server Component の利用事例も増えてきました。
私が開発を担当する BtoBtoC Web アプリケーションにおいても、Page Router から App Router への移行を開始したのですが、その中でデータフェッチ戦略について再考する必要が出てきました。
例えば、下記のような悩みがあります。
- SC を新しく取り入れる場合、CC で実行していたデータフェッチを SC に移管した方が良い箇所はどれだけあるか?
- データフェッチを CC から SC に移管する場合のメリットとデメリットは何か?
- 現状、GraphQL Query でページに必要なリソースは一括取得できているが、SC に部分移管するメリットが勝るケースはあるか?
上記の悩みはまだ全て解決されたわけではないのですが、移行のとっかかりとして、とりあえず SC に移行できそうなデータフェッチと引き続き様子見とするデータフェッチの切り分けを行いました。本記事では、その最初の切り分けを紹介させていただきます。
App Router のデータフェッチを考える
今回は下記の2軸でデータを切り分けて検討した内容を記載します。
public/private: 認証が必要か否か
static/dynamic: データが静的か動的か
もちろん、上記の軸のみで切り分けられないケースは多々存在します。ただ、データフェッチを見直すとっかかりとしてはシンプルで考えやすいため、まずはこの2軸で考えてみました。
前提条件
- public/private, static/dynamic 以外の軸はスコープ外
- 初期表示必須/遅延読み込み可
- 常に最新データ表示/最新データ表示不要
- state/not state
- etc
- 弊社プロダクトでは Firebase Auth でクライアントに認証情報を保持しているため、その前提で考える
- apollo-client は SC でも使用可能(experimental だけど)
- 本記事における SC は全てデータフェッチを含む SC という前提(データフェッチを持たない SC はスコープ外)
- 本記事で検討したのはアプリ全体でなく、部分的に App Router 移行を実施する3ページが対象
- フェッチしたデータのユースケースはアプリのドメインに依存するため、本記事のデータフェッチ戦略はあくまで弊社プロダクトの場合の話
- 実運用はこれからなので、2ヶ月後にはまた意見が変わってるかもしれない。その時はまた実運用で発生したギャップを記事にしたい(希望)
4象限でのデータフェッチ戦略再検討
Page Router から App Router に移行するにあたって、データフェッチ戦略の before/after とその理由を記載していきます。
public & static データ
データ例
- 全体公開する静的コンテンツ(例:ヘルプページ、公式アナウンスメントなど)
データフェッチ戦略
- before:Incremental Static Regeneration (ISR)
- after:SC(デフォルト設定で基本キャッシュ利用)
備考
- 頻繁に更新されないデータであるため、Next.js Cache の利用が効率的
- この象限は before/after の差分がないに等しい
public & dynamic データ
データ例
- コンテンツに対する「いいね」の数など
データフェッチ戦略
- before:CC
- after:SC (revalidate で適宜キャッシュ更新)
備考
- データのユースケースによるため一概には言えないが、最新データ表示が必須でないコンポーネントのみだったため、revalidate により Cache 更新する SC にした
- Page Router の時は、データの更新頻度別に SSR することができなかったため CC でデータフェッチしていたが、それを SC にできるようになったのは嬉しい
private & static データ
データ例
- 会員のみに公開する静的コンテンツ
データフェッチ戦略
- before:CC
- after:CC と SC 混在
備考
- CC でよしなに state として持ち回りたいフィールドのみ CC でフェッチして、そうでないフィールドを扱うコンポーネントを SC として切り出せるケースは、SC にしてみた(下部にイメージ画像あり)。これは Graph Schema の柔軟性のおかげだが、GraphQL Query で一括取得していたデータを、SC と CC で分けることになるため、通信回数が増える本末転倒なことをしている気がしてしまう部分もある
- 上記方法で SC にできなくはないが、無理に SC にするメリットがあまり感じられないコンポーネントは引き続き CC となった
- キャッシュについては、最新データ必須か否かによって設定を変える必要がある
private & dynamic データ
データ例
- 会員情報、決済履歴、残高など
データフェッチ戦略
- before:CC
- after:CC
備考
- SC にできなくはないが、無理に SC にするメリットをあまり感じられないコンポーネントが多く、引き続き CC となった。これは弊社プロダクトのドメインが強く影響している
- ただ現状の after がベストとも思えないため、試験的に、例えば、残高表示コンポーネントのみを SC (no-store) にして、クライアント Mutation が走ったら router.refresh という実装が有効な箇所を模索中
なぜ private データは CC メイン?
弊社プロダクトにおいて、private データは主に「ユーザーデータ」と「特定ユーザー群に公開する共通コンテンツ」の2種類があります。
ユーザーデータ
ユーザーデータは CC でのイベント処理に使いたいフィールドが多くあります。しかし、SC でフェッチしたデータを CC (自分の場合は apollo-client)で良い感じに扱うことは現状できません。これが主な理由で、ユーザーデータをフェッチするコンポーネントは CC のままとなっています。別途記載している GraphQL Query の分割という手段もあるのですが、ユーザーデータについては CC での取り回しが多く、部分的に SC に寄せるメリットが薄かったため、この手段は現状採用していません。
特定ユーザー群に公開する共通コンテンツ
private な共通コンテンツは静的データが多く、SC に寄せることで CC の実装をシンプルにできたり、パフォーマンス改善できたりといった期待があります。これが private & static データを扱う CC を半分 SC にした理由です。
“フェッチしたデータのこのフィールドは CC の apollo-client で管理したい” というケースにおいては、GraphQL Query を CC 用と SC 用に分けて、CC の一部を SC に切り出すということを一部試してみました。しかし、これはアプリのコンテキストによるコンポーネント分割でなく、SC 都合での分割になるため、これがプラスに働くかは実運用しつつ様子見です。private & static の備考の繰り返しになりますが、GraphQL Query を分けることで、通信回数が増えているというのも、やはり気になります(トレードオフをちゃんと計算できていない)。
GraphQL Query で取得している一部を SC に置き換えるケース
下記画像にある3パターンのどれがよいかは、今後の実運用で検証していきたいと思います。
まとめと今後の検討
public/private, static/dynamic の2軸で、データフェッチ戦略を再考しました。
SC に関するデータフェッチ戦略は、多くの要素に影響されます。弊社プロダクトの場合、apollo-client が SC を考慮した設計に与える影響はとても大きかったです。SC ファーストでアプリを作る場合の技術選定は、これまでとまた違ったものになるのでしょう。
本検討で最も悩ましかったのは、“フェッチしたデータのこのフィールドは CC の apollo-client で良い感じに取り回したいが、残りはフェッチしたコンポーネントで描画に使うのみ” といったケースです。SC を使いつつフェッチしたデータを apollo-client でもよしなに扱えたら解決するのですが、現状はそういったことができません。このケースにおけるデータフェッチは、引き続き検討課題となりました。
また、今回の public/private, static/dynamic という軸はナイーブなものであるため、もう少し細かく切り分けて考えていきたいと思います。
蛇足ですが、App Router は、フロントエンドのデータフェッチ戦略を大きく変えるものだと改めて感じました。
これまで、弊社アプリがクライアントにロードされた後に、クライアントフェッチのリクエスト先は BFF か Cloud Storage のみでした(外部 API は割愛)。しかし、SC をリクエストする先は BFF でなく Next.js サーバ(Web Server)になります。適切な表現ではないと思いますが、これはフロントエンドが新しいキャッシュストレージを手に入れたような気分で、より良いユーザー体験を実現する上で欠かせないものになりそうですね。
皆さんがデータフェッチを検討する上での参考になれば幸いです。
Discussion