$30 off During Our Annual Pro Sale. View Details »

Next.js App Router での MPA フロントエンド刷新

Next.js App Router での MPA フロントエンド刷新

JSConf JP 2023

Hajime Mugishima

November 19, 2023
Tweet

More Decks by Hajime Mugishima

Other Decks in Programming

Transcript

  1. Agenda • サイボウズ Of fi ce と刷新プロジェクトの概要 • App Router

    を実際に導⼊して得られた課題・知⾒ • Caching • Server Components & Client Components • Storybook • Testing • Server Actions • MPA 刷新の⽂脈から⾒る App Router と所感
  2. 話さないこと • Next.js App Router の基本的な概要・概念 • 個別機能で複雑な部分のみ説明します • Next.js

    および App Router を選定した詳細な経緯や⽐較検討候補 • 段階的な移⾏のためのインフラ周りの仕組み • 1ページずつ段階刷新可能な環境は構築しています 時間がたりない… ※もし興味があれば、懇親会やまた別の機会などで…
  3. 現⾏のアーキテクチャ • C++ で書かれた CGI アプリケーション • 完全独⾃のスクリプト⾔語を全ての View テンプレートで利⽤

    • フロントエンドはかなりレガシーな状態 • ES5 & jQuery • モジュールバンドラ・型 などは無し
  4. 刷新後アーキテクチャ • Next.js App Router • CSS Modules • React

    Spectrum (React Aria / React Stately) • TypeScript • Jest / Playwright
  5. App Router におけるキャッシュとは • キャッシュと呼ばれるものは4種類存在する • Request Memoization • Data

    Cache • Full Route Cache • Router Cache 🔗 https://nextjs.org/docs/app/building-your-application/caching
  6. Full Route Cache • レンダリングした HTML および RSC Payload をキャッシュ

    • キャッシュに HIT するとレンダリングをスキップする • 複数リクエスト横断で動作する
  7. Router Cache • すでに表⽰したページや、 
 <Link> などで prefetch した 


    内容をキャッシュする • ひとつ前のセッションで 
 みなさん完全に理解したはず • クライアント側で動作する 完 全 理 解
  8. 刷新⽂脈におけるキャッシュの利⽤ • "グループウェアの刷新" という⼤前提から、 
 基本的には最新データの表⽰が期待される • Data Cache および

    Full Route Cache の利⽤には 
 fetch() 対象リソースおよび Segment 単位で 
 どの程度キャッシュを保持して良いか検討・設計が必要となる • 刷新に加えて +α の改善・最適化の作業が発⽣する • 刷新進捗を優先して、Data Cache と Full Route Cache は 
 利⽤しない判断をした
  9. 確実にキャッシュを無効化するには? • サイボウズOf fi ceはグループウェアという性質上、 
 ほぼすべてのページの表⽰で認証/認可が前提 • キャッシュ周りについては特に懸念点となる 


    → 他企業/ユーザのデータは絶対に⾒えてはいけない • Data Cache および Full Route Cache は 
 ユーザー横断でキャッシュが共⽤されるため 
 意図通り無効化できないとリスクになる? 

  10. Dynamic Functions とキャッシュ • cookies() や headers() は Dynamic Functions

    と呼ばれる • レンダリング中に Dynamic Functions が含まれると、 
 Full Route Cache は利⽤されず、 
 基本的にすべて動的レンダリングとなる • Dynamic Functions 実⾏後の fetch() についても 
 基本的にキャッシュ (Data Cache) は利⽤されない
  11. Dynamic Functions とキャッシュ • cookies() や headers() は Dynamics Function

    と呼ばれる • レンダリング中に Dynamic Functions が含まれると、 
 Full Route Cache は利⽤されず、 
 基本的にすべて動的レンダリングとなる • Dynamic Functions 実⾏後の fetch() についても 
 基本的にキャッシュ (Data Cache) は利⽤されない
  12. Dynamic Functions & Full Route Cache • Route Segment Con

    fi g の dynamic 指定で 
 強制的に Static Rendering への切り替えが可能 
 
 • Dynamic Functions の返り値は空になる
  13. Dynamic Functions & Data Cache • Dynamic Functions 実⾏後の fetch()

    でも 
 オプションが優先され Data Cache が利⽤される • revalidate > 0 • cache: "force-cache" 
 (Route Segment Con fi g の fetchCache = "force-cache" なども同様)
  14. 挙動のまとめ • Data Cache および Full Route Cache は 


    リクエスト横断でキャッシュが共⽤される • Dynamic Functions によって 
 キャッシュの利⽤はデフォルトで回避される • 個々の fetch() オプションによってさらに上書きして有効化できる
  15. 最終的なキャッシュ無効化の⽅針 • 明⽰的にキャッシュが無効な旨を何らかの形で定義しておきたい • 個々の fetch() での上書きも抑制したい • Route Segment

    Con fi g で dynamic = "force-dynamic" を指定 
 →動的レンダリング強制 & fetch() のキャッシュも無効化(上書き不可) • Undocumented な挙動には頼らず、明⽰的にキャッシュを無効化する • 念のため、Incremental Cache Handler で 
 noop なキャッシュハンドラも指定している
  16. 余談 / Data Cache とキー • Data Cache でのキー⽣成には、 


    URL や Method だけでなく、 
 ヘッダーの情報なども⽤いられる • 仮に認証/認可に Cookie を使うと 
 キャッシュは別のものになる 
 →暗黙的に漏洩リスクは回避される • 現状ではドキュメントに記載のない挙動 🔗https://github.com/vercel/next.js/blob/ 31c2f976cdd4deb0e7c412538a70b795dff4689e/packages/next/ src/server/lib/incremental-cache/index.ts#L374-L390
  17. "use client" はどこに付与すべきか • "use client" は Client Components の境界を宣⾔するもの

    🔗 https://nextjs.org/docs/app/building-your-application/rendering#the-use-client-directive > You can use the React "use client" convention to de fi ne the boundary. • シリアライズできない Props を取ると TS Warning となる • 境界を意識し、"use client" のネストは避けたほうがよいか?
  18. 困るケース: Server / Client で共通のコンポーネント • Server Components / Client

    Components の両⽅から使える 
 共通コンポーネントを定義したいケースがある • a11y のため React Aria を使いたい • React Aria は 内部的に useState / useEffect に依存 
 → 利⽤する際は Client Components である必要がある • <button> などの Wrapper ではイベントハンドラを取りたい 
 → "use client" を付与したいのに付与できない…? 発⽣した例 : React Aria の Wrapper コンポーネント
  19. Optional Props でハンドラを受け取る • 似たユースケースで参考になるものとして 
 next/link の <Link> コンポーネントが挙げられる

    • "use client" を付与した上で、 
 イベントハンドラを Optional Props として受け取っている 
 (この場合 TS Warning は発⽣しない)
  20. 最終的な "use client" の付与ポリシー • 基本的には Server Components を前提とする •

    useState / useEffect などを⽤いた 
 Client 側でのユーザーインタラクションが必要になったら 
 境界を定めて "use client" を付与する • Server / Client で汎⽤的なコンポーネントが必要なケースなどでは、 
 例外的に "use client" の付与を許容し、 
 イベントハンドラなどは Optional Props として受け取る
  21. Presentational Component の分離 • ⾒た⽬の部分のみを担保するコンポーネントを分離する • いわゆる Container / Presentational

    Pattern に近い形 • co-location し、近くに <_Components> の形で配備する形とした ↓Storybookでの表⽰対象
  22. Storybook と Server Components の今後 • Server Components サポートに関する Issue

    は存在する [Feature Request]: Support React Server Components (RSC) #21540 🔗 https://github.com/storybookjs/storybook/issues/21540 • プロトタイプは完成しており、数ヶ⽉以内に 
 Experimental な形でのリリースがあるとのこと • Presentational Component の分離は不要になる未来が来るかも? 期 待
  23. Server Components とテスト • testing-library は未対応 
 🔗 https://github.com/testing-library/react-testing-library/issues/1209 •

    関数として直接実⾏すればテストできなくもない 
 
 
 
 
 →破綻しやすい 
 ・Async Component がネストすると対応できない 
 ・後から⼦孫コンポーネントで fetch() が必要になることも多い
  24. Experimental test mode for Playwright • Experimental だが、 
 Playwright

    サポートが存在 • fetch() のモック化や 
 MSW との統合を提供する • 部分的なレンダリングは不可 
 ↓ 
 ページ内で必要な API は 
 すべてモック化が必要 QBDLBHFTOFYUTSDFYQFSJNFOUBMUFTUNPEFQMBZXSJHIU3&"%.&NEΑΓൈਮ
  25. • Docker Compose でバックエンドごと⽴ち上げた環境に対し、 
 Playwright でのテストを中⼼に整備する⽅針とした 
 (これ相当のものを E2E

    と呼んでいる⼈もいるかも) • データのセットアップは、必要に応じて内部 API を事前実⾏ • チームのテストに関するポリシーにもマッチしていた • 刷新前の段階で⾃動テストが無い画⾯も多く、効率よく拡充したい • Next.js のアップデートや⻑期の刷新作業での内部更新も予想され、 
 範囲を広めにした振る舞いの担保を厚めにしておきたい Integration Test (≒E2E) を主軸とした
  26. テストの課題と展望 • 実質 E2E が中⼼となるため、 将来的な実⾏コストの増⼤が懸念点 
 (ひとまず Sharding で分散している状態)

    • Client Components または Shared Components は 
 Jest での Unit Test でカバーし、Integration Test の軽減は試みている • QA と協⼒し、効率の良いテストケース設計となるよう調整している • testing-library の Server Components サポートが来たら 
 そちらの⽐重を増やしていけるかも
  27. Server Actions とは • 簡単に⾔うと「バックエンドで実⾏されるコードを、クライアントから直接呼び出している ように書ける」機能 (≒ React 提供の RPC)

    • Progressive Enhancement をサポートしている • Next.js 14 以降は Stable (Server Actions ⾃体は React 側の機能) • インラインで SQL を直接埋め込む、などは現実的なユースケースではほぼ無いと思われる
  28. Server Actions の採⽤ • 使わないケースと実装⽅法が⼤幅に異なる • 仮に Server Actions が主流になるとすべて書き換える必要が?

    • fetch() している Server Components をリフレッシュする⽅法が 
 現実的には Server Actions & revalidate の⼀択 • 採⽤しない場合、router.refresh() でページ全体を再描画するか、 
 データ取得箇所も Client 側で制御する形とする必要が⽣じる • Issue や PR の状態から、早くに Stable になると予測 • 前述の通り、振る舞いを担保するテストを厚めに⽤意しており、 
 「根本的に動作しなくなった」といったケースは即時検知できる状態 • 覚悟
  29. Server Actions と再レンダリング • Server Actions 内で revalidatePath または revalidateTag

    
 を実⾏すると、現在ページの再レンダリングが⾏われ、 
 レスポンスに RSC Payload が含まれる • これを利⽤することで、更新後の再描画が簡単に⾏える • Path および Tag の⼀致は関係なく、実⾏有無で判断される 
 ※v14.0.2-canary.13 時点
  30. ページ全体が再レンダリングされる • 再レンダリングはページ全体が対象になる点 • サイボウズ Of fi ce のように Data

    Cache がほぼ無効化されていると、 
 レンダリングのためすべての fetch() も実⾏されることになる • 操作頻度が⾼い場合、負荷増⼤に繋がる可能性に注意が必要 サイボウズ Of fi ce での例: いいね!ボタン
  31. Custom Invocation • Server Actions は <form> を使わず 
 Client

    Component でのユーザーインタラクションを契機に 
 実⾏することもできる = Custom Invocation
  32. Custom Invocation の使いどころ • JavaScript から Server Actions をトリガーしたい、 


    あるいは Server Actions 前後に必ず何らかの処理を⾏いたいケース • Progressive Enhancement は機能しない • サイボウズ Of fi ce では、エラーハンドリングの兼ね合いで 
 Custom Invocation での導⼊から始めた React 'use server' のドキュメントで記載のある利⽤例としては ローディング表⽰ / OptimisticUpdate / 予期しないエラーのハンドリング など 🔗 https://react.dev/reference/react/use-server#calling-a-server-action-outside-of-form > When using a Server Action outside of a form, call the Server Action in a transition, 
 > which allows you to display a loading indicator, 
 > show optimistic state updates, and handle unexpected errors. ちなみに...
  33. Custom Invocation とエラーハンドリング • Server Actions で throw された Error

    は、 
 Client 側の Custom Invocation 実⾏箇所の try-catchで拾える • しかし、セキュリティ上の観点から、 
 クライアントに転送されるエラーはすべてマスクされる 
 • これは production 時のみの挙動で、 
 知らずに next dev で動作確認を⾏っていると 
 本番で動かなくなるので注意が必要 🔗 https://nextjs.org/docs/app/building-your-application/routing/error-handling#securing-sensitive-error-information
  34. Result 型の利⽤ • Server Actions からの Error の throw は⼀切使えない前提で考える

    • Client 側でエラーの詳細を把握する必要がある場合、 
 レスポンス内にステータスコードを含める
  35. エラー分岐の複雑化 • Result 型でレスポンスを得られるが、 
 ネットワークエラーなども拾って 
 ユーザーにフィードバックしたい • try-catch

    が避けられず 
 エラー分岐が複雑化する • 汎⽤的なエラー処理もあるため 
 可能な範囲で共通化したい
  36. Client 側でエラーの re-throw • Server Actions を実⾏する Wrapper Function を⽤意し、

    
 レスポンスに含まれるステータスコードに応じてエラーを再度 throw • catch の中ですべてのエラーをハンドリング可能にしている エラーの内容に応じて独⾃のエラーオブジェクトを作成
  37. 今後の展望 / Progressive Enhancement の活⽤ • <form> の action や

    <button> の formAction などを使うことで、 
 ⾃動的に Progressive Enhancement が有効になる • Hydration 前に操作可能になるなど、メリットも多い • useFormStatus / useFormState を組み合わせることで、 
 Progressive Enhancement を維持したまま 
 単純なエラー表⽰や状態管理を実現できる • 徐々に Custom Invocation ではない範囲も広げていきたい
  38. MPA 刷新と App Router の相性は良好 • Server Components での描画を第⼀に検討するポリシーとした •

    Server 側でのレンダリングが基本 • 動きが必要な箇所で Client Component を⽤いる • 従来の MPA での描画とメンタルモデルが近い • MPA では URL 単位で機能の境界が存在することが多い • 段階的に app/ 配下に移植していける • ページ単位で移⾏していける • ⼩さく始めての試⾏錯誤もしやすい
  39. 所感 • Undocumented な挙動が多い • Next.js および React のコードは頻繁に確認する •

    気になる Issue/PR もチーム内で共有している • ⾃分でコントリビュートする気概も必要 • 後はエコシステムが整えばさらに開発しやすくなる印象 • 個⼈的には testing-library を使いたい