こんにちは、ルイド Software Engineer パク・ジュンヨルです。
最近、社内のWeb技術交流イベントで React Server Components と Streamingの重要性、そして新しい Data Fetchパターン について発表しました。
発表がメンバーの皆さんにとって有益だったという評価をいただき、社外にも共有したいと思いこのブログ記事を書くことになりました。
多くの方の助けになれば幸いです。
App Routerの登場
賛否両論の多かったNextJSのApp Routerが導入されてから、すでに2年以上が経ちました。最初は Pages Router があるのに、あえて App Router を使う必要があるのかという疑問が多くありました。しかし、NextJSチームの継続的なサポートと努力のおかげで、今では新規プロジェクトのほとんどが Pages Router の代わりに App Router を使って開始されています。
しかし、依然として App Router を使うべき理由と React Server Components の正しい使い方をよく理解している人は多くありません。
この記事を通して、多くの方が React Server Components を使う理由と、パフォーマンスを最大化する正しい使い方を明確に理解できることを願っています。
本記事はReact Server Componentsが何かを説明するというより、React Server Componentsを活用したパフォーマンス改善と新しいData Fetchパターンについて説明します。
React Server Components? Server Side Renderingとは何が違うの?
React Server Components が最初に発表されたとき、多くの人が最も混乱した点は、既存の Server Side Rendering (SSR) と何が違うのかということでした。
React Server Components と Server Side Rendering は似ていますが、明確な違いがあります。
共通点は、どちらの技術も BFF(Backend For Frontend) でコンポーネントを生成する関数が実行されるという点です。
違いは次のとおりです。
ここで React Server Components の重要な利点の1つが現れます。このコンポーネントのコードは BFFでのみ実行 されるため、クライアントに送られる JSバンドル からこのコードを完全に削除できます。
つまり、バンドルサイズを減らせます!
ただし、利点だけがあるわけではありません。
Clientに送信されるJSバンドルのサイズ は減りますが、React Server Components の動作方式により HTMLのサイズ はむしろ増えることになります。

App Routerを使用するWebページのElementsを確認すると、次のようなタグが現れます。
<script>self.__next_f.push(...)</script>
これらのタグは React Server ComponentsのPayload であり、React Server Components が動作するために必要な情報を含んでいます。
例えば、次のような情報がすべて の形でHTMLの末尾部分に追加されます。
React Server Components と Payload についてさらに深く見ていくのもよいのですが、今回の記事では React Server Componentsをうまく活用する方法 を重点的に扱うため、機会があれば次回もっと深く扱ってみたいと思います。
Data Fetch、どこで行うのがよいのか?
今日の主なテーマは、Data Fetchはどこで行うのがよいか という話です。
NextJS のような BFFをサポートするフレームワーク のおかげで、BFFで Data Fetch を行うという選択肢が生まれました。もちろん、認証管理が複雑になったり必要性を感じなかったりして、以前のように ClientでData Fetchを行う場合 も依然として多いです。
しかし、Data Fetchをどこでどのように行うか はユーザー体験に大きな影響を与えます。
したがって、状況に合った正しいパターンを選ぶことが非常に重要です。
次の3つが主要なData Fetchパターンです。
それぞれの Data Fetchパターン を順を追って見ながら、どのようにユーザー体験を最大化できるのかを確認していきましょう。
すべての例は、ユーザーがMain Pageにアクセスしてすでに読み込みが完了している状態で、Detail Pageへルーティングする状況を想定します。
1. Client Data Fetch
1つ目のパターンは、最もなじみのある方式である ClientでData Fetchを行うパターン です。
通常は useEffect を使ったり、Tanstack Query や SWR のようなライブラリを通じてデータを取得したりする方式です。
詳細な動作方式
この中で、ユーザーが詳細ページの有意義な情報を確認できる時点は
4段階目 である レスポンスを受け取った後、実際のデータに基づいてページが再レンダリングされるとき です。
これは、Data Fetch を通じて必要な情報を取得してはじめて、ユーザーが期待する内容を含んだUIを提供できるためです。
各段階にかかる時間を視覚的に確認すると

