Slide 1

Slide 1 text

Next.js App Router での MPA フロントエンド刷新 JSConf JP 2023 Hajime Mugishima / Cybozu Inc.

Slide 2

Slide 2 text

@mugi_uno Hajime Mugishima Cybozu Inc. Frontend Expert Team

Slide 3

Slide 3 text

Agenda • サイボウズ Of fi ce と刷新プロジェクトの概要 • App Router を実際に導⼊して得られた課題・知⾒ • Caching • Server Components & Client Components • Storybook • Testing • Server Actions • MPA 刷新の⽂脈から⾒る App Router と所感

Slide 4

Slide 4 text

話さないこと • Next.js App Router の基本的な概要・概念 • 個別機能で複雑な部分のみ説明します • Next.js および App Router を選定した詳細な経緯や⽐較検討候補 • 段階的な移⾏のためのインフラ周りの仕組み • 1ページずつ段階刷新可能な環境は構築しています 時間がたりない… ※もし興味があれば、懇親会やまた別の機会などで…

Slide 5

Slide 5 text

サイボウズ Office と刷新プロジェクト

Slide 6

Slide 6 text

サイボウズ Office • サイボウズが提供するグループウェア • 1997年にパッケージ販売を開始 • 2011年以降はクラウド版を提供

Slide 7

Slide 7 text

現⾏のアーキテクチャ • C++ で書かれた CGI アプリケーション • 完全独⾃のスクリプト⾔語を全ての View テンプレートで利⽤ • フロントエンドはかなりレガシーな状態 • ES5 & jQuery • モジュールバンドラ・型 などは無し

Slide 8

Slide 8 text

刷新プロジェクトの始動 • 開発・保守のコストが徐々に問題に... • フロントエンドの刷新を決定 
 (通称 DOGOプロジェクト) 爆誕したチームのロゴ

Slide 9

Slide 9 text

刷新後アーキテクチャ • Next.js App Router • CSS Modules • React Spectrum (React Aria / React Stately) • TypeScript • Jest / Playwright

Slide 10

Slide 10 text

App Router を実際に導⼊して得られた課題・知⾒

Slide 11

Slide 11 text

Caching

Slide 12

Slide 12 text

App Router におけるキャッシュとは?

Slide 13

Slide 13 text

App Router におけるキャッシュとは • キャッシュと呼ばれるものは4種類存在する • Request Memoization • Data Cache • Full Route Cache • Router Cache 🔗 https://nextjs.org/docs/app/building-your-application/caching

Slide 14

Slide 14 text

Request Memoization • 単⼀のリクエストでのコンポーネントツリーのレンダリング中、 
 複数発⽣した同様の fetch() の重複を排除する機能 • 以前は Automatic fetch() Request Deduping と呼ばれていた • リクエスト単位で動作する

Slide 15

Slide 15 text

Data Cache • fetch() のレスポンスをキャッシュする • 再検証は時間単位(revalidate)や revalidateTag() / revalidatePath() で⾏う • 複数リクエスト横断で動作する

Slide 16

Slide 16 text

Full Route Cache • レンダリングした HTML および RSC Payload をキャッシュ • キャッシュに HIT するとレンダリングをスキップする • 複数リクエスト横断で動作する

Slide 17

Slide 17 text

Router Cache • すでに表⽰したページや、 
 などで prefetch した 
 内容をキャッシュする • ひとつ前のセッションで 
 みなさん完全に理解したはず • クライアント側で動作する 完 全 理 解

Slide 18

Slide 18 text

グループウェア刷新 ×キャッシュ

Slide 19

Slide 19 text

刷新⽂脈におけるキャッシュの利⽤ • "グループウェアの刷新" という⼤前提から、 
 基本的には最新データの表⽰が期待される • Data Cache および Full Route Cache の利⽤には 
 fetch() 対象リソースおよび Segment 単位で 
 どの程度キャッシュを保持して良いか検討・設計が必要となる • 刷新に加えて +α の改善・最適化の作業が発⽣する • 刷新進捗を優先して、Data Cache と Full Route Cache は 
 利⽤しない判断をした

Slide 20

Slide 20 text

確実にキャッシュを無効化するには? • サイボウズOf fi ceはグループウェアという性質上、 
 ほぼすべてのページの表⽰で認証/認可が前提 • キャッシュ周りについては特に懸念点となる 
 → 他企業/ユーザのデータは絶対に⾒えてはいけない • Data Cache および Full Route Cache は 
 ユーザー横断でキャッシュが共⽤されるため 
 意図通り無効化できないとリスクになる? 


Slide 21

Slide 21 text

Data Cache および Full Route Cache の無効化

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

認証/認可に Cookie を⽤いる ↓ キャッシュは利⽤されない?

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Dynamic Functions でのキャッシュ無効化 = デフォルト挙動 ↓ 設定によっては有効化される

Slide 26

Slide 26 text

