Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

JSConf JP 2023

Avatar for mugi / Hajime Mugishima

mugi / Hajime Mugishima

November 19, 2023
Tweet

More Decks by mugi / 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 を使いたい