1番の過程と2番の過程 は 緑色のボックス ほどの時間がかかり、
3番の過程 は 青色のボックス ほどの時間が、
4番の過程 まですべて完了するには 黄色のボックス ほどの時間がかかります。
ここで注意深く見るべき2つの指標は、緑色のボックスと黄色のボックスの大きさです。
緑色のボックスは ユーザーがルーティングのクリックをして実際に移動されるまでにかかる時間 であり、
黄色のボックスは ユーザーがルーティングのクリックをして有意義な情報を確認するまでにかかる時間 です。
緑色のボックスの大きさ が大きいほど、ユーザーはサービスが素早く反応しないと感じ、もどかしさを経験することになります。
黄色のボックスの大きさ が大きいほど、ユーザーが実際にデータを待つ時間が長くなり、アプリが遅いという印象を受けるようになります。
Client Data Fetchの長所
Client Data Fetchの短所
2. Server Data Fetch without Streaming
2つ目のパターンは、ユーザーが詳細ページへルーティングすると BFFでページを生成してClientに一度に完成したページを返す方式 です。
このパターンは次の状況で現れます。
つまり、NextJS ベースのWebサイトでクリック後、ページ移動まで時間がかかる場合、このパターンを使用していると見ることができます。

上の画像をよく見ると、ユーザーがページ移動のためにクリックした後、約 500ms程度のディレイ が発生してから実際のページへ移動していることが確認できます。
以下は2つ目のパターンの詳細ページの読み込みプロセスです。
このプロセスを今回も視覚的に表すと、

Client Data Fetchと比較した際の主な違いは、次の3点です。
特に重要な点は、この方式では青いボックスの大きさがClient Data Fetch方式に比べてより小さいという点です。
Data Fetchを行うBFFは、結局のところ一種のサーバーであるため、次のような利点があります。
注目すべき点は、ユーザーが実際に有意味な情報を確認するまでにかかる時間(黄色いボックスの大きさ)がClient Data Fetchより小さいという点です。
これは次のような理由で可能です。

しかし、最も重要だと考えるインタラクション応答速度(緑のボックス)がClient Data Fetchに比べて著しく大きいことが、Server Data Fetch without Streamingの最大の欠点です。
ユーザーは有意味な情報をより早く受け取れるものの、クリック後に実際のページへルーティングされるまでの時間が長くなり、ユーザーに大きなもどかしさを与えることになります。
一部では次のような質問をするかもしれません。
「結局、Client FetchでもServer FetchでもAPIリクエストを待つのは同じではないですか? それなら、有意味な情報をより早く見られる方式のほうが良いのではないですか?」
しかし、実際のユーザー体験の観点では、このような単純比較は成り立ちません。
これに似た有名な事例として、遅いエレベーターと鏡の設置に関する逸話があります。

以前、ある建物でエレベーターの速度が遅いという利用者の不満が継続的に提起されていました。しかし、技術的にエレベーターの速度を改善することが難しい状況でした。
そこで管理者は、エレベーター内部に大型の鏡を設置しました。すると、その後利用者の不満は著しく減少しました。
その理由は、利用者は実際の待ち時間ではなく、体感する待ち時間により敏感だからです。
以前はエレベーターの中でただ待っているだけだったため待ち時間がより長く感じられていましたが、鏡を設置した後は利用者が自分の姿を見ることで待ち時間をそれほど退屈に感じなくなり、自然と速度に対する不満も減少したのです。
Webアプリケーションでも、まったく同じ原理が適用されます。
APIレスポンス速度そのものを劇的に速くすることには技術的な限界があります。実際のユーザーの立場では、データを受け取る物理的な時間よりも、待機中に提供される視覚的フィードバック(ローディングスケルトン、アニメーション、面白い要素など)によって、アプリ全体の体感速度が異なって感じられます。
したがって、UXの観点で最も重要なのは、ユーザーの体感待機時間を減らせる適切なフィードバックUIとスケルトン処理を提供することです。
むしろSSR(Server Side Rendering)を誤って活用すると、ユーザーはサービスがより遅いと感じることもあります。
3. Server Data Fetch with Streaming
これまで私たちは、Client Data FetchとServer Data Fetch without Streamingの2つを見ながら、それぞれの長所と短所を把握してきました。
2つのパターンの長所だけを組み合わせた魔法のようなパターンがあるとしたらどうでしょうか?
Client Data Fetchの長所である速い反応速度とSkeleton UI、
Server Data Fetch without Streamingの長所である短い読み込み時間を組み合わせたものが、まさにServer Data Fetch with Streamingです。
驚くべきことに、この2つの長所だけを得るための作業はたった1つ、
つまりloading.tsxファイルを追加することです!
NextJSのApp Routerでloading.tsxを追加することは、内部的にはページ全体をSuspenseで包むのと同じ効果を生みます。
Suspenseで包まれた子コンポーネントがレンダリング中にsuspend状態に入ると、完了するまで最も近いSuspense Boundaryのfallbackコンポーネントを表示するようになります。
コンポーネントがsuspend状態になる代表的な状況は次のとおりです。
このような状況では、レンダリングが完了するまでSuspense Boundaryのfallbackを表示します。
つまり、
結局、Server Data Fetch with Streamingは、この2つの長所を自然に組み合わせた理想的なパターンです。
結果としてSuspenseのおかげで、次のような利点が可能になります。
つまり、先ほど説明した2つのパターン(Client Data FetchとServer Data Fetch without Streaming)の長所だけを組み合わせたパターンが、まさにServer Data Fetch with Streamingです。

