<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Next.js on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/next.js/</link>
    <description>Recent content in Next.js on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Sun, 22 Feb 2026 18:00:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/next.js/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Next.js 16の変更点まとめ：Partial Prerenderingが安定版に</title>
      <link>https://www.ai2core.com/posts/2026-02-22-nextjs-16/</link>
      <pubDate>Sun, 22 Feb 2026 18:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-22-nextjs-16/</guid>
      <description>動的コンテンツと静的コンテンツを混在させるPPRの正式リリースについて。</description>
      <content:encoded><![CDATA[<h1 id="nextjs-16の変更点まとめpartial-prerenderingが安定版に">Next.js 16の変更点まとめ：Partial Prerenderingが安定版に</h1>
<h2 id="はじめに">はじめに</h2>
<p>「Next.jsアプリのパフォーマンス、もっと向上させたい…」
「静的サイトのような爆速表示と、動的コンテンツのリアルタイム更新、この二つを両立できずに悩んでいませんか？」</p>
<p>Next.js開発者であれば、一度はこのような課題に直面したことがあるでしょう。私たちはこれまで、ページの特性に応じてServer-Side Rendering (SSR)、Static Site Generation (SSG)、Incremental Static Regeneration (ISR) といったレンダリング戦略を使い分ける必要がありました。</p>
<ul>
<li><strong>SSR</strong>は動的なコンテンツに強い反面、リクエストごとにサーバーでレンダリングするためTime to First Byte (TTFB) が遅くなりがちです。</li>
<li><strong>SSG</strong>はCDNから配信できるため非常に高速ですが、動的コンテンツの扱いやビルド時間の増大という課題を抱えています。</li>
<li><strong>ISR</strong>はSSGの弱点を補いますが、リアルタイムの更新には対応しきれない場面もあります。</li>
</ul>
<p>これらの戦略は強力ですが、ページ全体を「静的」か「動的」のどちらか一方の型にはめる「All or Nothing」のアプローチでした。例えば、大半が静的なEコマースの商品ページでも、在庫情報やレビューといった一部の動的コンテンツのために、ページ全体をSSRせざるを得ず、パフォーマンスを犠牲にすることが少なくありませんでした。</p>
<p>この長年のジレンマに終止符を打つべく登場し、Next.js 16でついに安定版となったのが <strong>Partial Prerendering (PPR)</strong> です。</p>
<p>PPRは、静的なパフォーマンスと動的な柔軟性を融合させる、まさに革命的なレンダリングモデルです。この記事では、PPRがなぜ重要なのか、その仕組み、具体的な実装方法から実践的なTipsまで、Next.js 16の目玉機能であるPPRを徹底的に解剖します。この記事を読み終える頃には、あなたはPPRを完全に理解し、自身のプロジェクトでその強力なパフォーマンスを最大限に引き出せるようになっているはずです。</p>
<h2 id="なぜpprは登場したのかwebレンダリングの進化と課題">なぜPPRは登場したのか？Webレンダリングの進化と課題</h2>
<p>PPRの革新性を理解するためには、まずこれまでのWebレンダリングがどのような変遷を辿ってきたのかを知る必要があります。</p>
<h3 id="webレンダリング戦略の歴史">Webレンダリング戦略の歴史</h3>
<ol>
<li>
<p><strong>サーバーサイドレンダリングの黎明期 (CGI, PHP, Ruby on Rails)</strong>
Webの初期、コンテンツはリクエストごとにサーバーでHTMLを生成して返すのが主流でした。これにより動的なコンテンツは実現できましたが、リクエストのたびに重い処理が走るため、パフォーマンスに課題がありました。</p>
</li>
<li>
<p><strong>SPA (Single Page Application) の台頭 (React, Vue, Angular)</strong>
次に、クライアントサイドでJavaScriptがUIを構築するSPAが登場しました。初回ロード後はページ遷移なしに高速な画面更新が可能になり、リッチなUIが実現できます。しかし、初回のバンドルサイズが大きく初期表示が遅い、そしてSEOに課題があるという弱点がありました。</p>
</li>
<li>
<p><strong>Next.jsがもたらしたハイブリッドアプローチ (SSR, SSG, ISR)</strong>
この両者の課題を解決するために登場したのがNext.jsです。Next.jsは、ページ単位で最適なレンダリング戦略を選択できるハイブリッドなフレームワークとして絶大な支持を得ました。</p>
<ul>
<li><strong>SSR (Server-Side Rendering):</strong> リクエストごとにサーバーでHTMLを生成。SEOに強く、初期表示に必要なデータが揃っています。しかし、サーバー負荷が高く、TTFBが遅くなりがちです。</li>
<li><strong>SSG (Static Site Generation):</strong> ビルド時にすべてのページをHTMLとして事前生成。CDNに配置することで、リクエスト時にはファイルを返すだけなので超高速です。しかし、ユーザー固有の情報のような動的コンテンツには向かず、サイト規模が大きくなるとビルド時間が長大になる問題がありました。</li>
<li><strong>ISR (Incremental Static Regeneration):</strong> SSGの弱点を補う仕組み。一定時間ごとにバックグラウンドでページを再生成し、静的コンテンツの鮮度を保ちます。しかし、リアルタイム性が求められるコンテンツには対応が難しいという側面がありました。</li>
</ul>
</li>
</ol>
<h3 id="従来のレンダリング戦略が抱えるall-or-nothing問題">従来のレンダリング戦略が抱える「All or Nothing」問題</h3>
<p>これらの戦略は非常に強力でしたが、根本的な課題を抱えていました。それは、<strong>1つのページは1つのレンダリング戦略しか選べない</strong>という「All or Nothing」の問題です。</p>
<p>この問題が顕著になるのが、静的コンテンツと動的コンテンツが混在するページです。先ほどのEコマースの商品ページを例に考えてみましょう。</p>
<ul>
<li><strong>静的な部分:</strong> 商品名、商品説明、スペック、メイン画像など、滅多に変わらない情報。</li>
<li><strong>動的な部分:</strong> 在庫数、ユーザーレビュー、あなたへのおすすめ商品、セール価格など、リアルタイムで変化する情報。</li>
</ul>
<p>従来のモデルでは、このページに動的な部分が一つでも含まれていると、ページ全体をSSRまたはISR（短い間隔での再生成）で構築する必要がありました。これにより、本来は高速に表示できるはずの静的な部分まで、リクエストのたびにサーバーでレンダリングされるのを待たなければならず、TTFBが悪化し、ユーザー体験を損なう原因となっていました。</p>
<p>この「All or Nothing」の制約を打ち破り、静的な部分のパフォーマンスと動的な部分の柔軟性を1つのページ内で共存させるために開発されたのが、Partial Prerendering (PPR)なのです。</p>
<h2 id="partial-prerendering-ppr-の核心に迫る仕組みと使い方">Partial Prerendering (PPR) の核心に迫る：仕組みと使い方</h2>
<p>PPRは、Next.js App RouterとReact Server Components (RSC) のアーキテクチャを基盤として実現されています。そのコンセプトは非常にシンプルです。「ページの静的な部分を事前にビルドしておき、動的な部分だけをリクエスト時にストリーミングで配信する」というものです。</p>
<h3 id="静的シェルと動的ホール">「静的シェル」と「動的ホール」</h3>
<p>PPRを理解する上で最も重要な概念が「静的シェル (Static Shell)」と「動的ホール (Dynamic Holes)」です。</p>
<ul>
<li><strong>静的シェル:</strong> ページのレイアウト、ナビゲーションバー、フッター、そして商品名や商品説明といった静的なコンテンツを含む、ページの骨格となる部分です。これはビルド時に事前生成（Prerender）され、CDNにキャッシュされます。</li>
<li><strong>動的ホール:</strong> 在庫情報やレビューなど、動的に生成されるコンテンツが入る「穴」の部分です。静的シェルの中では、この部分はプレースホルダーとして空けられています。</li>
</ul>
<p>この仕組みを図でイメージしてみましょう。</p>
<pre tabindex="0"><code>【ビルド時】
+-------------------------------------------------+
|               静的シェル (Static Shell)           |
| +-------------------------------------------+ |
| | ナビゲーションバー (静的)                 | |
| +-------------------------------------------+ |
| |                                           | |
| | 商品画像、商品名、商品説明 (静的)         | |
| |                                           | |
| | +---------------------------------------+ | |
| | | 動的ホール (1): レビュー (プレースホルダー)| |
| | +---------------------------------------+ | |
| |                                           | |
| | +---------------------------------------+ | |
| | | 動的ホール (2): おすすめ商品 (プレースホルダー) | |
| | +---------------------------------------+ | |
| |                                           | |
| +-------------------------------------------+ |
| | フッター (静的)                           | |
| +-------------------------------------------+ |
+-------------------------------------------------+
</code></pre><h3 id="pprの動作フロー">PPRの動作フロー</h3>
<p>ユーザーがページにアクセスした際の動作フローは以下のようになります。</p>
<ol>
<li><strong>即時レスポンス:</strong> ユーザーからのリクエストに対し、CDNにキャッシュされている「静的シェル」が即座に返されます。これにより、TTFBが劇的に改善され、ユーザーはすぐにページの骨格を見ることができます。</li>
<li><strong>動的コンテンツのストリーミング:</strong> ブラウザが静的シェルを表示している間に、Next.jsサーバーは「動的ホール」の中身を非同期でレンダリングします。</li>
<li><strong>コンテンツの注入:</strong> レンダリングが完了した動的コンテンツは、HTTPストリーミングを通じてブラウザに送信され、対応するプレースホルダー部分にシームレスに注入されます。この間、<code>Suspense</code>の<code>fallback</code>に指定したスケルトンUIやローディングスピナーが表示されるため、ユーザーはデータが読み込み中であることを認識できます。</li>
</ol>
<p>このアーキテクチャにより、ユーザーはサーバーでの重い処理が終わるのを待つことなく、インタラクション可能なページを即座に手に入れることができるのです。</p>
<h3 id="コードで見るpprsuspenseが鍵">コードで見るPPR：<code>Suspense</code>が鍵</h3>
<p>驚くべきことに、PPRを有効にするために特別な設定はほとんど必要ありません。Next.js 14.1以降のApp Routerを使用していれば、<strong>PPRはデフォルトで有効</strong>です。</p>
<p>PPRの魔法を実現する鍵となるのが、Reactの <strong><code>&lt;Suspense&gt;</code></strong> コンポーネントです。<code>&lt;Suspense&gt;</code>でラップされたコンポーネントが「動的ホール」として扱われます。</p>
<p>それでは、Eコマースの商品ページを例に、具体的なコードを見ていきましょう。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">44
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">45
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">46
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">47
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">48
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">49
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">50
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">51
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">52
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">53
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">54
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">55
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">56
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">57
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">58
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">59
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// app/products/[id]/page.tsx
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Suspense</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;react&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">ProductDetails</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/ProductDetails&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Reviews</span>, <span style="color:#a6e22e">ReviewsSkeleton</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/Reviews&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Recommendations</span>, <span style="color:#a6e22e">RecommendationsSkeleton</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/Recommendations&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">getStaticProductData</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/lib/products&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// このページ自体は静的として扱われる
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">ProductPage</span>({ <span style="color:#a6e22e">params</span> }<span style="color:#f92672">:</span> { <span style="color:#a6e22e">params</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">id</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span> } }) {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// 商品の基本情報（商品名、説明など）はビルド時に取得される
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// このデータ取得はキャッシュされる
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">product</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">getStaticProductData</span>(<span style="color:#a6e22e">params</span>.<span style="color:#a6e22e">id</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* ============================= */</span>}
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* === ここからが静的シェル === */</span>}
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* ============================= */</span>}
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">header</span>&gt;<span style="color:#a6e22e">My</span> <span style="color:#a6e22e">E</span><span style="color:#f92672">-</span><span style="color:#a6e22e">Commerce</span> <span style="color:#a6e22e">Site</span>&lt;/<span style="color:#f92672">header</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">main</span>&gt;
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* このコンポーネントは静的にレンダリングされる */</span>}
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">ProductDetails</span> <span style="color:#a6e22e">product</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">product</span>} /&gt;
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">hr</span> /&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* ========================================= */</span>}
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* === ここからが動的なホール (1): レビュー === */</span>}
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* ========================================= */</span>}
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">Suspense</span> <span style="color:#a6e22e">fallback</span><span style="color:#f92672">=</span>{&lt;<span style="color:#f92672">ReviewsSkeleton</span> /&gt;}&gt;
</span></span><span style="display:flex;"><span>          {<span style="color:#75715e">/* 
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            Reviewsコンポーネントは内部で動的なデータ取得を行う。
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            (例: fetch(..., { cache: &#39;no-store&#39; }))
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            このため、この部分はリクエスト時にレンダリングされる。
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          */</span>}
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">Reviews</span> <span style="color:#a6e22e">productId</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">product</span>.<span style="color:#a6e22e">id</span>} /&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">Suspense</span>&gt;
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">hr</span> /&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* =================================================== */</span>}
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* === ここからが動的なホール (2): おすすめ商品 === */</span>}
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* =================================================== */</span>}
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">h2</span>&gt;<span style="color:#a6e22e">あなたへのおすすめ</span>&lt;/<span style="color:#f92672">h2</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">Suspense</span> <span style="color:#a6e22e">fallback</span><span style="color:#f92672">=</span>{&lt;<span style="color:#f92672">RecommendationsSkeleton</span> /&gt;}&gt;
</span></span><span style="display:flex;"><span>          {<span style="color:#75715e">/* 
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            Recommendationsコンポーネントも動的なデータ取得を行う。
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            レビューとは独立してストリーミングされる。
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          */</span>}
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">Recommendations</span> <span style="color:#a6e22e">category</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">product</span>.<span style="color:#a6e22e">category</span>} /&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">Suspense</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">main</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">footer</span>&gt;<span style="color:#a6e22e">Footer</span> <span style="color:#a6e22e">Content</span>&lt;/<span style="color:#f92672">footer</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* =========================== */</span>}
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* === ここまでが静的シェル === */</span>}
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* =========================== */</span>}
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>このコードのポイントは以下の通りです。</p>
<ol>
<li><strong>ページの基本構造は静的:</strong> <code>ProductPage</code>自体は<code>async</code>コンポーネントですが、内部で行われるデータ取得 (<code>getStaticProductData</code>) がキャッシュされるため、ページの主要部分は静的に事前生成されます。これが「静的シェル」となります。</li>
<li><strong><code>Suspense</code>で動的境界を定義:</strong> <code>&lt;Reviews&gt;</code>と<code>&lt;Recommendations&gt;</code>コンポーネントは、それぞれ<code>&lt;Suspense&gt;</code>でラップされています。これらのコンポーネントは、内部で動的なデータフェッチ（例えば <code>fetch</code> の <code>cache: 'no-store'</code> オプションや、<code>cookies()</code>、<code>headers()</code> といったDynamic Functionsの使用）を行うため、「動的ホール」として扱われます。</li>
<li><strong><code>fallback</code>でUXを向上:</strong> 動的コンテンツがロードされるまでの間、<code>fallback</code>プロパティに指定されたスケルトンコンポーネント (<code>&lt;ReviewsSkeleton /&gt;</code>, <code>&lt;RecommendationsSkeleton /&gt;</code>) が表示されます。これにより、レイアウトシフトを防ぎ、ユーザー体験を向上させます。</li>
</ol>
<p>このように、開発者は「どこを動的にしたいか」を<code>&lt;Suspense&gt;</code>で宣言するだけで、Next.jsが自動的にPPRを適用し、最適なレンダリングを行ってくれるのです。</p>
<h2 id="pprがもたらすメリットと考慮すべき点">PPRがもたらすメリットと考慮すべき点</h2>
<p>PPRは多くのメリットをもたらしますが、採用する上で知っておくべき考慮点も存在します。</p>
<h3 id="メリット">メリット</h3>
<ol>
<li>
<p><strong>圧倒的なパフォーマンス向上:</strong></p>
<ul>
<li><strong>TTFB (Time to First Byte) の劇的な改善:</strong> CDNから静的シェルを即座に返せるため、サーバーでのレンダリング待ち時間がなくなり、TTFBは限りなくゼロに近づきます。</li>
<li><strong>FCP/LCP の高速化:</strong> ユーザーはページの主要なコンテンツをすぐに視認できるため、First Contentful Paint (FCP) や Largest Contentful Paint (LCP) といったCore Web Vitalsの指標が大幅に改善されます。</li>
</ul>
</li>
<li>
<p><strong>最高のユーザー体験 (UX):</strong></p>
<ul>
<li>静的な部分は即座に表示され、インタラクション可能になります。</li>
<li>動的な部分はスケルトンUIなどで読み込み中であることが明示されるため、ユーザーは白紙の画面を見て待たされるストレスから解放されます。</li>
</ul>
</li>
<li>
<p><strong>優れた開発体験 (DX):</strong></p>
<ul>
<li>「SSGかSSRか」というページ単位での二者択一の悩みから解放されます。</li>
<li>開発者はコンポーネント単位でキャッシュ戦略を考えるだけでよくなり、より直感的にアプリケーションを構築できます。</li>
<li>複雑な設定は不要で、Reactの標準的な機能である<code>&lt;Suspense&gt;</code>を使うだけで実現できるシンプルさも魅力です。</li>
</ul>
</li>
<li>
<p><strong>インフラコストの削減:</strong></p>
<ul>
<li>静的シェルがCDNから配信されることで、オリジンサーバーへのリクエスト数が減少します。これにより、サーバーの負荷が軽減され、インフラコストの削減につながる可能性があります。</li>
</ul>
</li>
</ol>
<h3 id="デメリット--考慮すべき点">デメリット / 考慮すべき点</h3>
<ol>
<li>
<p><strong>App RouterとRSCへの依存:</strong>
PPRは、React Server Components (RSC) を前提としたApp Routerのアーキテクチャ上で成り立っています。従来のPages Routerでは利用できず、PPRの恩恵を受けるためにはApp Routerへの移行が必要です。</p>
</li>
<li>
<p><strong><code>Suspense</code>の境界設計の重要性:</strong>
<code>Suspense</code>の境界（boundary）をどこに設定するかが、パフォーマンスとUXを左右します。境界が大きすぎるとストリーミングのメリットが薄れ、細かすぎるとコンポーネントの管理が複雑になる可能性があります。戦略的な設計が求められます。</p>
</li>
<li>
<p><strong>キャッシュ戦略の理解:</strong>
ページ単位ではなく、コンポーネントやデータフェッチ単位でキャッシュを管理する必要があるため、アプリケーション全体のキャッシュ戦略をより深く理解し、設計することが重要になります。特に、意図せずページ全体が動的レンダリングになってしまうケース（ルートレイアウトでのDynamic Functionsの使用など）には注意が必要です。</p>
</li>
<li>
<p><strong>サードパーティライブラリの互換性:</strong>
RSCに対応していないクライアントサイド専用のライブラリ（例: <code>window</code>オブジェクトに直接アクセスするもの）は、<code>'use client'</code>ディレクティブを付けたClient Component内で使用する必要があります。PPRの文脈でもこの原則は変わらないため、ライブラリの選定や利用方法に注意が必要です。</p>
</li>
</ol>
<h2 id="現場で使えるppr実践tips">現場で使えるPPR実践Tips</h2>
<p>PPRを最大限に活用するための、より実践的なヒントをいくつかご紹介します。</p>
<h3 id="tip-1-suspenseの境界を戦略的に設計する">Tip 1: <code>Suspense</code>の境界を戦略的に設計する</h3>
<p><code>Suspense</code>でどこを囲むかは非常に重要です。以下の点を考慮して設計しましょう。</p>
<ul>
<li><strong>Above the Fold (ファーストビュー) は静的に:</strong> ユーザーが最初に目にする画面（スクロールせずに見える範囲）は、可能な限り静的シェルに含め、即座に表示されるようにします。LCPの改善に直結します。</li>
<li><strong>Below the Fold (スクロール後) を動的に:</strong> コメント欄や関連商品リストなど、スクロールしないと見えない部分は、積極的に<code>Suspense</code>で囲み、遅延ロードさせましょう。これにより、初期表示のパフォーマンスへの影響を最小限に抑えられます。</li>
<li><strong>データ取得が遅いコンポーネントを分離する:</strong> 外部APIへの依存などでどうしても読み込みが遅くなるコンポーネントは、他のUIから独立させて<code>Suspense</code>で囲むことで、そのコンポーネントの遅延がページ全体の表示をブロックするのを防ぎます。</li>
</ul>
<h3 id="tip-2-高品質なスケルトンuiを実装する">Tip 2: 高品質なスケルトンUIを実装する</h3>
<p><code>fallback</code>に表示されるUIは、ユーザー体験を大きく左右します。</p>
<ul>
<li><strong>単なる「ローディング中&hellip;」は避ける:</strong> テキストだけのローディング表示は、味気なく、ユーザーに不安を与える可能性があります。</li>
<li><strong>レイアウトを維持するスケルトンを:</strong> 実際に表示されるコンテンツのレイアウトを模したスケルトンUI（Shimmer UIとも呼ばれる）を用意しましょう。これにより、コンテンツが読み込まれた際のレイアウトシフト（CLS: Cumulative Layout Shift）を防ぎ、スムーズな表示を実現できます。<code>shadcn/ui</code>などのライブラリには、便利な<code>Skeleton</code>コンポーネントが用意されています。</li>
</ul>
<h3 id="tip-3-nextjsのキャッシュとレンダリングを可視化デバッグする">Tip 3: Next.jsのキャッシュとレンダリングを可視化・デバッグする</h3>
<p>意図通りにPPRが機能しているかを確認することが重要です。</p>
<ul>
<li><strong>ビルドログの確認:</strong> <code>next build</code> を実行すると、各ページが静的（λ）か動的（○）かが出力されます。PPRが適用されているページは静的（λ）としてマークされるはずです。</li>
<li><strong>ブラウザの開発者ツール:</strong> Networkタブでドキュメントのレスポンスを確認します。PPRが機能している場合、最初のHTMLレスポンスには静的シェルと<code>Suspense</code>の<code>fallback</code>が含まれ、その後、動的コンテンツがチャンクとしてストリーミングされてくる様子を観察できます。</li>
<li><strong>Vercelへのデプロイ:</strong> Vercelにデプロイすると、Speed InsightsやAnalytics機能を使って、TTFBやLCPといったパフォーマンス指標を実環境でモニタリングし、PPR導入による改善効果を定量的に測定できます。</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>Next.js 16で安定版となったPartial Prerendering (PPR)は、単なる新機能ではありません。それは、Webアプリケーションのパフォーマンスと体験を根底から変える、<strong>レンダリングの新しいパラダイム</strong>です。</p>
<p>これまで私たちが直面してきた、静的サイトの「速度」と動的アプリケーションの「柔軟性」のトレードオフ、すなわち「All or Nothing」問題は、PPRによってついに解決されました。PPRは、React Server Componentsと<code>Suspense</code>を組み合わせることで、1つのページ内に静的なシェルと動的なホールを共存させ、両者の利点を同時に享受することを可能にします。</p>
<p>その結果として得られるのは、CDNエッジからの即時レスポンスによる驚異的なTTFB、最適化されたCore Web Vitals、そしてシームレスでストレスのないユーザー体験です。開発者にとっても、複雑なレンダリング戦略の選択に頭を悩ませることなく、コンポーネント単位でその性質を考えるだけで、フレームワークが最適なパフォーマンスを引き出してくれるという、この上ない開発体験を提供します。</p>
<p>App Routerへの移行というハードルはありますが、PPRがもたらす恩恵はそれを補って余りあるものです。今後のWeb開発において、このアーキテクチャがスタンダードになっていくことは間違いないでしょう。</p>
<p>さあ、あなたのNext.jsプロジェクトでも<code>&lt;Suspense&gt;</code>を使って、遅延がちなコンポーネントをラップすることから始めてみませんか？ 小さな一歩からでも、PPRがもたらす次世代のWebパフォーマンスをきっと実感できるはずです。Next.js 16と共に、より速く、より快適なWebの世界を構築していきましょう。</p>
]]></content:encoded>
      <category>Frontend</category>
      <category>Next.js</category>
      <category>PPR</category>
      <category>Web</category>
    </item>
    <item>
      <title>React 20 Server Componentsのベストプラクティス</title>
      <link>https://www.ai2core.com/posts/2026-02-22-react-server-components/</link>
      <pubDate>Sun, 22 Feb 2026 12:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-22-react-server-components/</guid>
      <description>RSCの導入で変わるデータフェッチの常識と、クライアントコンポーネントとの使い分け。</description>
      <content:encoded><![CDATA[<h1 id="react-20-server-componentsのベストプラクティス-rsc導入で変わるデータフェッチの常識とクライアントコンポーネントとの使い分け">React 20 Server Componentsのベストプラクティス: RSC導入で変わるデータフェッチの常識と、クライアントコンポーネントとの使い分け</h1>
<h2 id="はじめに">はじめに</h2>
<p>「Reactのデータフェッチといえば、<code>useEffect</code>と<code>useState</code>を使って、クライアントサイドでAPIを叩くのが当たり前。」
もしあなたがまだそう考えているなら、この記事はあなたの常識を覆すことになるかもしれません。</p>
<p>Next.js 13のApp Routerと共に本格的に導入されたReact Server Components（RSC）は、Reactアプリケーションのアーキテクチャ、特にデータフェッチの方法を根本から変えようとしています。もはや、すべてのコンポーネントがブラウザ上で動くわけではありません。サーバーでレンダリングを完結させ、クライアントにはインタラクティブなUIの部品だけを送る、という新しい時代が到来したのです。</p>
<p>しかし、この大きなパラダイムシフトは、多くの開発者に新たな問いを突きつけています。</p>
<ul>
<li>「Server ComponentとClient Componentは、具体的にどう使い分ければいいの？」</li>
<li>「<code>&quot;use client&quot;</code>はどこに書くのが正解？」</li>
<li>「<code>useEffect</code>でのデータフェッチは、もう時代遅れなの？」</li>
<li>「SWRやReact Query（TanStack Query）はもう不要になる？」</li>
</ul>
<p>この記事では、そんな疑問を抱えるあなたのために、React Server Componentsの核心を解き明かし、次世代のReact開発におけるベストプラクティスを徹底的に解説します。この記事を読み終える頃には、あなたはRSCを自信を持って使いこなし、より高速で、より効率的なWebアプリケーションを構築するための確かな知識を手にしていることでしょう。</p>
<h2 id="なぜrscは登場したのか-従来のreact開発が抱えていた課題">なぜRSCは登場したのか？ 従来のReact開発が抱えていた課題</h2>
<p>React Server Componentsがなぜこれほど注目されているのかを理解するためには、まず従来のクライアントサイドレンダリング（CSR）やサーバーサイドレンダリング（SSR）が抱えていた課題を振り返る必要があります。</p>
<h3 id="課題1-クライアントサイドでのデータフェッチの限界">課題1: クライアントサイドでのデータフェッチの限界</h3>
<p>SPA（Single Page Application）の普及以降、React開発の主流はクライアントサイドでのデータフェッチでした。しかし、このアプローチにはいくつかの構造的な問題が存在します。</p>
<h4 id="ネットワークウォーターフォール問題">ネットワークウォーターフォール問題</h4>
<p>コンポーネントのレンダリングが完了してから、<code>useEffect</code>内でデータフェッチを開始するため、親コンポーネントのデータ取得が終わらないと子コンポーネントのデータ取得が始まらない、といった連鎖的な遅延（ウォーターフォール）が発生しがちでした。これにより、ページの表示完了までに時間がかかっていました。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// 典型的なウォーターフォール
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">ProfilePage</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">user</span> } <span style="color:#f92672">=</span> <span style="color:#a6e22e">useUser</span>(); <span style="color:#75715e">// 1回目のAPIコール
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// userが取得できてから、次のコンポーネントがレンダリングされる
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">user</span> <span style="color:#f92672">?</span> &lt;<span style="color:#f92672">UserDetails</span> <span style="color:#a6e22e">userId</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">id</span>} /&gt; <span style="color:#f92672">:</span> &lt;<span style="color:#f92672">Spinner</span> /&gt;;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">UserDetails</span>({ <span style="color:#a6e22e">userId</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">posts</span> } <span style="color:#f92672">=</span> <span style="color:#a6e22e">usePosts</span>(<span style="color:#a6e22e">userId</span>); <span style="color:#75715e">// 2回目のAPIコール
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></td></tr></table>
</div>
</div><h4 id="バンドルサイズの肥大化">バンドルサイズの肥大化</h4>
<p>データフェッチのためのライブラリ（axios, SWRなど）、状態管理ロジック、データ整形ロジックなど、すべてがJavaScriptバンドルに含まれ、クライアントに送信されます。アプリケーションが複雑になるほどバンドルサイズは増大し、初期ロード時間（Initial Load Time）を悪化させる大きな要因となっていました。</p>
<h4 id="セキュリティリスク">セキュリティリスク</h4>
<p>APIキーやデータベースへの接続情報など、本来サーバーサイドに留めておくべき機密情報を、クライアントサイドのコードから（直接的でなくとも）扱わざるを得ないケースがありました。これにより、情報漏洩のリスクが高まります。</p>
<h3 id="課題2-ssrssgの限界">課題2: SSR/SSGの限界</h3>
<p>これらの課題を解決するためにSSR（Server-Side Rendering）やSSG（Static-Site Generation）が登場しましたが、これらにもまた別の限界がありました。</p>
<ul>
<li><strong>SSRの課題</strong>: サーバーでページ全体のHTMLを生成するため初期表示は高速ですが、JavaScriptがダウンロードされ、ハイドレーションが完了するまでページはインタラクティブになりません。また、データフェッチが完了するまでHTMLの生成がブロックされるため、一部のデータ取得が遅いとページ全体の表示が遅れてしまいます。</li>
<li><strong>SSGの課題</strong>: ビルド時にすべてのページを生成するため非常に高速ですが、動的なデータやユーザー固有のコンテンツを表示するには不向きでした。</li>
</ul>
<p>これらの課題を解決し、「サーバーのパワーを最大限に活用しつつ、クライアントの豊かなインタラクティビティも損なわない」という、両者の&quot;いいとこ取り&quot;を目指して生まれたのが、React Server Componentsなのです。</p>
<h2 id="rscの核心-server-componentとclient-componentの徹底解説">RSCの核心: Server ComponentとClient Componentの徹底解説</h2>
<p>RSCの最も重要な概念は、コンポーネントを「サーバーで実行されるもの」と「クライアントで実行されるもの」に明確に分離したことです。Next.jsのApp Routerでは、<strong>すべてのコンポーネントはデフォルトでServer Component</strong>として扱われます。</p>
<h3 id="server-component-デフォルト">Server Component (デフォルト)</h3>
<p>Server Componentは、その名の通りサーバーサイドでのみレンダリングされるコンポーネントです。</p>
<p><strong>特徴:</strong></p>
<ul>
<li><code>useState</code>, <code>useEffect</code>, <code>useContext</code>などのフックや、<code>onClick</code>などのイベントハンドラは使用できません。</li>
<li>ブラウザAPI（<code>window</code>, <code>localStorage</code>など）にアクセスできません。</li>
<li><code>async/await</code>を直接使用して、データフェッチや非同期処理を行えます。</li>
<li>データベースやファイルシステムなど、サーバーサイドのリソースに直接アクセスできます。</li>
<li>レンダリングされても、そのJavaScriptコードはクライアントのバンドルに含まれません。</li>
</ul>
<p>Server Componentの最大の利点は、データソースの近くでコンポーネントをレンダリングできることです。これにより、データフェッチのレイテンシーを最小限に抑え、必要なデータだけをコンポーネントに埋め込んでクライアントに送信できます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// app/posts/page.tsx (Server Component)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">db</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/lib/db&#39;</span>; <span style="color:#75715e">// サーバーサイドのDBクライアント
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getPosts</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// サーバー上で直接データベースにクエリを発行
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">posts</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">findMany</span>();
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">posts</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">PostsPage</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// コンポーネント自体を非同期関数にできる
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">posts</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">getPosts</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">main</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">h1</span>&gt;<span style="color:#a6e22e">All</span> <span style="color:#a6e22e">Posts</span>&lt;/<span style="color:#f92672">h1</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">ul</span>&gt;
</span></span><span style="display:flex;"><span>        {<span style="color:#a6e22e">posts</span>.<span style="color:#a6e22e">map</span>((<span style="color:#a6e22e">post</span>) =&gt; (
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">li</span> <span style="color:#a6e22e">key</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">id</span>}&gt;{<span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">title</span>}&lt;/<span style="color:#f92672">li</span>&gt;
</span></span><span style="display:flex;"><span>        ))}
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">ul</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">main</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>このコードでは、APIエンドポイントを介さずに直接データベースからデータを取得しています。また、このコンポーネントのロジックはクライアントに送られないため、バンドルサイズはゼロです。</p>
<h3 id="client-component">Client Component</h3>
<p>Client Componentは、従来のReactコンポーネントと同じように、クライアント（ブラウザ）でレンダリングされ、インタラクティブな動作を担当します。</p>
<p><strong>特徴:</strong></p>
<ul>
<li>ファイルの先頭に<code>&quot;use client&quot;;</code>というディレクティブを記述する必要があります。</li>
<li><code>useState</code>, <code>useEffect</code>などのフックを使用して、状態管理や副作用を扱えます。</li>
<li><code>onClick</code>, <code>onChange</code>などのイベントハンドラを定義できます。</li>
<li>ブラウザAPIにアクセスできます。</li>
</ul>
<p><code>&quot;use client&quot;</code>は、単なるマーカーではありません。これは、<strong>Server ComponentとClient Componentの境界線を定義する</strong>重要な役割を果たします。<code>&quot;use client&quot;</code>が書かれたファイルからインポートされるすべてのコンポーネントも、自動的にClient Componentとして扱われます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// components/Counter.tsx (Client Component)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#e6db74">&#34;use client&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useState</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;react&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Counter</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">count</span>, <span style="color:#a6e22e">setCount</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">useState</span>(<span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">onClick</span><span style="color:#f92672">=</span>{() =&gt; <span style="color:#a6e22e">setCount</span>(<span style="color:#a6e22e">count</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>)}&gt;
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">Count</span><span style="color:#f92672">:</span> {<span style="color:#a6e22e">count</span>}
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="使い分けの基本フロー">使い分けの基本フロー</h3>
<p>では、具体的にどう使い分ければ良いのでしょうか。以下のフローチャートを参考に判断するのがおすすめです。</p>
<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">graph TD
    A[コンポーネント開発開始] --&gt; B{インタラクティブな要素は必要？&lt;br&gt;(onClick, onChangeなど)};
    B -- Yes --&gt; C[Client Component&lt;br&gt;(&#34;use client&#34; を使用)];
    B -- No --&gt; D{状態管理やライフサイクルフックは必要？&lt;br&gt;(useState, useEffectなど)};
    D -- Yes --&gt; C;
    D -- No --&gt; E{ブラウザ専用APIは必要？&lt;br&gt;(window, localStorageなど)};
    E -- Yes --&gt; C;
    E -- No --&gt; F[Server Component&lt;br&gt;(デフォルトのまま)];
</code></pre><p><strong>基本原則は、「可能な限りServer Componentを使い、インタラクティブ性が必要な部分だけをClient Componentとして切り出す」ことです。</strong></p>
<h2 id="rsc時代のデータフェッチ戦略">RSC時代のデータフェッチ戦略</h2>
<p>RSCの導入により、データフェッチの考え方は大きく変わりました。クライアントでの<code>useEffect</code>に頼るのではなく、サーバーでデータを取得することが第一の選択肢となります。</p>
<h3 id="基本戦略-データフェッチはserver-componentで行う">基本戦略: データフェッチはServer Componentで行う</h3>
<p>Server Component内では、<code>async/await</code>を使って、まるでNode.jsのスクリプトを書くかのようにシンプルにデータをフェッチできます。</p>
<p>Next.jsは、標準の<code>fetch</code> APIを拡張しており、きめ細やかなキャッシュ戦略を可能にしています。</p>
<ul>
<li><strong>静的データフェッチ (SSG相当):</strong> デフォルトの動作。ビルド時にデータを取得し、キャッシュします。
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#75715e">// キャッシュを強制（デフォルト）
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">fetch</span>(<span style="color:#e6db74">&#39;https://...&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// または明示的に
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">fetch</span>(<span style="color:#e6db74">&#39;https://...&#39;</span>, { <span style="color:#a6e22e">cache</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;force-cache&#39;</span> });
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li><strong>再検証 (ISR相当):</strong> 指定した時間（秒）が経過すると、次にリクエストがあった際にバックグラウンドでデータを再取得します。
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#75715e">// 60秒ごとにデータを再検証
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">fetch</span>(<span style="color:#e6db74">&#39;https://...&#39;</span>, { <span style="color:#a6e22e">next</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">revalidate</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">60</span> } });
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li><strong>動的データフェッチ (SSR相当):</strong> リクエストごとに常に最新のデータを取得します。
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#75715e">// キャッシュを利用しない
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">fetch</span>(<span style="color:#e6db74">&#39;https://...&#39;</span>, { <span style="color:#a6e22e">cache</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;no-store&#39;</span> });
</span></span></code></pre></td></tr></table>
</div>
</div></li>
</ul>
<p>これにより、<code>getStaticProps</code>や<code>getServerSideProps</code>といったNext.jsの旧来のAPIは不要になり、コンポーネント内でデータ要件を完結させることができます。</p>
<h3 id="パターン1-server-componentでデータをフェッチしclient-componentにpropsで渡す">パターン1: Server Componentでデータをフェッチし、Client ComponentにPropsで渡す</h3>
<p>これは最も基本的で強力なパターンです。データの取得と表示ロジックはServer Componentに任せ、ユーザーインタラクションが必要な部分だけをClient Componentとして分離します。</p>
<p><strong>例: 記事一覧と、各記事の「いいね」ボタン</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// app/blog/page.tsx (Server Component)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">LikeButton</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/LikeButton&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">getPosts</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/lib/posts&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">BlogPage</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">posts</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">getPosts</span>(); <span style="color:#75715e">// サーバーで全記事データを取得
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">h1</span>&gt;<span style="color:#a6e22e">Blog</span>&lt;/<span style="color:#f92672">h1</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#a6e22e">posts</span>.<span style="color:#a6e22e">map</span>((<span style="color:#a6e22e">post</span>) =&gt; (
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">article</span> <span style="color:#a6e22e">key</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">id</span>}&gt;
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">h2</span>&gt;{<span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">title</span>}&lt;/<span style="color:#f92672">h2</span>&gt;
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">p</span>&gt;{<span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">content</span>}&lt;/<span style="color:#f92672">p</span>&gt;
</span></span><span style="display:flex;"><span>          {<span style="color:#75715e">/* インタラクティブな部分だけClient Componentに分離 */</span>}
</span></span><span style="display:flex;"><span>          {<span style="color:#75715e">/* 初期データはPropsとして渡す */</span>}
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">LikeButton</span> <span style="color:#a6e22e">postId</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">id</span>} <span style="color:#a6e22e">initialLikes</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">likes</span>} /&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">article</span>&gt;
</span></span><span style="display:flex;"><span>      ))}
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// components/LikeButton.tsx (Client Component)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#e6db74">&#34;use client&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useState</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;react&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">incrementLikes</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/app/actions&#39;</span>; <span style="color:#75715e">// Server Actionを利用
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">LikeButton</span>({ <span style="color:#a6e22e">postId</span>, <span style="color:#a6e22e">initialLikes</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">likes</span>, <span style="color:#a6e22e">setLikes</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">useState</span>(<span style="color:#a6e22e">initialLikes</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">isPending</span>, <span style="color:#a6e22e">startTransition</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">useTransition</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">handleClick</span> <span style="color:#f92672">=</span> () =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">startTransition</span>(<span style="color:#66d9ef">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// サーバー側の関数を直接呼び出してDBを更新
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">newLikes</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">incrementLikes</span>(<span style="color:#a6e22e">postId</span>);
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">setLikes</span>(<span style="color:#a6e22e">newLikes</span>);
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">onClick</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">handleClick</span>} <span style="color:#a6e22e">disabled</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">isPending</span>}&gt;
</span></span><span style="display:flex;"><span>      <span style="color:#960050;background-color:#1e0010">👍</span> {<span style="color:#a6e22e">likes</span>} {<span style="color:#a6e22e">isPending</span> <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;...&#39;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;&#39;</span>}
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>この構成により、以下のメリットが生まれます。</p>
<ul>
<li>記事一覧のデータ取得はサーバーで完結し、クライアントのバンドルサイズを圧迫しない。</li>
<li>インタラクティブな<code>LikeButton</code>のみがClient Componentとなり、JavaScriptのロード量を最小限に抑える。</li>
<li><code>initialLikes</code>をPropsで渡すことで、ハイドレーション後も初期状態が正しく表示される。</li>
</ul>
<h3 id="パターン2-server-componentをclient-componentに埋め込む-children-props">パターン2: Server ComponentをClient Componentに埋め込む (<code>children</code> props)</h3>
<p>Client Componentの中に、静的なコンテンツとしてServer Componentを配置したい場合があります。これは<code>children</code> propsを活用することで実現できます。これにより、Client Componentの島（Island）の中に、サーバーでレンダリングされたコンテンツの穴（Hole）を作ることができます。</p>
<p><strong>例: タブ切り替えUI（Client）と、各タブのコンテンツ（Server）</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// components/TabContainer.tsx (Client Component)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#e6db74">&#34;use client&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useState</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;react&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">TabContainer</span>({ <span style="color:#a6e22e">tabs</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">activeTab</span>, <span style="color:#a6e22e">setActiveTab</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">useState</span>(<span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">role</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;tablist&#34;</span>&gt;
</span></span><span style="display:flex;"><span>        {<span style="color:#a6e22e">tabs</span>.<span style="color:#a6e22e">map</span>((<span style="color:#a6e22e">tab</span>, <span style="color:#a6e22e">index</span>) =&gt; (
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">button</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">key</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">tab</span>.<span style="color:#a6e22e">label</span>}
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">role</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;tab&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">onClick</span><span style="color:#f92672">=</span>{() =&gt; <span style="color:#a6e22e">setActiveTab</span>(<span style="color:#a6e22e">index</span>)}
</span></span><span style="display:flex;"><span>          &gt;
</span></span><span style="display:flex;"><span>            {<span style="color:#a6e22e">tab</span>.<span style="color:#a6e22e">label</span>}
</span></span><span style="display:flex;"><span>          &lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>        ))}
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">role</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;tabpanel&#34;</span>&gt;
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* childrenとして渡されたServer Componentがここに表示される */</span>}
</span></span><span style="display:flex;"><span>        {<span style="color:#a6e22e">tabs</span>[<span style="color:#a6e22e">activeTab</span>].<span style="color:#a6e22e">content</span>}
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// app/dashboard/page.tsx (Server Component)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">TabContainer</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/TabContainer&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">UserProfile</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/UserProfile&#39;</span>; <span style="color:#75715e">// Server Component
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">AnalyticsData</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/AnalyticsData&#39;</span>; <span style="color:#75715e">// Server Component
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// これらのコンポーネントは重いデータフェッチを行うと仮定
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">UserProfile</span>() { <span style="color:#75715e">/* ... */</span> }
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">AnalyticsData</span>() { <span style="color:#75715e">/* ... */</span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">DashboardPage</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">tabs</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">label</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Profile&#39;</span>, <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> &lt;<span style="color:#f92672">UserProfile</span> /&gt; },
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">label</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Analytics&#39;</span>, <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> &lt;<span style="color:#f92672">AnalyticsData</span> /&gt; }
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">main</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">h1</span>&gt;<span style="color:#a6e22e">Dashboard</span>&lt;/<span style="color:#f92672">h1</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* Client ComponentにServer ComponentをPropsとして渡す */</span>}
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">TabContainer</span> <span style="color:#a6e22e">tabs</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">tabs</span>} /&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">main</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>このパターンでは、タブの切り替えという状態管理はクライアントで行いつつ、各タブの中身はサーバーで完全にレンダリングされます。これにより、初期表示時に不要なタブのコンテンツまでクライアントに送られることがなくなり、パフォーマンスが向上します。</p>
<h3 id="パターン3-クライアントサイドでのデータフェッチが依然として有効な場合">パターン3: クライアントサイドでのデータフェッチが依然として有効な場合</h3>
<p>RSCが強力だからといって、クライアントサイドでのデータフェッチが完全になくなるわけではありません。SWRやTanStack Query (旧React Query) は、特定のユースケースにおいて依然として非常に有効です。</p>
<p><strong>主なユースケース:</strong></p>
<ul>
<li><strong>ユーザーの操作に頻繁に反応するデータ:</strong> 検索ボックスのサジェスト機能、無限スクロール、頻繁に更新されるダッシュボードなど。</li>
<li><strong>クライアントの状態に依存するデータ:</strong> 認証状態（ログインしているユーザーの情報など）に基づいて取得するデータ。</li>
<li><strong>複雑なキャッシュ戦略やミューテーション管理:</strong> オプティミスティックUIアップデートなど、高度なUI/UXを実現したい場合。</li>
</ul>
<p>RSCは<strong>初期ロード</strong>のパフォーマンス最適化に絶大な効果を発揮しますが、ロード<strong>後</strong>のクライアント上でのリッチなデータインタラクションは、これらのライブラリが得意とする領域です。RSCとクライアントデータフェッチライブラリは、対立するものではなく、<strong>共存し補完し合う関係</strong>と捉えるのが正解です。</p>
<h2 id="現場で使える実践的なtips">現場で使える実践的なTips</h2>
<h3 id="tip-1-use-clientはできるだけ末端leafのコンポーネントに設定する">Tip 1: <code>&quot;use client&quot;</code>はできるだけ末端（Leaf）のコンポーネントに設定する</h3>
<p><code>&quot;use client&quot;</code>は境界線であり、一度宣言されるとその配下のすべてのコンポーネントがClient Componentになります。そのため、できるだけコンポーネントツリーの末端、つまり本当にインタラクティブ性が必要な最小単位のコンポーネントに設定することが重要です。</p>
<p><strong>悪い例:</strong> ページ全体をClient Componentにしてしまう</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// app/dashboard/layout.tsx
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#e6db74">&#34;use client&#34;</span>; <span style="color:#75715e">// ← アンチパターン！
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">Header</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/Header&#39;</span>; <span style="color:#75715e">// Client Componentになる
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">Sidebar</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/Sidebar&#39;</span>; <span style="color:#75715e">// Client Componentになる
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">MainContent</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/MainContent&#39;</span>; <span style="color:#75715e">// Client Componentになる
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">DashboardLayout</span>({ <span style="color:#a6e22e">children</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>良い例:</strong> インタラクティブな部分だけを分離する</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// app/dashboard/layout.tsx (Server Component)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">UserMenu</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/UserMenu&#39;</span>; <span style="color:#75715e">// Client Component
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">Sidebar</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/Sidebar&#39;</span>; <span style="color:#75715e">// Server Component
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">MainContent</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/MainContent&#39;</span>; <span style="color:#75715e">// Server Component
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">DashboardLayout</span>({ <span style="color:#a6e22e">children</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">header</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">nav</span>&gt;...&lt;/<span style="color:#f92672">nav</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">UserMenu</span> /&gt; {<span style="color:#75715e">/* インタラクティブな部分だけがClient Component */</span>}
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">header</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">Sidebar</span> /&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">MainContent</span>&gt;{<span style="color:#a6e22e">children</span>}&lt;/<span style="color:#f92672">MainContent</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="tip-2-server-actionsでデータ更新をシンプルにする">Tip 2: Server Actionsでデータ更新をシンプルにする</h3>
<p>フォーム送信やデータ更新（Mutation）のために、もはやAPIエンドポイントを自前で用意する必要はありません。Server Actionsを使えば、サーバーサイドで実行される関数をクライアントコンポーネントから直接、安全に呼び出せます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// app/actions.ts
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#e6db74">&#34;use server&#34;</span>; <span style="color:#75715e">// ファイルの先頭に記述
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">db</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/lib/db&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">revalidatePath</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;next/cache&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">addPost</span>(<span style="color:#a6e22e">formData</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">FormData</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">title</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">formData</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;title&#39;</span>) <span style="color:#a6e22e">as</span> <span style="color:#a6e22e">string</span>;
</span></span><span style="display:flex;"><span>  
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">create</span>({ <span style="color:#a6e22e">data</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">title</span> } });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// 関連するページのキャッシュをクリアして再描画をトリガー
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">revalidatePath</span>(<span style="color:#e6db74">&#39;/posts&#39;</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// components/AddPostForm.tsx (Client Component)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#e6db74">&#34;use client&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">addPost</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/app/actions&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">AddPostForm</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">form</span> <span style="color:#a6e22e">action</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">addPost</span>}&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">input</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text&#34;</span> <span style="color:#a6e22e">name</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;title&#34;</span> <span style="color:#a6e22e">required</span> /&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;submit&#34;</span>&gt;<span style="color:#a6e22e">Add</span> <span style="color:#a6e22e">Post</span>&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">form</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>form</code>タグの<code>action</code>属性に関数を渡すだけで、フォーム送信時にサーバー上の<code>addPost</code>関数が実行されます。APIルートの作成、<code>fetch</code>の呼び出し、ローディング状態の管理といった定型的なコードが大幅に削減され、開発体験が飛躍的に向上します。</p>
<h3 id="tip-3-suspenseとstreamingで体感速度を向上させる">Tip 3: SuspenseとStreamingで体感速度を向上させる</h3>
<p>ページの特定の部分でデータ取得に時間がかかる場合でも、<code>Suspense</code>を使うことで他の部分の表示をブロックすることなく、UIを段階的に表示（Streaming）できます。</p>
<p>Next.jsでは、<code>loading.tsx</code>という規約ファイルを使うことで、これを簡単に実現できます。</p>
<p><strong>例: 記事詳細ページとコメント欄</strong>
記事本体はすぐに表示したいが、コメントの読み込みには時間がかかるとします。</p>
<pre tabindex="0"><code>app/
└ posts/
  └ [slug]/
    ├ page.tsx      // 記事本体を表示するServer Component
    └ loading.tsx   // page.tsxのレンダリング中に表示されるUI