Dynamic Functions & Full Route Cache • Route Segment Con fi g の dynamic 指定で 
 強制的に Static Rendering への切り替えが可能 
 
 • Dynamic Functions の返り値は空になる

Slide 27

Slide 27 text

Dynamic Functions & Data Cache • Dynamic Functions 実⾏後の fetch() でも 
 オプションが優先され Data Cache が利⽤される • revalidate > 0 • cache: "force-cache" 
 (Route Segment Con fi g の fetchCache = "force-cache" なども同様)

Slide 28

Slide 28 text

挙動のまとめ • Data Cache および Full Route Cache は 
 リクエスト横断でキャッシュが共⽤される • Dynamic Functions によって 
 キャッシュの利⽤はデフォルトで回避される • 個々の fetch() オプションによってさらに上書きして有効化できる

Slide 29

Slide 29 text

最終的なキャッシュ無効化の⽅針 • 明⽰的にキャッシュが無効な旨を何らかの形で定義しておきたい • 個々の fetch() での上書きも抑制したい • Route Segment Con fi g で dynamic = "force-dynamic" を指定 
 →動的レンダリング強制 & fetch() のキャッシュも無効化(上書き不可) • Undocumented な挙動には頼らず、明⽰的にキャッシュを無効化する • 念のため、Incremental Cache Handler で 
 noop なキャッシュハンドラも指定している

Slide 30

Slide 30 text

もしキャッシュが有効になってしまったら? ちなみに...

Slide 31

Slide 31 text

余談 / 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

Slide 32

Slide 32 text

Server Components 
 & 
 Client Components

Slide 33

Slide 33 text

"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" のネストは避けたほうがよいか?

Slide 34

Slide 34 text

困るケース: Server / Client で共通のコンポーネント • Server Components / Client Components の両⽅から使える 
 共通コンポーネントを定義したいケースがある • a11y のため React Aria を使いたい • React Aria は 内部的に useState / useEffect に依存 
 → 利⽤する際は Client Components である必要がある • などの Wrapper ではイベントハンドラを取りたい 
 → "use client" を付与したいのに付与できない…? 発⽣した例 : React Aria の Wrapper コンポーネント

Slide 35

Slide 35 text

Optional Props でハンドラを受け取る • 似たユースケースで参考になるものとして 
 next/link の コンポーネントが挙げられる • "use client" を付与した上で、 
 イベントハンドラを Optional Props として受け取っている 
 (この場合 TS Warning は発⽣しない)

Slide 36

Slide 36 text

最終的な "use client" の付与ポリシー • 基本的には Server Components を前提とする • useState / useEffect などを⽤いた 
 Client 側でのユーザーインタラクションが必要になったら 
 境界を定めて "use client" を付与する • Server / Client で汎⽤的なコンポーネントが必要なケースなどでは、 
 例外的に "use client" の付与を許容し、 
 イベントハンドラなどは Optional Props として受け取る

Slide 37

Slide 37 text

Storybook

Slide 38

Slide 38 text

Storybook と Server Components • Storybook を導⼊し、 
 コンポーネント単位での⾒た⽬・振る舞いの確認をしたい • 現状では Storybook は Server Components 未サポート • どうするか?

Slide 39

Slide 39 text

Presentational Component の分離 • ⾒た⽬の部分のみを担保するコンポーネントを分離する • いわゆる Container / Presentational Pattern に近い形 • co-location し、近くに <_Components> の形で配備する形とした ↓Storybookでの表⽰対象

Slide 40

Slide 40 text

Presentational Component の分離 • @Quramy さんが書かれた記事に近い発想 🔗 https://quramy.medium.com/react-server-component-のテストと-container-presentation- separation-7da455d66576 • Server Components のネストも考慮されており、⼤変参考になります

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Testing

Slide 43

Slide 43 text

Server Components とテスト • testing-library は未対応 
 🔗 https://github.com/testing-library/react-testing-library/issues/1209 • 関数として直接実⾏すればテストできなくもない 
 
 
 
 
 →破綻しやすい 
 ・Async Component がネストすると対応できない 
 ・後から⼦孫コンポーネントで fetch() が必要になることも多い

Slide 44

Slide 44 text

Experimental test mode for Playwright • Experimental だが、 
 Playwright サポートが存在 • fetch() のモック化や 
 MSW との統合を提供する • 部分的なレンダリングは不可 
 ↓ 
 ページ内で必要な API は 
 すべてモック化が必要 QBDLBHFTOFYUTSDFYQFSJNFOUBMUFTUNPEFQMBZXSJHIU3&"%.&NEΑΓൈਮ

Slide 45

Slide 45 text

• Docker Compose でバックエンドごと⽴ち上げた環境に対し、 
 Playwright でのテストを中⼼に整備する⽅針とした 
 (これ相当のものを E2E と呼んでいる⼈もいるかも) • データのセットアップは、必要に応じて内部 API を事前実⾏ • チームのテストに関するポリシーにもマッチしていた • 刷新前の段階で⾃動テストが無い画⾯も多く、効率よく拡充したい • Next.js のアップデートや⻑期の刷新作業での内部更新も予想され、 
 範囲を広めにした振る舞いの担保を厚めにしておきたい Integration Test (≒E2E) を主軸とした