先ほど見てきた内容を可視化して整理すると、次のようになります。
したがって、もし皆さんがNextJS App Routerを使っているのであれば、必ずloading.tsxファイルを追加して、Server Data Fetch with Streamingの利点を活用してください!
App Routerが初めて公開されたとき、人々はそれまで蓄積してきたNextJSの大きく頻繁な変更に対する不満が爆発し、App Routerの長所よりも短所や不便さにより注目していたと思います。
しかしApp Routerを深く学ぶほど、
NextJSチームが既存のPages Routerの限界や問題点を克服するために、本当に多くの研究と努力を重ねてきたことが分かりました。
その中でも代表的な機能が、まさにStreamingとReact Server Components互換だと思います。
先ほど説明した内容を通して、私たちはStreamingがユーザー体験に非常に重要であるという事実を確認することができました。
従来のPages RouterではStreamingをサポートしていなかったため、速度が遅くなっていました。
また、Static Site Generation(SSG)またはIncremental Static Regeneration(ISR)を適切に活用せず、Server Side Rendering(SSR)だけを使うと、かえってユーザー体験を損ねることもありました。
一方でApp RouterではReact Server Componentsが初めて導入されたことで、
既存のPages Routerではページ単位でしかできなかったSSRが、今ではコンポーネント単位で可能になり、これによってStreaming機能の効果がさらに強化されました。
Streamingによってユーザーは、先に見せられる部分から先に見て、後で準備される部分はその後に見られるようになりました。
従来のPages Routerではコンポーネント単位でServer Side Rendering(SSR)を行うことが不可能だったため、Streaming機能の実質的な利点は限定的でした。
しかしReact Server Componentsが導入されたことで、コンポーネント単位でSSRができるようになり、これによってStreamingが本格的に輝けるようになりました。
たとえば、あるページの90%がStaticで、10%だけがDynamicだと仮定してみましょう。
従来のPages Routerでは、ページの大部分がStaticであっても一部のDynamicな部分のためにページ全体がsuspendされ、ユーザーはすべての内容を待たなければなりませんでした。
一方、App Routerではコンポーネント単位のServer Side RenderingとStreamingが可能になったため、一部のコンポーネントの準備が遅くても、すでに準備できた残りのコンポーネントを先にレンダリングしてユーザーに素早く見せることができます。
つまり、ユーザーはより速くページを体験できるようになるのです。

上のスクリーンショットは、同じページをPages RouterとApp Routerの違いだけを置いて実装した結果です。
同じページ、同じデータであっても、Streamingの有無によってページリソースのローディングタイミングが変わり、結果としてWeb Vitalsの数値とユーザー体験に大きな影響を与えることになります。これは最終的に売上にも直接影響し得ます。
したがって、この記事を通してたった一つだけ覚えるべきことがあるなら、
App Routerを使っているなら、loading.tsxファイルを忘れずに必ず作成しましょう!
4. (番外編ですが本記事の最大の目的)待たなければならない範囲を狭めるおいしいパターン
上記の内容を通して、私たちはData FetchはBFFサーバーで実行し、Streamingを通じてページをClientへ渡す方式が最も理想的であることを知ることができました。
しかし実際の開発過程では、状態管理やインタラクション処理、あるいはReact Hooksの使用が必要で、やむを得ずServer ComponentsではなくClient Componentsを使わなければならない状況も少なくありません。
このような状況で、「仕方なくClient Data Fetchしかできない」と考え、先ほど説明した利点を諦めてしまうこともあるかもしれません。
まさにそんな方のために、便利なパターンを一つご紹介します!
次のようなページ状況を仮定してみましょう。

