第三章:タイプライター効果の実装:AI Streaming APIのフロントエンド処理を深く解説
ビジネス向けのAI搭載SaaSを開発したい場合、「一文字ずつゆっくり表示される」タイプライター効果(Streaming UI)は必須機能です。ユーザーが10秒間真っ白な画面を待たされた後にまとめて回答を見せられても、サイトが重いかサーバーがダウンしていると感じるだけでしょう。
本章では、OpenAIやAnthropic APIから返されるStreamingデータストリームをフロントエンドで受信しレンダリングする方法を手順を追って解説します。
ReadableStreamとは?
従来のAPIリクエストでは、await fetch(...)でレスポンスを取得後、await res.json()でデータ全体をJavaScriptのObjectに変換するのが一般的でした。
しかしStreamingモードではこの方法は使えません。サーバーはJSONデータを一括で送信せず、少量のデータ(Chunk)を少しずつ送信します。
現代のブラウザのJavaScriptでは、このようなデータを処理するインターフェースを**ReadableStream**と呼びます。
実践:フロントエンドタイプライターフックの作成
ロジックを整理するため、複雑なデコード処理をUIコンポーネント内に記述すべきではありません。useAiChatというCustom Hookを作成しましょう。
このHookの役割は:ユーザーのメッセージをバックエンドAPIに送信し、続々と送られてくるテキストストリームを受信して文字列を連結することです。
```typescript import { useState } from "react"
export function useAiChat() { const [currentMessage, setCurrentMessage] = useState("") const [isLoading, setIsLoading] = useState(false)
const sendMessage = async (userText: string) => { setIsLoading(true) setCurrentMessage("") // 現在のメッセージをクリアして新規応答を受信準備
try {
// 自前で作成したNext.js Route Handler(バックエンド)を呼び出し
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: userText })
})
if (!response.ok) throw new Error("APIリクエスト失敗")
if (!response.body) throw new Error("無効なデータストリーム")
// ReadableStreamのリーダーを取得
const reader = response.body.getReader()
// 送信されてきたバイト列を人間が読めるテキスト(UTF-8)にデコード
const decoder = new TextDecoder("utf-8")
let done = false
// コアループ:終了するまで新しいテキストチャンクを読み続ける
while (!done) {
const { value, done: readerDone } = await reader.read()
done = readerDone
if (value) {
// stream: trueを指定するとデコーダーはバイト列が不完全でもエラーにせず、次のデータ到着を待つ
const chunkString = decoder.decode(value, { stream: true })
// 新しく到着したテキストを現在の文字列末尾に連結
setCurrentMessage((prev) => prev + chunkString)
}
}
} catch (error) {
console.error("Streaming処理でエラー発生:", error)
setCurrentMessage("申し訳ありません、接続エラーが発生しました。")
} finally {
setIsLoading(false)
}
}
return { currentMessage, sendMessage, isLoading } } ```
この一見単純なwhileループは、実は非常に深遠なフロントエンド問題を解決しています。これによりウェブページは水道管のように文字を途切れなく受信できます。
UIでのHook使用方法
上記の強力なHookが複雑なバイト列デコード処理を肩代わりしてくれるため、UIコンポーネント内のコードは非常に簡潔になります:
```tsx "use client" import { useState } from "react" import { useAiChat } from "./useAiChat"
export default function AiChatBox() { const [input, setInput] = useState("") const { currentMessage, sendMessage, isLoading } = useAiChat()
const handleSubmit = () => { if (!input.trim()) return sendMessage(input) setInput("") // 入力欄をクリア }
return (
{/* AI応答表示エリア */}
<div className="min-h-[150px] mb-4 p-4 bg-gray-50 rounded-lg text-gray-800 whitespace-pre-wrap leading-relaxed shadow-inner">
{currentMessage || (isLoading ? "考え中..." : "下の入力欄から質問を開始してください。")}
{/* ちょっとした演出:点滅するカーソルでタイプライター感を向上 */}
{isLoading && <span className="inline-block w-2 h-4 ml-1 bg-blue-500 animate-pulse" />}
</div>
{/* 入力エリア */}
<div className="flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
className="flex-1 p-3 border rounded-xl focus:outline-blue-500 focus:ring-2 focus:ring-blue-200 transition-all"
placeholder="何でも聞いてください..."
/>
<button
onClick={handleSubmit}
disabled={isLoading}
className="px-6 py-3 bg-blue-600 text-white font-bold rounded-xl hover:bg-blue-700 disabled:opacity-50 transition-all"
>
送信
</button>
</div>
</div>
) } ```
whitespace-pre-wrapを使用する理由
AI応答を表示する<div>にTailwindのwhitespace-pre-wrapクラスを追加しています。これは非常に見落とされがちなUIの重要なディテールです!
AIが返すテキストには改行コード(\n)が含まれることがよくあります。HTMLのデフォル���動作では、プレーンテキスト内の\nは単なる空白として扱われ、レイアウトが崩れてしまいます。
whitespace-pre-wrapを追加すると、ブラウザは「文字列内の改行コードを尊重する」ようになり、AIが生成した段落構成やコードのインデントを完璧に再現できます。
AIエコシステムの超便利ツール:Vercel AI SDK
ReadableStreamやwhileループを自前で実装するのが面倒だと感じる場合、業界には強力な解決策があります。
Next.jsの開発元であるVercelが提供するオープンソースライブラリ:Vercel AI SDK(npm i ai)です。
これは前述のuseAiChatを極限まで簡略化したuseChat Hookを提供しており、Streaming処理だけでなく、質問履歴の管理、回答の再生成、生成停止機能まで全て組み込まれています。実際の商業プロジェクトでは、この強力なSDKを活用して開発を加速させるのが一般的です。
ただし、低レベルのReadableStreamの仕組みを理解しておけば、様々なバグに遭遇した際に他にはないデバッグ能力を発揮できるでしょう!
完全なチュートリアルをロック解除
このチャプターは有料コンテンツです。プロジェクトに参加して、10以上の神レベルのPromptや実際のソースコード例を含む、5000字以上の深い分析をロック解除してください!