</code></pre><div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// app/posts/[slug]/page.tsx
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">PostContent</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/PostContent&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">Comments</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/Comments&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Suspense</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;react&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">PostPage</span>({ <span style="color:#a6e22e">params</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* 記事本体はすぐにレンダリングされる */</span>}
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">PostContent</span> <span style="color:#a6e22e">slug</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">params</span>.<span style="color:#a6e22e">slug</span>} /&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* コメントはデータ取得が終わるまでフォールバックUIを表示 */</span>}
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">Suspense</span> <span style="color:#a6e22e">fallback</span><span style="color:#f92672">=</span>{&lt;<span style="color:#f92672">p</span>&gt;<span style="color:#a6e22e">Loading</span> <span style="color:#a6e22e">comments</span>...&lt;/<span style="color:#f92672">p</span>&gt;}&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">Comments</span> <span style="color:#a6e22e">slug</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">params</span>.<span style="color:#a6e22e">slug</span>} /&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">Suspense</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// components/Comments.tsx (Server Component)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getComments</span>(<span style="color:#a6e22e">slug</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#66d9ef">new</span> Promise(<span style="color:#a6e22e">resolve</span> =&gt; <span style="color:#a6e22e">setTimeout</span>(<span style="color:#a6e22e">resolve</span>, <span style="color:#ae81ff">2000</span>)); <span style="color:#75715e">// 意図的に遅延
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Comments</span>({ <span style="color:#a6e22e">slug</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">comments</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">getComments</span>(<span style="color:#a6e22e">slug</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>この構成により、ユーザーはまず記事本体を読み始めることができ、その間にコメントが非同期でロードされます。これにより、実際の読み込み時間が同じでも、体感速度は劇的に改善されます。</p>
<h2 id="まとめ">まとめ</h2>
<p>React Server Componentsは、単なる新機能ではなく、Reactアプリケーションの設計思想そのものを変える、大きなパラダイムシフトです。</p>
<p>本記事の要点をまとめます。</p>
<ol>
<li><strong>デフォルトはServer Component:</strong> すべてのコンポーネントはサーバーで実行されるものと考え、インタラクティブ性が必要な最小単位だけを<code>&quot;use client&quot;</code>でClient Componentに切り出す。</li>
<li><strong>データフェッチの主戦場はサーバーへ:</strong> <code>async/await</code>をServer Componentで直接使い、データソースの近くでデータを取得する。<code>useEffect</code>でのデータフェッチは、クライアントでのインタラクションに応じた動的な取得に限定される。</li>
<li><strong><code>&quot;use client&quot;</code>は境界線:</strong> このディレクティブは、サーバーとクライアントの世界を分ける重要な役割を果たす。ツリーのなるべく末端に配置することを心がける。</li>
<li><strong>パターンを使い分ける:</strong> 「Propsでデータを渡す」「<code>children</code>でコンポーネントを埋め込む」といった基本的なパターンをマスターし、コンポーネントの責務を明確に分離する。</li>
<li><strong>エコシステムを賢く利用する:</strong> Server Actionsでミューテーションを簡略化し、SuspenseとStreamingでUXを向上させる。SWRやTanStack Queryも、依然としてクライアントサイドでの複雑なデータ管理に有効。</li>
</ol>
<p>最初は戸惑うことも多いかもしれません。しかし、このサーバーとクライアントの新しい協調モデルを理解し、使いこなすことができれば、これまで以上に高速で、スケーラブルで、そして開発者体験の良いアプリケーションを構築できることは間違いありません。</p>
<p>さあ、今日からあなたのReact開発に、Server Componentsの力を取り入れてみませんか？</p>
]]></content:encoded>
      <category>Frontend</category>
      <category>React</category>
      <category>RSC</category>
      <category>Next.js</category>
    </item>
    <item>
      <title>Vercel AI SDKの新機能：マルチモーダル対応の強化</title>
      <link>https://www.ai2core.com/posts/2026-02-20-vercel-ai-sdk/</link>
      <pubDate>Fri, 20 Feb 2026 12:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-20-vercel-ai-sdk/</guid>
      <description>Vercel AI SDKのアップデートで追加された画像・音声入力のハンドリング手法。</description>
      <content:encoded><![CDATA[<h1 id="vercel-ai-sdkの新機能マルチモーダル対応の強化">Vercel AI SDKの新機能：マルチモーダル対応の強化</h1>
<h2 id="はじめに">はじめに</h2>
<p>テキストベースのチャットボットや文章生成AIが当たり前になった今、開発者の関心は次のステージ、すなわち「マルチモーダルAI」へと急速にシフトしています。ユーザーがアップロードした画像を認識して説明を生成したり、音声で指示を受け付けたりするアプリケーションは、もはやSFの世界の話ではありません。</p>
<p>しかし、このようなリッチな体験を実現しようとすると、開発者は多くの課題に直面します。</p>
<ul>
<li>「画像データをどうやってフロントエンドからサーバーに送ればいいんだろう？」</li>
<li>「OpenAI、Google、Anthropic… 各社で微妙に違うマルチモーダルAPIの仕様を吸収するのが大変だ」</li>
<li>「Base64エンコード？ファイルアップロード？ストリーミングUIとどう組み合わせるの？」</li>
</ul>
<p>これらの悩みは、AIアプリケーションのアイデアを形にする上で大きな障壁となっていました。</p>
<p>そんな中、フロントエンド開発者にとっての福音とも言えるVercel AI SDKが、待望のマルチモーダル対応を大幅に強化しました。このアップデートにより、これまで複雑だった画像や音声データのハンドリングが驚くほどシンプルになり、開発者は本来注力すべきアプリケーションのコアロジックとユーザー体験の向上に集中できるようになったのです。</p>
<p>この記事では、プロの技術ブロガーとして、Vercel AI SDKの最新機能を徹底的に掘り下げます。具体的なコード例を交えながら、画像や音声といった非テキストデータを活用した次世代AIアプリケーションを、いかに効率的に、そして高速に構築できるかをご紹介します。この記事を読み終える頃には、あなたもマルチモーダルAIアプリ開発の最前線に立つ準備が整っているはずです。</p>
<h2 id="なぜ今マルチモーダルaiとvercel-ai-sdkなのか">なぜ今、マルチモーダルAIとVercel AI SDKなのか？</h2>
<p>この新機能の詳細に入る前に、なぜこのトピックがこれほどまでに重要なのか、その背景と課題を整理しておきましょう。</p>
<h3 id="マルチモーダルaiの台頭とユーザー体験の革新">マルチモーダルAIの台頭とユーザー体験の革新</h3>
<p>GPT-4oやClaude 3、Geminiといった最新のLLM（大規模言語モデル）は、テキストだけでなく画像、音声、さらには動画までも理解・生成する能力（マルチモーダル能力）を獲得しています。これにより、可能になるアプリケーションの幅は飛躍的に広がりました。</p>
<ul>
<li><strong>ビジュアルQ&amp;A</strong>: ホワイトボードの写真を撮って「この図をコードに書き起こして」と指示する。</li>
<li><strong>デザイン支援</strong>: Webサイトのスクリーンショットをアップロードして「このデザインの改善点を指摘して」と依頼する。</li>
<li><strong>音声アシスタント</strong>: 音声で「今日の天気と、それに合った服装を提案して」と話しかける。</li>
</ul>
<p>これらはほんの一例ですが、人間が普段行うような、複数の感覚情報を組み合わせたコミュニケーションをAIと行えるようになることで、ユーザー体験はより直感的で豊かなものになります。このトレンドに適応できないアプリケーションは、近い将来、時代遅れと見なされてしまうかもしれません。</p>
<h3 id="従来の実装における複雑さという壁">従来の実装における複雑さという「壁」</h3>
<p>アイデアはあっても、それを実現する道のりは平坦ではありませんでした。マルチモーダルAIアプリを自前で実装しようとすると、以下のような複雑な処理に直面します。</p>
<ol>
<li>
<p><strong>フロントエンドでのデータハンドリング</strong>:</p>
<ul>
<li>ユーザーが選択した画像や音声ファイルをJavaScriptで読み込み、適切な形式（多くはBase64文字列やバイナリデータ）に変換する必要があります。</li>
<li>大きなファイルの場合、UIがフリーズしないよう非同期処理を適切に管理しなければなりません。</li>
</ul>
</li>
<li>
<p><strong>クライアント・サーバー間のデータ転送</strong>:</p>
<ul>
<li>変換したデータをHTTPリクエストのボディに含めてサーバーに送信します。JSONペイロードにBase64文字列を埋め込むのが一般的ですが、データサイズが大きくなりがちで、リクエストサイズの制限に抵触する可能性もあります。</li>
</ul>
</li>
<li>
<p><strong>バックエンドでのAPI差異の吸収</strong>:</p>
<ul>
<li>サーバーサイドでは、受け取ったデータを各AIベンダーのAPI仕様に合わせて再度整形する必要があります。例えば、OpenAIのAPIとAnthropicのAPIでは、画像データの渡し方が異なります。</li>
<li>新しいモデルが登場したり、API仕様が変更されたりするたびに、コードの修正を迫られます。</li>
</ul>
</li>
<li>
<p><strong>ストリーミングとの組み合わせ</strong>:</p>
<ul>
<li>AIの応答をリアルタイムでユーザーに表示するストリーミングは、現代のAIチャットUIには不可欠です。しかし、これにマルチモーダルな入力を組み合わせると、状態管理やエラーハンドリングはさらに複雑になります。</li>
</ul>
</li>
</ol>
<p>これらの課題は、開発者から多くの時間とエネルギーを奪い、イノベーションの足かせとなっていました。</p>
<h3 id="vercel-ai-sdkがもたらす解決策">Vercel AI SDKがもたらす解決策</h3>
<p>Vercel AI SDKは、まさにこの「壁」を打ち破るために生まれました。ReactやNext.jsといったモダンなWebフレームワークと深く統合し、AIアプリケーション開発における定型的な処理を美しく抽象化してくれます。</p>
<p>今回のマルチモーダル対応強化により、SDKは以下の価値を提供します。</p>
<ul>
<li><strong>統一されたAPI</strong>: どのLLMを使うかに関わらず、同じような記述で画像やテキストを送信できます。</li>
<li><strong>シンプルなデータ形式</strong>: フロントエンドでファイルを読み込んだら、あとはSDKの関数に渡すだけ。面倒なエンコードやリクエストの組み立てはSDKが裏側で処理してくれます。</li>
<li><strong>Generative UIとのシームレスな統合</strong>: <code>streamUI</code> や <code>generateUI</code> といった革新的な機能と組み合わせることで、「画像を入力として、動的なUIコンポーネントを生成・ストリーミングする」といった高度なアプリケーションも驚くほど簡単に実装できます。</li>
</ul>
<p>次章から、この強力なSDKを使って、実際にマルチモーダルアプリケーションを構築していく手順を詳しく見ていきましょう。</p>
<h2 id="具体的な実装解説画像と音声でaiと対話する">具体的な実装解説：画像と音声でAIと対話する</h2>
<p>ここからは、Vercel AI SDKを使ってマルチモーダルアプリケーションを構築する具体的な方法を、コードを交えてステップバイステップで解説します。今回はNext.js App Routerを使った実装を例に進めます。</p>
<h3 id="1-環境構築">1. 環境構築</h3>
<p>まずは、Next.jsプロジェクトをセットアップし、必要なパッケージをインストールします。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Next.jsプロジェクトの作成</span>
</span></span><span style="display:flex;"><span>npx create-next-app@latest vercel-ai-multimodal-app
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># プロジェクトディレクトリに移動</span>
</span></span><span style="display:flex;"><span>cd vercel-ai-multimodal-app
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Vercel AI SDKと関連パッケージのインストール</span>
</span></span><span style="display:flex;"><span>npm install ai zod-to-json-schema
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># UIのためにshadcn/uiなどを入れておくと便利です</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># (本記事では省略しますが、実際の開発では推奨します)</span>
</span></span><span style="display:flex;"><span>npx shadcn-ui@latest init
</span></span><span style="display:flex;"><span>npx shadcn-ui@latest add input button
</span></span></code></pre></td></tr></table>
</div>
</div><p>次に、プロジェクトのルートに <code>.env.local</code> ファイルを作成し、使用するAIモデルのAPIキーを設定します。今回はOpenAIのGPT-4oを例にします。</p>
<pre tabindex="0"><code class="language-.env.local" data-lang=".env.local">OPENAI_API_KEY=&#34;sk-...&#34;
</code></pre><h3 id="2-基本的な画像入力チャットの実装">2. 基本的な画像入力チャットの実装</h3>
<p>最も基本的なユースケースとして、「ユーザーが画像をアップロードし、その画像に関する質問をテキストで入力できるチャットUI」を実装します。</p>
<h4 id="データの流れる仕組み">データの流れる仕組み</h4>
<p>実装に入る前に、全体のデータの流れを把握しておきましょう。</p>
<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">sequenceDiagram
    participant Client as ブラウザ (Reactコンポーネント)
    participant Server as Next.jsサーバー (Server Action)
    participant OpenAI as OpenAI API (GPT-4o)

    Client-&gt;&gt;Client: ユーザーが画像とテキストを入力
    Client-&gt;&gt;Client: 画像をBase64文字列に変換
    Client-&gt;&gt;Server: Server Actionを呼び出し (テキスト + Base64画像)
    Server-&gt;&gt;OpenAI: Vercel AI SDKを使いAPIリクエストを整形・送信
    OpenAI--&gt;&gt;Server: テキスト応答をストリーミング
    Server--&gt;&gt;Client: 応答をクライアントにストリーミング
    Client-&gt;&gt;Client: ストリーミングされたテキストをリアルタイムで表示
</code></pre><p>Vercel AI SDKは、特にサーバーとOpenAI API間の通信を劇的に簡素化してくれます。</p>
<h4 id="サーバーサイドserver-actionの作成">サーバーサイド：Server Actionの作成</h4>
<p>まず、AIとの通信を担うサーバーサイドのロジックを <code>app/actions.ts</code> に実装します。React Server Components（RSC）とServer Actionsの組み合わせは、Vercel AI SDKと非常に相性が良いです。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">44
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">45
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">46
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">47
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">48
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">49
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">50
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">51
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">52
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">53
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript:app/actions.ts" data-lang="typescript:app/actions.ts"><span style="display:flex;"><span><span style="color:#e6db74">&#39;use server&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">streamUI</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;ai/rsc&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">openai</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;@ai-sdk/openai&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">ReactNode</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;react&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">z</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;zod&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// APIキーを環境変数から取得
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// Next.jsでは、`process.env`で自動的に.env.localを読み込みます
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// const openaiApiKey = process.env.OPENAI_API_KEY;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">continueConversation</span>(
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">history</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">role</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;user&#39;</span> <span style="color:#f92672">|</span> <span style="color:#e6db74">&#39;assistant&#39;</span>; <span style="color:#a6e22e">content</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">data?</span>: <span style="color:#66d9ef">any</span> }[],
</span></span><span style="display:flex;"><span>)<span style="color:#f92672">:</span> <span style="color:#a6e22e">Promise</span><span style="color:#f92672">&lt;</span>{
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">messages</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">role</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;user&#39;</span> <span style="color:#f92672">|</span> <span style="color:#e6db74">&#39;assistant&#39;</span>; <span style="color:#a6e22e">content</span>: <span style="color:#66d9ef">string</span>; <span style="color:#a6e22e">data?</span>: <span style="color:#66d9ef">any</span> }[];
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">ui</span>: <span style="color:#66d9ef">ReactNode</span>;
</span></span><span style="display:flex;"><span>}<span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">messages</span>, <span style="color:#a6e22e">ui</span> } <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">streamUI</span>({
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">model</span>: <span style="color:#66d9ef">openai</span>(<span style="color:#e6db74">&#39;gpt-4o&#39;</span>), <span style="color:#75715e">// 最新のマルチモーダルモデルを指定
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// system: &#39;あなたは優秀なアシスタントです。&#39;, // 必要に応じてシステムプロンプトを設定
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">messages</span>: <span style="color:#66d9ef">history.map</span>((<span style="color:#a6e22e">message</span>) <span style="color:#f92672">=&gt;</span> ({
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">role</span>: <span style="color:#66d9ef">message.role</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">content</span>: <span style="color:#66d9ef">message.content</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#75715e">// dataプロパティに画像データが含まれている場合、それをcontentに含める
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>      ...(<span style="color:#a6e22e">message</span>.<span style="color:#a6e22e">data</span> <span style="color:#f92672">&amp;&amp;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>          { <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;text&#39;</span>, <span style="color:#a6e22e">text</span>: <span style="color:#66d9ef">message.content</span> },
</span></span><span style="display:flex;"><span>          ...<span style="color:#a6e22e">message</span>.<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">images</span>.<span style="color:#a6e22e">map</span>((<span style="color:#a6e22e">img</span>: <span style="color:#66d9ef">string</span>) <span style="color:#f92672">=&gt;</span> ({
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">type</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;image&#39;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">image</span>: <span style="color:#66d9ef">Buffer.from</span>(<span style="color:#a6e22e">img</span>.<span style="color:#a6e22e">split</span>(<span style="color:#e6db74">&#39;,&#39;</span>)[<span style="color:#ae81ff">1</span>], <span style="color:#e6db74">&#39;base64&#39;</span>),
</span></span><span style="display:flex;"><span>          })),
</span></span><span style="display:flex;"><span>        ],
</span></span><span style="display:flex;"><span>      }),
</span></span><span style="display:flex;"><span>    })),
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">text</span><span style="color:#f92672">:</span> ({ <span style="color:#a6e22e">content</span>, <span style="color:#a6e22e">done</span> }) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">done</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// AIの応答が完了したら、そのメッセージを履歴に追加
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#a6e22e">history</span>.<span style="color:#a6e22e">push</span>({ <span style="color:#a6e22e">role</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;assistant&#39;</span>, <span style="color:#a6e22e">content</span> });
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> &lt;<span style="color:#f92672">div</span>&gt;{<span style="color:#a6e22e">content</span>}&lt;/<span style="color:#f92672">div</span>&gt;;
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// 将来的にUIコンポーネントを生成する場合のスキーマ
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// zodスキーマを定義しておくことで、構造化されたUIを生成可能
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// schema: z.object({
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">//   ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// })
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">messages</span>: <span style="color:#66d9ef">history</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ui</span>,
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>ポイント解説:</strong></p>
<ul>
<li><strong><code>'use server'</code></strong>: このファイルがサーバーサイドでのみ実行されることを示します。</li>
<li><strong><code>streamUI</code></strong>: Vercel AI SDKのコア機能の一つ。テキストだけでなく、Reactコンポーネント（UI）そのものをストリーミングできます。</li>
<li><strong><code>openai('gpt-4o')</code></strong>: 使用するモデルを簡単に指定できます。<code>@ai-sdk/anthropic</code> や <code>@ai-sdk/google</code> を使えば、ClaudeやGeminiも同様に扱えます。</li>
<li><strong><code>messages</code>の整形</strong>: <code>history</code> 配列をループし、AI SDKが要求する形式に変換しています。</li>
<li><strong>マルチモーダル入力</strong>: ユーザーメッセージに <code>data.images</code>（Base64文字列の配列）が含まれている場合、<code>content</code> をテキストと画像のオブジェクト配列に変換しています。<code>{ type: 'image', image: ... }</code> という形式が重要です。SDKはこの形式を解釈し、モデルに適切に送信してくれます。</li>
<li><strong>Base64からBufferへ</strong>: <code>image</code> プロパティには、Base64文字列からデコードしたBufferを渡しています。これにより、SDKが効率的にデータを処理できます。</li>
</ul>
<h4 id="クライアントサイドチャットuiの実装">クライアントサイド：チャットUIの実装</h4>
<p>次に、ユーザーが操作するフロントエンド部分を <code>app/page.tsx</code> に実装します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">  1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">  2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">  3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">  4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">  5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">  6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">  7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">  8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">  9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 36
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 37
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 39
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 40
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 41
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 42
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 43
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 44
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 45
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 46
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 47
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 48
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 49
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 50
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 51
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 52
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 53
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 54
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 55
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 56
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 57
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 58
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 59
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 60
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 61
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 62
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 63
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 64
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 65
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 66
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 67
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 68
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 69
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 70
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 71
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 72
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 73
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 74
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 75
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 76
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 77
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 78
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 79
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 80
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 81
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 82
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 83
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 84
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 85
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 86
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 87
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 88
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 89
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 90
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 91
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 92
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 93
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 94
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 95
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 96
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 97
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 98
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 99
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">100
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">101
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">102
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">103
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">104
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">105
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">106
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">107
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">108
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">109
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">110
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">111
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">112
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">113
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">114
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">115
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">116
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">117
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">118
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">119
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">120
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">121
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">122
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">123
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tsx:app/page.tsx" data-lang="tsx:app/page.tsx"><span style="display:flex;"><span><span style="color:#e6db74">&#39;use client&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useState</span>, <span style="color:#a6e22e">useRef</span>, <span style="color:#a6e22e">ChangeEvent</span>, <span style="color:#a6e22e">FormEvent</span>, <span style="color:#a6e22e">ReactNode</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;react&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useActions</span>, <span style="color:#a6e22e">useUIState</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;ai/rsc&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">continueConversation</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;./actions&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">Image</span> <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;next/image&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// メッセージの型定義
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Message</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">role</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;user&#39;</span> <span style="color:#f92672">|</span> <span style="color:#e6db74">&#39;assistant&#39;</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">content</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// 画像データを保持するためのオプショナルなプロパティ
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">data</span><span style="color:#f92672">?:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">images?</span>: <span style="color:#66d9ef">string</span>[];
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">Home() {</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">inputValue</span>, <span style="color:#a6e22e">setInputValue</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">useState</span>(<span style="color:#e6db74">&#39;&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">messages</span>, <span style="color:#a6e22e">setMessages</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">useUIState</span>&lt;<span style="color:#f92672">typeof</span> <span style="color:#a6e22e">continueConversation</span>&gt;([]);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">continueConversation</span>: <span style="color:#66d9ef">performAction</span> } <span style="color:#f92672">=</span> <span style="color:#a6e22e">useActions</span>&lt;<span style="color:#f92672">typeof</span> <span style="color:#a6e22e">continueConversation</span>&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> [<span style="color:#a6e22e">imageFiles</span>, <span style="color:#a6e22e">setImageFiles</span>] <span style="color:#f92672">=</span> <span style="color:#a6e22e">useState</span>&lt;<span style="color:#f92672">string</span><span style="color:#960050;background-color:#1e0010">[]</span>&gt;([]);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">fileInputRef</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">useRef</span>&lt;<span style="color:#f92672">HTMLInputElement</span>&gt;(<span style="color:#66d9ef">null</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// ファイルが選択されたときのハンドラ
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">handleFileChange</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">e</span>: <span style="color:#66d9ef">ChangeEvent</span>&lt;<span style="color:#f92672">HTMLInputElement</span>&gt;) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">files</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">target</span>.<span style="color:#a6e22e">files</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">files</span>) <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">newImageFiles</span>: <span style="color:#66d9ef">string</span>[] <span style="color:#f92672">=</span> [];
</span></span><span style="display:flex;"><span>    Array.<span style="color:#66d9ef">from</span>(<span style="color:#a6e22e">files</span>).<span style="color:#a6e22e">forEach</span>((<span style="color:#a6e22e">file</span>) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">reader</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">FileReader</span>();
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">reader</span>.<span style="color:#a6e22e">onloadend</span> <span style="color:#f92672">=</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">typeof</span> <span style="color:#a6e22e">reader</span>.<span style="color:#a6e22e">result</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;string&#39;</span>) {
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">newImageFiles</span>.<span style="color:#a6e22e">push</span>(<span style="color:#a6e22e">reader</span>.<span style="color:#a6e22e">result</span>);
</span></span><span style="display:flex;"><span>          <span style="color:#75715e">// すべてのファイルの読み込みが終わったらstateを更新
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>          <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">newImageFiles</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">===</span> <span style="color:#a6e22e">files</span>.<span style="color:#a6e22e">length</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">setImageFiles</span>((<span style="color:#a6e22e">prev</span>) <span style="color:#f92672">=&gt;</span> [...<span style="color:#a6e22e">prev</span>, ...<span style="color:#a6e22e">newImageFiles</span>]);
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">reader</span>.<span style="color:#a6e22e">readAsDataURL</span>(<span style="color:#a6e22e">file</span>); <span style="color:#75715e">// ファイルをBase64文字列として読み込む
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    });
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// フォーム送信時のハンドラ
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">handleSubmit</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">async</span> (<span style="color:#a6e22e">e</span>: <span style="color:#66d9ef">FormEvent</span>&lt;<span style="color:#f92672">HTMLFormElement</span>&gt;) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">preventDefault</span>();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">inputValue</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">imageFiles</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">0</span>) <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// 現在の入力値と画像でユーザーメッセージを作成
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">userMessage</span>: <span style="color:#66d9ef">Message</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">role</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;user&#39;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">content</span>: <span style="color:#66d9ef">inputValue</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">data</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">images</span>: <span style="color:#66d9ef">imageFiles</span>,
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// UIStateを更新し、アクションを実行
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">setMessages</span>((<span style="color:#a6e22e">currentMessages</span>) <span style="color:#f92672">=&gt;</span> [...<span style="color:#a6e22e">currentMessages</span>, <span style="color:#a6e22e">userMessage</span>]);
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">response</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">performAction</span>([
</span></span><span style="display:flex;"><span>        ...<span style="color:#a6e22e">messages</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">userMessage</span>,
</span></span><span style="display:flex;"><span>      ]);
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">setMessages</span>(<span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">messages</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// 送信後に入力と画像をクリア
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">setInputValue</span>(<span style="color:#e6db74">&#39;&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">setImageFiles</span>([]);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">fileInputRef</span>.<span style="color:#a6e22e">current</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">fileInputRef</span>.<span style="color:#a6e22e">current</span>.<span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">className</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;flex flex-col w-full max-w-2xl mx-auto py-8&#34;</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* メッセージ表示エリア */</span>}
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">className</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;flex-grow mb-4 space-y-4&#34;</span>&gt;
</span></span><span style="display:flex;"><span>        {<span style="color:#a6e22e">messages</span>.<span style="color:#a6e22e">map</span>((<span style="color:#a6e22e">msg</span>, <span style="color:#a6e22e">index</span>) <span style="color:#f92672">=&gt;</span> (
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">key</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">index</span>} <span style="color:#a6e22e">className</span><span style="color:#f92672">=</span>{<span style="color:#e6db74">`flex </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">role</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;user&#39;</span> <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;justify-end&#39;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;justify-start&#39;</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>}&gt;
</span></span><span style="display:flex;"><span>            &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">className</span><span style="color:#f92672">=</span>{<span style="color:#e6db74">`p-3 rounded-lg </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">role</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;user&#39;</span> <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;bg-blue-500 text-white&#39;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;bg-gray-200&#39;</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>}&gt;
</span></span><span style="display:flex;"><span>              {<span style="color:#75715e">/* ユーザーメッセージの画像プレビュー */</span>}
</span></span><span style="display:flex;"><span>              {<span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">data</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">images</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">map</span>((<span style="color:#a6e22e">img</span>, <span style="color:#a6e22e">i</span>) <span style="color:#f92672">=&gt;</span> (
</span></span><span style="display:flex;"><span>                  &lt;<span style="color:#f92672">Image</span> <span style="color:#a6e22e">key</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">i</span>} <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">img</span>} <span style="color:#a6e22e">alt</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;uploaded image&#34;</span> <span style="color:#a6e22e">width</span><span style="color:#f92672">=</span>{<span style="color:#ae81ff">200</span>} <span style="color:#a6e22e">height</span><span style="color:#f92672">=</span>{<span style="color:#ae81ff">200</span>} <span style="color:#a6e22e">className</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;rounded-md mb-2&#34;</span> /&gt;
</span></span><span style="display:flex;"><span>              ))}
</span></span><span style="display:flex;"><span>              {<span style="color:#75715e">/* テキストコンテンツ */</span>}
</span></span><span style="display:flex;"><span>              &lt;<span style="color:#f92672">p</span>&gt;{<span style="color:#66d9ef">typeof</span> <span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">content</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;string&#39;</span> <span style="color:#f92672">?</span> <span style="color:#a6e22e">msg</span>.<span style="color:#a6e22e">content</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;UI Component&#34;</span>}&lt;/<span style="color:#f92672">p</span>&gt;
</span></span><span style="display:flex;"><span>            &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>          &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>        ))}
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* 入力フォーム */</span>}
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">form</span> <span style="color:#a6e22e">onSubmit</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">handleSubmit</span>} <span style="color:#a6e22e">className</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;flex flex-col gap-2&#34;</span>&gt;
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* 画像プレビュー */</span>}
</span></span><span style="display:flex;"><span>        {<span style="color:#a6e22e">imageFiles</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">&amp;&amp;</span> (
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">className</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;flex gap-2 p-2 border rounded-md&#34;</span>&gt;
</span></span><span style="display:flex;"><span>            {<span style="color:#a6e22e">imageFiles</span>.<span style="color:#a6e22e">map</span>((<span style="color:#a6e22e">src</span>, <span style="color:#a6e22e">index</span>) <span style="color:#f92672">=&gt;</span> (
</span></span><span style="display:flex;"><span>              &lt;<span style="color:#f92672">Image</span> <span style="color:#a6e22e">key</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">index</span>} <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">src</span>} <span style="color:#a6e22e">alt</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;preview&#34;</span> <span style="color:#a6e22e">width</span><span style="color:#f92672">=</span>{<span style="color:#ae81ff">80</span>} <span style="color:#a6e22e">height</span><span style="color:#f92672">=</span>{<span style="color:#ae81ff">80</span>} <span style="color:#a6e22e">className</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;rounded-md object-cover&#34;</span> /&gt;
</span></span><span style="display:flex;"><span>            ))}
</span></span><span style="display:flex;"><span>          &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>        )}
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">className</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#39;flex gap-2&#39;</span>&gt;
</span></span><span style="display:flex;"><span>            &lt;<span style="color:#f92672">input</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;file&#34;</span> <span style="color:#a6e22e">accept</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;image/*&#34;</span> <span style="color:#a6e22e">multiple</span> <span style="color:#a6e22e">onChange</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">handleFileChange</span>} <span style="color:#a6e22e">ref</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">fileInputRef</span>} <span style="color:#a6e22e">className</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;flex-1 p-2 border rounded-md&#34;</span>/&gt;
</span></span><span style="display:flex;"><span>            &lt;<span style="color:#f92672">input</span>
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text&#34;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">value</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">inputValue</span>}
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">onChange</span><span style="color:#f92672">=</span>{(<span style="color:#a6e22e">e</span>) <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">setInputValue</span>(<span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">target</span>.<span style="color:#a6e22e">value</span>)}
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">placeholder</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;画像について質問...&#34;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">className</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;flex-[3] p-2 border rounded-md&#34;</span>
</span></span><span style="display:flex;"><span>            /&gt;
</span></span><span style="display:flex;"><span>            &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;submit&#34;</span> <span style="color:#a6e22e">className</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;px-4 py-2 bg-blue-600 text-white rounded-md&#34;</span>&gt;
</span></span><span style="display:flex;"><span>                <span style="color:#960050;background-color:#1e0010">送信</span>
</span></span><span style="display:flex;"><span>            &lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">form</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>ポイント解説:</strong></p>
<ul>
<li><strong><code>'use client'</code></strong>: このコンポーネントがクライアントサイドで動作することを示します。</li>
<li><strong><code>useUIState</code> と <code>useActions</code></strong>: Vercel AI SDKが提供するフックです。<code>useUIState</code> はUIの状態（この場合はメッセージ履歴）を管理し、<code>useActions</code> はServer Actionをクライアントサイドから安全に呼び出すためのものです。</li>
<li><strong>ファイルハンドリング</strong>:
<ol>
<li><code>&lt;input type=&quot;file&quot;&gt;</code> でユーザーに画像を選択させます。<code>multiple</code> 属性で複数選択を許可しています。</li>
<li><code>handleFileChange</code> 内で <code>FileReader</code> APIを使用し、選択されたファイルを <strong>Base64形式のデータURL</strong> に変換しています。これが、サーバーに送信する画像の形式になります。</li>
<li><code>useState</code> (<code>imageFiles</code>)でBase64文字列を保持し、プレビュー表示に利用します。</li>
</ol>
</li>
<li><strong>データ送信</strong>:
<ol>
<li><code>handleSubmit</code> で、テキスト入力 (<code>inputValue</code>) と画像 (<code>imageFiles</code>) をまとめて <code>userMessage</code> オブジェクトを作成します。</li>
<li><code>performAction</code>（実体は <code>continueConversation</code>）を呼び出す際に、この <code>userMessage</code> を含んだメッセージ履歴を渡します。</li>
<li>サーバーサイド（<code>actions.ts</code>）では、この <code>userMessage.data.images</code> を見て、マルチモーダル入力として処理します。</li>
</ol>
</li>
</ul>
<p>これで、画像とテキストを同時に送信できる基本的なチャットアプリケーションが完成しました。驚くほどボイラープレートコードが少ないことにお気づきでしょうか。SDKが面倒な部分をすべて引き受けてくれているのです。</p>
<h3 id="3-応用編音声入力のハンドリング">3. 応用編：音声入力のハンドリング</h3>
<p>Vercel AI SDKは直接的な音声処理機能は持ちませんが、他のライブラリと組み合わせることで、音声入力アプリケーションも簡単に構築できます。ここでは「ユーザーが録音した音声を文字起こしし、そのテキストをLLMに送信する」という流れを考えます。</p>
<h4 id="データの流れる仕組み-1">データの流れる仕組み</h4>
<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">sequenceDiagram
    participant Client as ブラウザ (Reactコンポーネント)
    participant Server as Next.jsサーバー (API Route)
    participant OpenAI_STT as OpenAI Whisper API
    participant OpenAI_LLM as OpenAI LLM API (GPT-4o)

    Client-&gt;&gt;Client: ユーザーがマイクで録音
    Client-&gt;&gt;Server: 音声ファイル (Blob) をAPI Routeに送信
    Server-&gt;&gt;OpenAI_STT: Whisper APIに音声データを転送し文字起こしを依頼
    OpenAI_STT--&gt;&gt;Server: 文字起こしされたテキストを返す
    Server-&gt;&gt;OpenAI_LLM: Vercel AI SDKを使い、テキストをLLMに送信
    OpenAI_LLM--&gt;&gt;Server: テキスト応答をストリーミング
    Server--&gt;&gt;Client: 応答をクライアントにストリーミング
    Client-&gt;&gt;Client: 応答をリアルタイムで表示