非常に典型的なページの構造で、これをツリーの形で見ると

このように見ることができます。
機能が新たに追加され、Main Content 1コンポーネントがAPIを呼び出して取得したデータを使う必要があると仮定してみましょう。
このとき、私たちは先ほど学んだServer Data Fetchの利点を活用するために、次のような方法を使うことができます。
こうすることで、Client Componentで直接データを取得する方式よりも、より速くユーザーに意味のある情報を届けることができます。
export default async function Home() {
const data = await getData();
return (
<div className={cn('h-screen w-screen bg-white flex flex-col')}>
<TopBar />
<div className={cn('flex-1 w-full flex')}>
<SideBar />
<div className={cn('flex flex-col w-full')}>
<ContentOne data={data}/>
<ContentTwo />
<ContentThree />
</div>
</div>
</div>
)
}
しかし、この方式には非常に大きな問題があります。
Main PageでData Fetchを行うということは、
- Main PageのレンダリングがSuspendされるという意味であり、
- Main Pageを包むSuspense Boundary(loading.tsx)が発動し、
- Main Page全体がFallbackを表示することになります。

この状況では、実際のデータを必要とするコンポーネントは Main Content 1 だけであるにもかかわらず、Top Bar、Sidebar、Main Content 2、Main Content 3 のような他のコンポーネントまで不要に Fallback に隠れてしまいます。
Server Data Fetch の利点を享受するためだけに、表示できる情報まで隠してユーザー体験を低下させるのは望ましくありません。可能であれば、ユーザーに 先に見せられる情報はすぐに見せたほうがよいです。
このとき、多くの方は自然と次のように考えます。
「Main Content 1 は Client Component だから、どうしても Client Data Fetch をしなければならないんだろうな。」
もちろん、この方法も1つの解決策にはなります。
しかし本当にこの方法しかないのでしょうか?
もし新しい Server Component を1つ作成できるなら、もっとよいアプローチが可能ではないでしょうか?

まさに次のような方法を使うことができます。
export default async function Home() {
return (
<div className={cn('h-screen w-screen bg-white flex flex-col')}>
<TopBar />
<div className={cn('flex-1 w-full flex')}>
<SideBar />
<div className={cn('flex flex-col w-full')}>
<Suspense fallback={<div>Loading...</div>}>
<ContentOneWrapper />
</Suspense>
<ContentTwo />
<ContentThree />
</div>
</div>
</div>
)
}
export const ContentOneWrapper = async () => {
const data = await getData();
return <ContentOne data={data}/>
}
このようにすると、新しい Suspense Boundary を Main Content 1 の外側に追加して Suspend される範囲を狭められるため、

