$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. Next.js App Router での


    MPA フロントエンド刷新
    JSConf JP 2023


    Hajime Mugishima / Cybozu Inc.

    View Slide

  2. @mugi_uno
    Hajime Mugishima
    Cybozu Inc.
    Frontend Expert Team

    View Slide

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


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


    • Caching


    • Server Components & Client Components


    • Storybook


    • Testing


    • Server Actions


    • MPA 刷新の⽂脈から⾒る App Router と所感

    View Slide

  4. 話さないこと
    • Next.js App Router の基本的な概要・概念


    • 個別機能で複雑な部分のみ説明します


    • Next.js および App Router を選定した詳細な経緯や⽐較検討候補


    • 段階的な移⾏のためのインフラ周りの仕組み


    • 1ページずつ段階刷新可能な環境は構築しています
    時間がたりない…
    ※もし興味があれば、懇親会やまた別の機会などで…

    View Slide

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

    View Slide

  6. サイボウズ Office
    • サイボウズが提供するグループウェア


    • 1997年にパッケージ販売を開始


    • 2011年以降はクラウド版を提供

    View Slide

  7. 現⾏のアーキテクチャ
    • C++ で書かれた CGI アプリケーション


    • 完全独⾃のスクリプト⾔語を全ての View テンプレートで利⽤


    • フロントエンドはかなりレガシーな状態


    • ES5 & jQuery


    • モジュールバンドラ・型 などは無し

    View Slide

  8. 刷新プロジェクトの始動
    • 開発・保守のコストが徐々に問題に...


    • フロントエンドの刷新を決定

    (通称 DOGOプロジェクト)
    爆誕したチームのロゴ

    View Slide

  9. 刷新後アーキテクチャ
    • Next.js App Router


    • CSS Modules


    • React Spectrum (React Aria / React Stately)


    • TypeScript


    • Jest / Playwright

    View Slide

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

    View Slide

  11. Caching

    View Slide

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

    View Slide

  13. App Router におけるキャッシュとは
    • キャッシュと呼ばれるものは4種類存在する


    • Request Memoization


    • Data Cache


    • Full Route Cache


    • Router Cache
    🔗 https://nextjs.org/docs/app/building-your-application/caching

    View Slide

  14. Request Memoization
    • 単⼀のリクエストでのコンポーネントツリーのレンダリング中、

    複数発⽣した同様の fetch() の重複を排除する機能


    • 以前は Automatic fetch() Request Deduping と呼ばれていた


    • リクエスト単位で動作する

    View Slide

  15. Data Cache
    • fetch() のレスポンスをキャッシュする


    • 再検証は時間単位(revalidate)や revalidateTag() / revalidatePath() で⾏う


    • 複数リクエスト横断で動作する

    View Slide

  16. Full Route Cache
    • レンダリングした HTML および RSC Payload をキャッシュ


    • キャッシュに HIT するとレンダリングをスキップする


    • 複数リクエスト横断で動作する

    View Slide

  17. Router Cache
    • すでに表⽰したページや、

    などで prefetch した

    内容をキャッシュする


    • ひとつ前のセッションで

    みなさん完全に理解したはず


    • クライアント側で動作する




    View Slide

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

    View Slide

  19. 刷新⽂脈におけるキャッシュの利⽤
    • "グループウェアの刷新" という⼤前提から、

    基本的には最新データの表⽰が期待される


    • Data Cache および Full Route Cache の利⽤には

    fetch() 対象リソースおよび Segment 単位で

    どの程度キャッシュを保持して良いか検討・設計が必要となる


    • 刷新に加えて +α の改善・最適化の作業が発⽣する


    • 刷新進捗を優先して、Data Cache と Full Route Cache は

    利⽤しない判断をした

    View Slide

  20. 確実にキャッシュを無効化するには?
    • サイボウズOf
    fi
    ceはグループウェアという性質上、

    ほぼすべてのページの表⽰で認証/認可が前提


    • キャッシュ周りについては特に懸念点となる

    → 他企業/ユーザのデータは絶対に⾒えてはいけない


    • Data Cache および Full Route Cache は

    ユーザー横断でキャッシュが共⽤されるため

    意図通り無効化できないとリスクになる?

    View Slide

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

    View Slide

  22. Dynamic Functions とキャッシュ
    • cookies() や headers() は Dynamic Functions と呼ばれる


    • レンダリング中に Dynamic Functions が含まれると、

    Full Route Cache は利⽤されず、

    基本的にすべて動的レンダリングとなる


    • Dynamic Functions 実⾏後の fetch() についても

    基本的にキャッシュ (Data Cache) は利⽤されない

    View Slide

  23. 認証/認可に Cookie を⽤いる





    キャッシュは利⽤されない?

    View Slide

  24. Dynamic Functions とキャッシュ
    • cookies() や headers() は Dynamics Function と呼ばれる


    • レンダリング中に Dynamic Functions が含まれると、

    Full Route Cache は利⽤されず、

    基本的にすべて動的レンダリングとなる


    • Dynamic Functions 実⾏後の fetch() についても

    基本的にキャッシュ (Data Cache) は利⽤されない

    View Slide

  25. Dynamic Functions でのキャッシュ無効化 = デフォルト挙動





    設定によっては有効化される

    View Slide

  26. Dynamic Functions & Full Route Cache
    • Route Segment Con
    fi
    g の dynamic 指定で

    強制的に Static Rendering への切り替えが可能


    • Dynamic Functions の返り値は空になる

    View Slide

  27. Dynamic Functions & Data Cache
    • Dynamic Functions 実⾏後の fetch() でも

    オプションが優先され Data Cache が利⽤される


    • revalidate > 0


    • cache: "force-cache"

    (Route Segment Con
    fi
    g の fetchCache = "force-cache" なども同様)

    View Slide

  28. 挙動のまとめ
    • Data Cache および Full Route Cache は

    リクエスト横断でキャッシュが共⽤される


    • Dynamic Functions によって

    キャッシュの利⽤はデフォルトで回避される


    • 個々の fetch() オプションによってさらに上書きして有効化できる

    View Slide

  29. 最終的なキャッシュ無効化の⽅針
    • 明⽰的にキャッシュが無効な旨を何らかの形で定義しておきたい


    • 個々の fetch() での上書きも抑制したい


    • Route Segment Con
    fi
    g で dynamic = "force-dynamic" を指定

    →動的レンダリング強制 & fetch() のキャッシュも無効化(上書き不可)


    • Undocumented な挙動には頼らず、明⽰的にキャッシュを無効化する


    • 念のため、Incremental Cache Handler で

    noop なキャッシュハンドラも指定している

    View Slide

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

    View Slide

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

    View Slide

  32. Server Components

    &

    Client Components

    View Slide

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

    View Slide

  34. 困るケース: Server / Client で共通のコンポーネント
    • Server Components / Client Components の両⽅から使える

    共通コンポーネントを定義したいケースがある
    • a11y のため React Aria を使いたい


    • React Aria は 内部的に useState / useEffect に依存

    → 利⽤する際は Client Components である必要がある


    • などの Wrapper ではイベントハンドラを取りたい

    → "use client" を付与したいのに付与できない…?
    発⽣した例 : React Aria の Wrapper コンポーネント

    View Slide

  35. Optional Props でハンドラを受け取る
    • 似たユースケースで参考になるものとして

    next/link の コンポーネントが挙げられる


    • "use client" を付与した上で、

    イベントハンドラを Optional Props として受け取っている

    (この場合 TS Warning は発⽣しない)

    View Slide

  36. 最終的な "use client" の付与ポリシー
    • 基本的には Server Components を前提とする


    • useState / useEffect などを⽤いた

    Client 側でのユーザーインタラクションが必要になったら

    境界を定めて "use client" を付与する


    • Server / Client で汎⽤的なコンポーネントが必要なケースなどでは、

    例外的に "use client" の付与を許容し、

    イベントハンドラなどは Optional Props として受け取る

    View Slide

  37. Storybook

    View Slide

  38. Storybook と Server Components
    • Storybook を導⼊し、

    コンポーネント単位での⾒た⽬・振る舞いの確認をしたい


    • 現状では Storybook は Server Components 未サポート


    • どうするか?

    View Slide

  39. Presentational Component の分離
    • ⾒た⽬の部分のみを担保するコンポーネントを分離する


    • いわゆる Container / Presentational Pattern に近い形


    • co-location し、近くに <_Components> の形で配備する形とした
    ↓Storybookでの表⽰対象

    View Slide

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

    View Slide

  41. Storybook と Server Components の今後
    • Server Components サポートに関する Issue は存在する
    [Feature Request]: Support React Server Components (RSC) #21540


    🔗 https://github.com/storybookjs/storybook/issues/21540
    • プロトタイプは完成しており、数ヶ⽉以内に

    Experimental な形でのリリースがあるとのこと


    • Presentational Component の分離は不要になる未来が来るかも?


    View Slide

  42. Testing

    View Slide

  43. Server Components とテスト
    • testing-library は未対応

    🔗 https://github.com/testing-library/react-testing-library/issues/1209


    • 関数として直接実⾏すればテストできなくもない





    →破綻しやすい

    ・Async Component がネストすると対応できない

    ・後から⼦孫コンポーネントで fetch() が必要になることも多い

    View Slide

  44. Experimental test mode for Playwright
    • Experimental だが、

    Playwright サポートが存在


    • fetch() のモック化や

    MSW との統合を提供する


    • 部分的なレンダリングは不可



    ページ内で必要な API は

    すべてモック化が必要
    QBDLBHFTOFYUTSDFYQFSJNFOUBMUFTUNPEFQMBZXSJHIU3&"%.&NEΑΓൈਮ

    View Slide

  45. • Docker Compose でバックエンドごと⽴ち上げた環境に対し、

    Playwright でのテストを中⼼に整備する⽅針とした

    (これ相当のものを E2E と呼んでいる⼈もいるかも)


    • データのセットアップは、必要に応じて内部 API を事前実⾏


    • チームのテストに関するポリシーにもマッチしていた


    • 刷新前の段階で⾃動テストが無い画⾯も多く、効率よく拡充したい


    • Next.js のアップデートや⻑期の刷新作業での内部更新も予想され、

    範囲を広めにした振る舞いの担保を厚めにしておきたい
    Integration Test (≒E2E) を主軸とした

    View Slide

  46. テストの課題と展望
    • 実質 E2E が中⼼となるため、 将来的な実⾏コストの増⼤が懸念点

    (ひとまず Sharding で分散している状態)


    • Client Components または Shared Components は

    Jest での Unit Test でカバーし、Integration Test の軽減は試みている


    • QA と協⼒し、効率の良いテストケース設計となるよう調整している


    • testing-library の Server Components サポートが来たら

    そちらの⽐重を増やしていけるかも

    View Slide

  47. Server Actions

    View Slide

  48. Server Actions とは
    • 簡単に⾔うと「バックエンドで実⾏されるコードを、クライアントから直接呼び出している
    ように書ける」機能 (≒ React 提供の RPC)


    • Progressive Enhancement をサポートしている


    • Next.js 14 以降は Stable (Server Actions ⾃体は React 側の機能)


    • インラインで SQL を直接埋め込む、などは現実的なユースケースではほぼ無いと思われる

    View Slide

  49. Server Actions の採⽤
    • 使わないケースと実装⽅法が⼤幅に異なる


    • 仮に Server Actions が主流になるとすべて書き換える必要が?


    • fetch() している Server Components をリフレッシュする⽅法が

    現実的には Server Actions & revalidate の⼀択


    • 採⽤しない場合、router.refresh() でページ全体を再描画するか、

    データ取得箇所も Client 側で制御する形とする必要が⽣じる


    • Issue や PR の状態から、早くに Stable になると予測


    • 前述の通り、振る舞いを担保するテストを厚めに⽤意しており、

    「根本的に動作しなくなった」といったケースは即時検知できる状態


    • 覚悟

    View Slide

  50. Server Actions 導⼊のポイント

    View Slide

  51. Server Actions と再レンダリング

    View Slide

  52. Server Actions と再レンダリング
    • Server Actions 内で revalidatePath または revalidateTag

    を実⾏すると、現在ページの再レンダリングが⾏われ、

    レスポンスに RSC Payload が含まれる


    • これを利⽤することで、更新後の再描画が簡単に⾏える


    • Path および Tag の⼀致は関係なく、実⾏有無で判断される

    ※v14.0.2-canary.13 時点

    View Slide

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

    View Slide

  54. ページ全体が再レンダリングされる
    • 再レンダリングはページ全体が対象になる点


    • サイボウズ Of
    fi
    ce のように Data Cache がほぼ無効化されていると、

    レンダリングのためすべての fetch() も実⾏されることになる


    • 操作頻度が⾼い場合、負荷増⼤に繋がる可能性に注意が必要
    サイボウズ Of
    fi
    ce での例: いいね!ボタン

    View Slide

  55. Custom Invocation

    View Slide

  56. Custom Invocation
    • Server Actions は を使わず

    Client Component でのユーザーインタラクションを契機に

    実⾏することもできる = Custom Invocation

    View Slide

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

    View Slide

  58. 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

    View Slide

  59. development
    production

    View Slide

  60. Result 型の利⽤
    • Server Actions からの Error の throw は⼀切使えない前提で考える


    • Client 側でエラーの詳細を把握する必要がある場合、

    レスポンス内にステータスコードを含める

    View Slide

  61. エラー分岐の複雑化
    • Result 型でレスポンスを得られるが、

    ネットワークエラーなども拾って

    ユーザーにフィードバックしたい


    • try-catch が避けられず

    エラー分岐が複雑化する


    • 汎⽤的なエラー処理もあるため

    可能な範囲で共通化したい

    View Slide

  62. Client 側でエラーの re-throw
    • Server Actions を実⾏する Wrapper Function を⽤意し、

    レスポンスに含まれるステータスコードに応じてエラーを再度 throw


    • catch の中ですべてのエラーをハンドリング可能にしている
    エラーの内容に応じて独⾃のエラーオブジェクトを作成

    View Slide

  63. 今後の展望 / Progressive Enhancement の活⽤
    • の action や の formAction などを使うことで、

    ⾃動的に Progressive Enhancement が有効になる


    • Hydration 前に操作可能になるなど、メリットも多い


    • useFormStatus / useFormState を組み合わせることで、

    Progressive Enhancement を維持したまま

    単純なエラー表⽰や状態管理を実現できる


    • 徐々に Custom Invocation ではない範囲も広げていきたい

    View Slide

  64. MPA の刷新⽂脈から⾒る


    Next.js App Router と所感

    View Slide

  65. MPA 刷新と App Router の相性は良好
    • Server Components での描画を第⼀に検討するポリシーとした


    • Server 側でのレンダリングが基本


    • 動きが必要な箇所で Client Component を⽤いる


    • 従来の MPA での描画とメンタルモデルが近い


    • MPA では URL 単位で機能の境界が存在することが多い


    • 段階的に app/ 配下に移植していける


    • ページ単位で移⾏していける


    • ⼩さく始めての試⾏錯誤もしやすい

    View Slide

  66. 所感
    • Undocumented な挙動が多い


    • Next.js および React のコードは頻繁に確認する


    • 気になる Issue/PR もチーム内で共有している


    • ⾃分でコントリビュートする気概も必要


    • 後はエコシステムが整えばさらに開発しやすくなる印象


    • 個⼈的には testing-library を使いたい

    View Slide

  67. ありがとう


    ございました

    View Slide