</code></pre><p>このシナリオでは、音声データを一旦サーバーに送り、Whisper APIなどで文字起こしするステップが加わります。</p>
<h4 id="実装のポイント">実装のポイント</h4>
<ol>
<li>
<p><strong>クライアントサイド</strong>:</p>
<ul>
<li><code>react-media-recorder</code> や <code>react-use-microphone</code> といったライブラリを使って、ブラウザのマイクから音声を録音し、Blobオブジェクトとして取得します。</li>
<li>取得したBlobを <code>FormData</code> に詰めて、Next.jsのAPI Routeに <code>fetch</code> でPOSTします。</li>
</ul>
</li>
<li>
<p><strong>サーバーサイド（API Route）</strong>:</p>
<ul>
<li>API Route (例: <code>app/api/transcribe/route.ts</code>) でリクエストを受け取ります。</li>
<li><code>formidable</code> のようなライブラリを使って、マルチパートフォームデータをパースし、音声ファイルを取得します。</li>
<li>取得した音声ファイルをOpenAIのWhisper APIに送信して文字起こしをします。これには <code>openai</code> ライブラリの <code>audio.transcriptions.create</code> メソッドが使えます。</li>
<li><strong>文字起こししたテキストを、前述の <code>continueConversation</code> のようなServer Actionに渡すか、あるいはこのAPI Route内で直接Vercel AI SDKの <code>streamText</code> などを使ってLLMに送信します。</strong></li>
</ul>
</li>
</ol>
<p>このように、Vercel AI SDKはエコシステムの他のツールと柔軟に組み合わせることができ、音声という異なるモダリティもシームレスにAIアプリケーションのワークフローに組み込むことが可能です。</p>
<h2 id="メリットとデメリットあるいは注意点">メリットとデメリット（あるいは注意点）</h2>
<p>Vercel AI SDKのマルチモーダル対応は非常に強力ですが、万能ではありません。そのメリットと、利用する上で注意すべき点を整理します。</p>
<h3 id="メリット">メリット</h3>
<ul>
<li><strong>圧倒的な開発速度</strong>: 面倒なAPI仕様の差異吸収、データエンコード、ストリーミング制御などをSDKが担うため、開発者は数行のコードでマルチモーダル機能を実装できます。</li>
<li><strong>コードの可読性と保守性</strong>: 宣言的なAPI設計により、何をしているかが分かりやすいコードになります。モデルを <code>openai('gpt-4o')</code> から <code>anthropic('claude-3-opus-20240229')</code> に変更するのも一行で済みます。</li>
<li><strong>Generative UIとの相性抜群</strong>: 画像を入力として、その説明文だけでなく、関連商品を表示するカードコンポーネントや、分析結果を可視化するグラフコンポーネントなどを動的に生成・ストリーミングできます。これは他のライブラリにはない大きな強みです。</li>
<li><strong>Vercelプラットフォームとの親和性</strong>: Vercelへのデプロイがスムーズなのはもちろん、Serverless Functionsの実行時間制限やペイロードサイズの制限などを考慮した設計になっています。</li>
</ul>
<h3 id="デメリット注意点">デメリット・注意点</h3>
<ul>
<li><strong>抽象化のトレードオフ</strong>: SDKは便利な反面、内部実装を隠蔽します。非常にニッチなAPIパラメータを使いたい場合や、特殊な認証フローを実装したい場合には、SDKの提供するインターフェースでは対応できない可能性があります。</li>
<li><strong>クライアントサイドのパフォーマンス</strong>: 特に画像をBase64にエンコードする処理は、画像のサイズが大きいとクライアントのCPUとメモリを消費します。ユーザー体験を損なわないよう、後述するTipsで紹介するような前処理が重要になります。</li>
<li><strong>コスト管理の徹底</strong>: マルチモーダルAPIは、テキストのみのAPIに比べて高価になる傾向があります。特に高解像度の画像を頻繁に送信すると、API利用料が想定外に膨れ上がる可能性があります。入力画像の解像度や枚数に制限を設けるなどの対策が不可欠です。</li>
</ul>
<h2 id="現場で使える実践的なtips">現場で使える実践的なTips</h2>
<p>最後に、開発現場でマルチモーダルAIアプリを運用していく上で役立つ、より実践的なテクニックをいくつかご紹介します。</p>
<h3 id="1-クライアントサイドでの画像圧縮">1. クライアントサイドでの画像圧縮</h3>
<p>ユーザーがアップロードする画像は、スマートフォンで撮影した高解像度の写真など、数MBに及ぶことがよくあります。これをそのままBase64にエンコードして送信すると、以下の問題が発生します。</p>
<ul>
<li>アップロードに時間がかかり、ユーザー体験が悪い。</li>
<li>APIリクエストのペイロードが大きくなりすぎる。</li>
<li>LLMのAPIコストが高くなる（多くのモデルは画像解像度に応じてコストが変動する）。</li>
</ul>
<p>そこで、<code>browser-image-compression</code>のようなライブラリを使い、画像をサーバーに送信する前にクライアントサイドで圧縮・リサイズするのが非常に効果的です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install browser-image-compression
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">imageCompression</span> <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#39;browser-image-compression&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">handleFileChange</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">async</span> (<span style="color:#a6e22e">e</span>: <span style="color:#66d9ef">ChangeEvent</span>&lt;<span style="color:#f92672">HTMLInputElement</span>&gt;) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">file</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">target</span>.<span style="color:#a6e22e">files</span><span style="color:#f92672">?</span>.[<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">file</span>) <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">`Original file size: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">file</span>.<span style="color:#a6e22e">size</span> <span style="color:#f92672">/</span> <span style="color:#ae81ff">1024</span> <span style="color:#f92672">/</span> <span style="color:#ae81ff">1024</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> MB`</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">options</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">maxSizeMB</span>: <span style="color:#66d9ef">1</span>, <span style="color:#75715e">// 最大ファイルサイズ
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">maxWidthOrHeight</span>: <span style="color:#66d9ef">1024</span>, <span style="color:#75715e">// 最大幅・高さ
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">useWebWorker</span>: <span style="color:#66d9ef">true</span>, <span style="color:#75715e">// Web Workerを使ってUIスレッドをブロックしない
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">compressedFile</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">imageCompression</span>(<span style="color:#a6e22e">file</span>, <span style="color:#a6e22e">options</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">`Compressed file size: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">compressedFile</span>.<span style="color:#a6e22e">size</span> <span style="color:#f92672">/</span> <span style="color:#ae81ff">1024</span> <span style="color:#f92672">/</span> <span style="color:#ae81ff">1024</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> MB`</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// この圧縮されたファイルをFileReaderでBase64に変換して送信する
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  } <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">error</span>(<span style="color:#a6e22e">error</span>);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></td></tr></table>
</div>
</div><p>この一手間を加えるだけで、アプリケーションのパフォーマンスと運用コストを劇的に改善できます。</p>
<h3 id="2-堅牢なuiuxの構築">2. 堅牢なUI/UXの構築</h3>
<p>AIの応答には時間がかかるため、ユーザーが「今、何が起きているのか」を理解できるようなUI/UXが不可欠です。</p>
<ul>
<li><strong>ローディング状態の明示</strong>: APIにリクエストを送信してから応答が返ってくるまでの間、ボタンを無効化したり、スピナーやスケルトンスクリーンを表示したりしましょう。</li>
<li><strong>画像の即時プレビュー</strong>: <code>URL.createObjectURL()</code> を使えば、ファイルを読み込みながらすぐにプレビューを表示できます。ユーザーは自分が正しい画像をアップロードしたことを即座に確認できます。</li>
<li><strong>エラーハンドリングとフィードバック</strong>: ファイル形式が不正な場合、ファイルサイズが大きすぎる場合、API通信に失敗した場合など、想定されるエラーを適切にキャッチし、「〜のため処理に失敗しました。もう一度お試しください」といった具体的なメッセージをユーザーに提示しましょう。</li>
</ul>
<h3 id="3-コストとセキュリティの考慮">3. コストとセキュリティの考慮</h3>
<ul>
<li><strong>入力制限</strong>: ユーザーが一度にアップロードできる画像の枚数や、合計ファイルサイズに上限を設けましょう。これにより、意図しない高額なAPI利用を防ぎます。</li>
<li><strong>認証とレートリミット</strong>: 誰でも無制限にAPIを呼び出せる状態は非常に危険です。NextAuth.jsなどでユーザー認証を導入し、ユーザーごとにAPI呼び出し回数の制限（レートリミット）を設けることを強く推奨します。</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>本記事では、Vercel AI SDKの強力な新機能であるマルチモーダル対応について、その重要性から具体的な実装方法、そして実践的なTipsまでを網羅的に解説しました。</p>
<p>Vercel AI SDKは、これまで専門家でなければ困難だったマルチモーダルAIアプリケーションの開発を、フロントエンド開発者にとって身近なものへと変えてくれました。煩雑なデータハンドリングやAPIの差異を抽象化することで、私たちはより創造的な作業、すなわち「AIを使ってどのような新しいユーザー体験を創造するか」に集中することができます。</p>
<p>画像認識チャット、デザインレビューツール、ビジュアル検索エンジンなど、あなたのアイデアを形にするためのツールは、かつてないほどシンプルで強力なものになりました。もちろん、コスト管理やパフォーマンスチューニングといった実践的な課題は残りますが、それらを乗り越えた先には、ユーザーをあっと驚かせるような革新的なアプリケーションが待っています。</p>
<p>テキストの時代から、画像、音声、そしてUIが融合するマルチモーダルの時代へ。Vercel AI SDKは、その大きな潮流に乗るための、最も確かな羅針盤となるでしょう。さあ、あなたもこの新しいツールを手に、次世代のAIアプリケーション開発に挑戦してみてはいかがでしょうか。</p>
]]></content:encoded>
      <category>Web Dev</category>
      <category>Vercel</category>
      <category>AI SDK</category>
      <category>Next.js</category>
    </item>
  </channel>
</rss>