この画像のように、データが必要な Main Content 1 コンポーネントだけが Loading State を表示し、すぐに見せられるコンポーネントは即座にユーザーへ表示されるようになります。ユーザー体験がはるかに改善されるのです。
しかし実際に開発をしていると、企画が変更されたり機能が追加されたりすることが多くあります。
たとえば、追加で Top Bar コンポーネント でも 同じデータ を受け取る必要がある状況が生じたと仮定してみましょう。
この場合、最も早く直感的な方法は次のとおりです。
export default async function Home() {
return (
<div className={cn('h-screen w-screen bg-white flex flex-col')}>
<Suspense fallback={<div>Loading...</div>}>
<TopBarWrapper />
</Suspense>
<div className={cn('flex-1 w-full flex')}>
<SideBar />
<div className={cn('flex flex-col w-full')}>
<Suspense fallback={<div>Loading...</div>}>
<ContentOneWrapper />
</Suspense>
<ContentTwo />
<ContentThree />
</div>
</div>
</div>
)
}
export const TopBarWrapper = async () => {
const data = await getData();
return <TopBar data={data}/>
}
しかし、この方法は適切ではありません。
もし Sidebar や Main Content 2 でも同じデータを使う必要がある場合を考えてみましょう。
さらに、ページ内に 100個のコンポーネント があり、そのうち 99個のコンポーネント が同じデータを必要としているとしたらどうでしょうか?
このような状況で、それぞれのコンポーネントごとに Wrapper コンポーネント を作成してデータを渡す方式は非効率であり、保守にも困難をもたらします。
特に、すべての Wrapper コンポーネントが似たようなロジックを繰り返すことになるため、コード管理が容易ではありません。
もっとよいアプローチはないでしょうか?
もう一度要件を整理してみると、
これを考慮すると、コードの重複を最小化するには Main Page で Data Fetch を行い、受け取ったデータを各コンポーネントに prop として渡す方式が適しています。
しかし、この方式にはデータが必要ないコンポーネントまで不要にブロッキングされるという欠点がありました。
私たちはこの問題を React Server Components の Payload と React の use フック を活用して解決してみようと思います。
本記事の冒頭で説明した React Server Components Payload には次のような値が含まれます。
ここで注目すべき事実は、これらの prop に Promise も含められるという点です。
これをコードで表すと次のようになります。
export default async function Home() {
const dataPromise = getData();
return (
<div className={cn('h-screen w-screen bg-white flex flex-col')}>
<Suspense fallback={<div className='w-full h-[120px] bg-red-300 flex items-center justify-center text-6xl'>Loading...</div>}>
<TopBarWrapper />
</Suspense>
<div className={cn('flex-1 w-full flex')}>
<SideBar />
<div className={cn('flex flex-col w-full')}>
<Suspense fallback={<div className='w-full flex-1 bg-fuchsia-300 flex items-center justify-center text-6xl overflow-hidden'>Loading...</div>}>
<ContentOneWrapper />
</Suspense>
<ContentTwo />
<Suspense fallback={<div className='w-full flex-1 bg-yellow-300 flex items-center justify-center text-6xl'>Loading...</div>}>
<ContentThree dataPromise={dataPromise}/>
</Suspense>
</div>
</div>
</div>
)
}
つまり、上のコードのように getData() で返された Promise を await せずに そのままクライアントコンポーネントへ渡すことができます。
すると Client Component の内部では React use を使って
export default function ContentThree({ dataPromise }: { dataPromise: Promise<Data[]> }) {
const data = use(dataPromise);
return (
<div className={cn('w-full flex-1 bg-yellow-300 flex items-center justify-center text-6xl')}>
Content 3
<div className='flex flex-col'>
{data.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
</div>
)
}
渡されたPromiseに入っている値を受け取れるようになります。
このようにして私たちは、従来の不便で重複したWrapperコンポーネントを作成しなくても

Main PageでData Fetchを行いながらも、データが必要なClient ComponentだけをSuspendさせることができます。
では、既存コードもリファクタリングして不要なWrapperコンポーネントを取り除きましょう。
export default async function Home() {
const dataPromise = getData();
return (
<div className={cn('h-screen w-screen bg-white flex flex-col')}>
<Suspense fallback={<div className='w-full h-[120px] bg-red-300 flex items-center justify-center text-6xl'>Loading...</div>}>
<TopBar dataPromise={dataPromise}/>
</Suspense>
<div className={cn('flex-1 w-full flex')}>
<SideBar />
<div className={cn('flex flex-col w-full')}>
<Suspense fallback={<div className='w-full flex-1 bg-fuchsia-300 flex items-center justify-center text-6xl overflow-hidden'>Loading...</div>}>
<ContentOne dataPromise={dataPromise}/>
</Suspense>
<ContentTwo />
<Suspense fallback={<div className='w-full flex-1 bg-yellow-300 flex items-center justify-center text-6xl'>Loading...</div>}>
<ContentThree dataPromise={dataPromise}/>
</Suspense>
</div>
</div>
</div>
)
}
これでServer ComponentではPromiseを生成するだけにして、データが必要なClient ComponentでのみReactのuseフックを通じてデータを受け取る、非常に実用的なパターンを活用できるようになりました。
Deep Dive: Promiseはどのように渡されるのか?
上で説明したパターンを見ると、resolveされていないPromiseをサーバーからクライアントへ送ることがどうして可能なのか、そしてData Fetchがサーバーとクライアントのどちらで実行されるのか、あるいは両方にまたがって実行されるのかを明確に理解するのは難しいかもしれません。
まず結論から言うと、実際のPromiseはサーバーで管理されます。
そうなると、クライアントはどのようにそのPromiseの値を受け取り、Promiseがresolveされるまでどのように待機するのかという疑問が生じるかもしれません。
React Server ComponentsのPayloadは、JSONを文字列に変換した(Stringify)形でクライアントに送信されます。
つまり、React Server ComponentsのPayloadはJSONシリアライズの過程を経るのですが、その過程でもしシリアライズしようとしている値がPromise(またはThenable)の場合、そのPromise自体をそのまま入れるのではなく、新しいIDを発行してそのPromiseに割り当てたうえでJSONシリアライズを行います。
理解を助けるために、コードでその過程を見てみましょう。すべてのコードはReact公式Githubで確認できます。
1. Jsonシリアライズの過程でPending状態のPromiseを発見

このコードはJSONシリアライズコードの一部で、JSONシリアライズの過程でThenableが見つかった場合、serializeThenable関数によってそのThenableのID値が発行されます。
serializeThenable関数のロジックは複雑ですが、そのPromiseの状態(fulfilled、rejected、pending)に応じて追加ロジックを実行し、最終的には常にそのPromiseのID値を返す、という点だけ理解すれば十分です。
https://github.com/facebook/react/blob/main/packages/react-server/src/ReactFlightServer.js#L674

そしてそのpromiseId値を使ってserializePromiseID関数を呼び出し、JSONをパースするクライアント側でそのIDがPromiseであることを知らせるために'$@'接頭辞(prefix)を追加します。
実際に私たちが先ほど見ていたプロジェクトを確認すると、

{"dataPromise":"$@11"} このようなPayloadを受け取っていたことがわかります。
するとクライアントはこの情報をもとに、現在dataPromiseというpropにはidが11の現在pending状態のpromiseが渡されることを認識できます。
2. Clientでは受け取ったPromiseのIdに対応するPending Chunkを生成

クライアントは Payload をパースする過程で '$@' を見つけた場合、この値が Promise の ID 値であることを認識し、この ID 値を使って新しい Pending chunk を生成します。

この Pending Chunk は内部的には Pending 状態の Promise を生成することと同じであり、React の use ではこの Promise を待つことになります。
実際には Promise ではなく、受け取った chunk をラップし、Promise のプロトタイプを再定義した ReactPromise という新しいオブジェクトですが、理解のために Promise と表現しました。
https://github.com/facebook/react/blob/main/packages/react-client/src/ReactFlightClient.js#L233
3. Promise が Resolve されて目的のデータを受け取る
BFF で管理していた実際の Promise が resolve され、私たちが欲しいデータの準備ができると、そのデータを既存の Promise の ID 値とともにクライアントへ送信します。
このデータも同様にストリーミングを通じてクライアントへ渡され、形式も同じく になります。
実際に上のプロジェクトの element を見てみると、

このようなデータを見つけることができますが、
この.push([1, "11:[{\"id\":1,...}"] というデータを少し分析してみると、
[1, …] は現在入ってきたデータが React Server Components の Payload であることを意味し、 "11:[{\"id\":1,...}" は Id が 11 の Promise のデータは …である、ということを意味します。
では実際のコードではどのようにこのデータを解釈し、既に suspend されていたコンポーネントをどのように続けてレンダリングするのかを見ていきましょう。
4. Client で Payload をパースし、コンポーネントレンダリングまで
受け取った Payload をパースし、suspend されたコンポーネントのレンダリングを続けるために、まず受け取ったデータから以下の resolveModel 関数を通して既存の pending chunk を見つけ、resolveModelChunk 関数を呼び出します。

既存の pending chunk とデータを使って resolveModelChunk 関数を呼び出すと、

initializeModelChunk 関数を通じて既存の Pending 状態の Promise に受け取ったデータを追加し、
wakeChunkIfInitialized 関数を通じて Pending 状態の Promise を待っていたコンポーネントを「起こして」レンダリングを再開します。
このような過程を通じて React は、サーバーで管理する Promise を直接クライアントに渡さなくても、固有の ID とストリーミングされたデータを活用して、クライアントがその Promise の状態と結果を認識し、適切にレンダリングできるようにします。
今回の記事では、さまざまな Data Fetch パターン と React Server Components の長所を活かした新しいパターン について見てきました。
長い文章を読んでいただき、ありがとうございました!