Slide 46

Slide 46 text

テストの課題と展望 • 実質 E2E が中⼼となるため、 将来的な実⾏コストの増⼤が懸念点 
 (ひとまず Sharding で分散している状態) • Client Components または Shared Components は 
 Jest での Unit Test でカバーし、Integration Test の軽減は試みている • QA と協⼒し、効率の良いテストケース設計となるよう調整している • testing-library の Server Components サポートが来たら 
 そちらの⽐重を増やしていけるかも

Slide 47

Slide 47 text

Server Actions

Slide 48

Slide 48 text

Server Actions とは • 簡単に⾔うと「バックエンドで実⾏されるコードを、クライアントから直接呼び出している ように書ける」機能 (≒ React 提供の RPC) • Progressive Enhancement をサポートしている • Next.js 14 以降は Stable (Server Actions ⾃体は React 側の機能) • インラインで SQL を直接埋め込む、などは現実的なユースケースではほぼ無いと思われる

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

Server Actions 導⼊のポイント

Slide 51

Slide 51 text

Server Actions と再レンダリング

Slide 52

Slide 52 text

Server Actions と再レンダリング • Server Actions 内で revalidatePath または revalidateTag 
 を実⾏すると、現在ページの再レンダリングが⾏われ、 
 レスポンスに RSC Payload が含まれる • これを利⽤することで、更新後の再描画が簡単に⾏える • Path および Tag の⼀致は関係なく、実⾏有無で判断される 
 ※v14.0.2-canary.13 時点

Slide 53

Slide 53 text

Server Actions と再レンダリング revalidateTag の実⾏無し revalidateTag の実⾏あり

Slide 54

Slide 54 text

ページ全体が再レンダリングされる • 再レンダリングはページ全体が対象になる点 • サイボウズ Of fi ce のように Data Cache がほぼ無効化されていると、 
 レンダリングのためすべての fetch() も実⾏されることになる • 操作頻度が⾼い場合、負荷増⼤に繋がる可能性に注意が必要 サイボウズ Of fi ce での例: いいね!ボタン

Slide 55

Slide 55 text

Custom Invocation

Slide 56

Slide 56 text

Custom Invocation • Server Actions は を使わず 
 Client Component でのユーザーインタラクションを契機に 
 実⾏することもできる = Custom Invocation

Slide 57

Slide 57 text

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. ちなみに...

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

development production

Slide 60

Slide 60 text

Result 型の利⽤ • Server Actions からの Error の throw は⼀切使えない前提で考える • Client 側でエラーの詳細を把握する必要がある場合、 
 レスポンス内にステータスコードを含める

Slide 61

Slide 61 text

エラー分岐の複雑化 • Result 型でレスポンスを得られるが、 
 ネットワークエラーなども拾って 
 ユーザーにフィードバックしたい • try-catch が避けられず 
 エラー分岐が複雑化する • 汎⽤的なエラー処理もあるため 
 可能な範囲で共通化したい

Slide 62

Slide 62 text

Client 側でエラーの re-throw • Server Actions を実⾏する Wrapper Function を⽤意し、 
 レスポンスに含まれるステータスコードに応じてエラーを再度 throw • catch の中ですべてのエラーをハンドリング可能にしている エラーの内容に応じて独⾃のエラーオブジェクトを作成

Slide 63

Slide 63 text

今後の展望 / Progressive Enhancement の活⽤ • の action や の formAction などを使うことで、 
 ⾃動的に Progressive Enhancement が有効になる • Hydration 前に操作可能になるなど、メリットも多い • useFormStatus / useFormState を組み合わせることで、 
 Progressive Enhancement を維持したまま 
 単純なエラー表⽰や状態管理を実現できる • 徐々に Custom Invocation ではない範囲も広げていきたい

Slide 64

Slide 64 text

MPA の刷新⽂脈から⾒る Next.js App Router と所感

Slide 65

Slide 65 text

MPA 刷新と App Router の相性は良好 • Server Components での描画を第⼀に検討するポリシーとした • Server 側でのレンダリングが基本 • 動きが必要な箇所で Client Component を⽤いる • 従来の MPA での描画とメンタルモデルが近い • MPA では URL 単位で機能の境界が存在することが多い • 段階的に app/ 配下に移植していける • ページ単位で移⾏していける • ⼩さく始めての試⾏錯誤もしやすい

Slide 66

Slide 66 text

所感 • Undocumented な挙動が多い • Next.js および React のコードは頻繁に確認する • 気になる Issue/PR もチーム内で共有している • ⾃分でコントリビュートする気概も必要 • 後はエコシステムが整えばさらに開発しやすくなる印象 • 個⼈的には testing-library を使いたい

Slide 67

Slide 67 text

ありがとう ございました