チャット、通知、ダッシュボード、コラボレーション編集——リアルタイム通信が必要なWebアプリは増えていますが、WebSocket/SSEの選定、接続管理、再接続戦略、水平スケーリングの設計は複雑です。Claude Codeはこの複雑さを吸収してくれます。通信プロトコルの選定から、型安全なSocket.IOイベント設計、Redis Adapterによるスケーリング設定、React Hookによる接続状態管理まで、一貫した文脈で生成できます。
この記事では、WebSocketとSSEの使い分けから、Socket.IO + TypeScript、Hono SSE、Next.js SSE Route Handlers、接続管理パターンまで、Claude Codeを使ったリアルタイムアプリ開発の実践的なワークフローを解説します。
WebSocket vs SSE ── どちらを選ぶか
| 観点 | WebSocket | SSE(Server-Sent Events) |
|---|---|---|
| 通信方向 | 双方向(サーバー↔クライアント) | 片方向(サーバー→クライアント) |
| プロトコル | ws://(独自プロトコル) | 通常のHTTP |
| データ形式 | テキスト + バイナリ | UTF-8テキストのみ |
| 自動再接続 | なし(自前実装が必要) | EventSourceに組み込み済み |
| HTTP/2対応 | 別コネクション | マルチプレクス対応(接続数制限なし) |
| 適用例 | チャット、ゲーム、コラボ編集 | 通知、ダッシュボード、LLMストリーミング |
選定の目安:クライアントからサーバーへの頻繁な送信が必要ならWebSocket、サーバーからのpushが主体なら SSE。ダッシュボード更新・通知・AIストリーミングなどの95%のユースケースはSSEで十分です。
Socket.IO + TypeScript で型安全なリアルタイム通信
Socket.IO v4はTypeScriptの型定義を完全サポートしており、サーバー・クライアント間のイベントを型安全に設計できます。
Socket.IO セットアッププロンプト
Socket.IO v4 + TypeScript + Redis Adapterでチャットサーバーを実装してください。 要件: - 4つの型インターフェース(ServerToClient/ClientToServer/InterServer/SocketData) - JWT認証middleware - Redis Adapterで水平スケーリング対応 - 名前空間: /chat(チャット), /notifications(通知) - ルーム: チャットルームID単位のグループ配信 - 接続数上限: ユーザーあたり5接続まで
型定義(shared/types.ts)
// サーバー→クライアントに送るイベント
interface ServerToClientEvents {
message: (data: { id: string; text: string; userId: string; createdAt: string }) => void;
userJoined: (data: { userId: string; roomId: string }) => void;
userLeft: (data: { userId: string; roomId: string }) => void;
}
// クライアント→サーバーに送るイベント
interface ClientToServerEvents {
sendMessage: (data: { text: string; roomId: string }) => void;
joinRoom: (roomId: string, callback: (success: boolean) => void) => void;
leaveRoom: (roomId: string) => void;
}
// サーバー間通信イベント(Redis Adapter経由)
interface InterServerEvents {
ping: () => void;
}
// ソケットに紐づくデータ
interface SocketData {
userId: string;
role: "admin" | "user";
}
サーバー実装(lib/socket/server.ts)
import { Server } from "socket.io";
import { createAdapter } from "@socket.io/redis-adapter";
import { createClient } from "redis";
import jwt from "jsonwebtoken";
import type { ServerToClientEvents, ClientToServerEvents, InterServerEvents, SocketData } from "@/shared/types";
export async function createSocketServer(httpServer: any) {
const io = new Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>(httpServer, {
cors: { origin: process.env.CLIENT_URL, credentials: true },
});
// Redis Adapter(水平スケーリング)
const pub = createClient({ url: process.env.REDIS_URL });
const sub = pub.duplicate();
await Promise.all([pub.connect(), sub.connect()]);
io.adapter(createAdapter(pub, sub));
// JWT認証middleware
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) return next(new Error("認証が必要です"));
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; role: string };
socket.data.userId = decoded.sub;
socket.data.role = decoded.role as "admin" | "user";
next();
} catch {
next(new Error("無効なトークンです"));
}
});
// チャット名前空間
const chat = io.of("/chat");
chat.on("connection", (socket) => {
socket.on("joinRoom", (roomId, callback) => {
socket.join(roomId);
socket.to(roomId).emit("userJoined", { userId: socket.data.userId, roomId });
callback(true);
});
socket.on("sendMessage", async ({ text, roomId }) => {
const message = {
id: crypto.randomUUID(),
text,
userId: socket.data.userId,
createdAt: new Date().toISOString(),
};
// DBに永続化(省略)
chat.to(roomId).emit("message", message);
});
socket.on("disconnect", () => {
// クリーンアップ処理
});
});
return io;
}
Redis Adapterを設定すると、複数のSocket.IOサーバー間でメッセージが自動同期されます。ユーザーAがサーバー1に接続し、ユーザーBがサーバー2に接続していても、同じルーム内でメッセージが配信されます。ただしSocket.IOはデフォルトでHTTP long-pollingフォールバックを使うため、スティッキーセッションは依然必要です。WebSocketトランスポートのみに制限すればスティッキーセッションは不要になります。
Hono SSE ── 軽量なサーバーpush実装
SSEが適切なユースケース(通知、ダッシュボード更新、AIストリーミング等)では、HonoのstreamSSEで簡潔に実装できます。
Hono SSEストリーミング
import { Hono } from "hono";
import { streamSSE } from "hono/streaming";
const app = new Hono();
// SSEエンドポイント
app.get("/api/events", async (c) => {
return streamSSE(c, async (stream) => {
let id = 0;
while (true) {
const data = { time: new Date().toISOString(), count: id };
await stream.writeSSE({
data: JSON.stringify(data),
event: "update",
id: String(id++),
});
await stream.sleep(1000); // 1秒間隔
}
});
});
Hono v4.12.4以降を使ってください。それ以前のバージョンにはSSEのCR/LFインジェクション脆弱性(CVE-2026-29085)があります。
Next.js Route HandlersでSSEを実装する
既存のNext.jsプロジェクトにリアルタイム通知を追加する場合、Route HandlersでSSEを実装するのが最も手軽です。
app/api/notifications/route.ts
import { NextRequest } from "next/server";
export const dynamic = "force-dynamic"; // 静的化を防止
export async function GET(req: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
// ハートビート(30秒間隔で接続維持)
const heartbeat = setInterval(() => {
controller.enqueue(encoder.encode(": heartbeat\n\n"));
}, 30000);
// 通知データの送信(例: 1秒間隔)
const send = setInterval(() => {
const data = { type: "notification", message: "新しいメッセージ", time: Date.now() };
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
}, 1000);
// クライアント切断時のクリーンアップ
req.signal.addEventListener("abort", () => {
clearInterval(heartbeat);
clearInterval(send);
controller.close();
});
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"X-Accel-Buffering": "no", // Nginx/Vercelでのバッファリング防止
},
});
}
export const dynamic = "force-dynamic"がないとNext.jsがレスポンスを静的化しようとします。またX-Accel-Buffering: noがないとNginxやVercelのリバースプロキシがチャンクをまとめて送信してしまい、リアルタイム性が失われます。React Hookで接続状態を管理する
接続管理Hook生成プロンプト
WebSocket接続を管理するReact Hookを作成してください。 要件: - 型パラメータで受信メッセージの型を指定可能 - 接続状態(connecting / connected / disconnected)を管理 - 指数バックオフによる自動再接続(base 500ms, max 30s, jitter付き) - コンポーネントアンマウント時に自動切断 - send関数で型安全にメッセージ送信
hooks/useWebSocket.ts(Claude Codeが生成)
import { useEffect, useRef, useState, useCallback } from "react";
type Status = "connecting" | "connected" | "disconnected";
export function useWebSocket<T>(url: string) {
const [status, setStatus] = useState<Status>("connecting");
const [lastMessage, setLastMessage] = useState<T | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const attemptRef = useRef(0);
const connect = useCallback(() => {
const ws = new WebSocket(url);
wsRef.current = ws;
setStatus("connecting");
ws.onopen = () => {
setStatus("connected");
attemptRef.current = 0;
};
ws.onmessage = (e) => {
setLastMessage(JSON.parse(e.data) as T);
};
ws.onclose = () => {
setStatus("disconnected");
// 指数バックオフ + jitter
const delay = Math.min(500 * 2 ** attemptRef.current, 30000);
const jitter = Math.random() * 1000;
attemptRef.current++;
setTimeout(connect, delay + jitter);
};
}, [url]);
useEffect(() => {
connect();
return () => wsRef.current?.close();
}, [connect]);
const send = useCallback((data: unknown) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(data));
}
}, []);
return { status, lastMessage, send };
}
hooks/useSSE.ts(SSE版)
import { useEffect, useRef } from "react";
export function useSSE<T>(url: string, onMessage: (data: T) => void) {
const callbackRef = useRef(onMessage);
callbackRef.current = onMessage;
useEffect(() => {
const es = new EventSource(url);
es.onmessage = (e) => {
callbackRef.current(JSON.parse(e.data) as T);
};
es.onerror = () => {
// EventSourceは自動再接続するため、ここではログのみ
console.warn("SSE connection error, reconnecting...");
};
return () => es.close();
}, [url]);
}
SSEの
EventSourceは接続切断時に自動で再接続します(デフォルトで数秒後)。WebSocketのように自前で再接続ロジックを書く必要がないのがSSEの大きなメリットです。CLAUDE.mdテンプレート(リアルタイムアプリ用)
CLAUDE.md(リアルタイム通信ルール)
## リアルタイム通信
### プロトコル選定
- 双方向通信(チャット等): Socket.IO v4
- サーバーpush(通知・ダッシュボード): SSE(Hono streamSSE or Next.js Route Handlers)
- 素のWebSocket APIは禁止(Socket.IOを使用すること)
### Socket.IO規約
- 4つの型インターフェースを必ず定義(ServerToClient/ClientToServer/InterServer/SocketData)
- 認証: socket.handshake.auth.token でJWT検証(query stringは禁止)
- 名前空間: 機能単位(/chat, /notifications)
- ルーム: 動的グループ単位(ルームID、テナントID)
- @socket.io/redis-adapterで水平スケーリング対応
### SSE規約
- Next.js: export const dynamic = "force-dynamic" を必ず指定
- ハートビート: 30秒間隔で ": heartbeat\n\n" を送信
- ヘッダー: X-Accel-Buffering: no を必ず設定(Nginx/Vercel対策)
- クライアント切断時のクリーンアップ: req.signal.addEventListener("abort", ...)
### 接続管理
- 指数バックオフ再接続(base 500ms, max 30s, jitter付き)
- React Hook(useWebSocket / useSSE)で接続状態を管理
- コンポーネントアンマウント時にWebSocket/EventSourceをcloseする
よくある質問
QWebSocketとSSEのどちらを使うべきですか?
Aクライアントからサーバーへの頻繁な送信が必要な場合(チャット、ゲーム、コラボ編集)はWebSocket、サーバーからのpushが主体の場合(通知、ダッシュボード更新、LLMストリーミング)はSSEを選んでください。SSEはHTTPの上で動くためプロキシ・ファイアウォールとの相性が良く、EventSourceの自動再接続もあるため実装が簡潔です。迷ったらSSEから始めてください。
QSocket.IOの水平スケーリングはどうすればよいですか?
A
@socket.io/redis-adapterを導入するだけです。複数のSocket.IOサーバー間でRedis Pub/Subを経由してメッセージが自動同期されるため、スティッキーセッションが不要になります。CLAUDE.mdに「Redis Adapterで水平スケーリング対応」と書いておけば、Claude Codeが最初からRedis Adapterを含んだコードを生成します。QNext.jsのSSE Route HandlerがVercelで動きません
AVercelのEdge Runtimeはストリーミングレスポンスをサポートしていますが、
export const dynamic = "force-dynamic"とX-Accel-Buffering: noヘッダーが必須です。また、Vercelのサーバーレス関数には実行時間の制限(Hobby: 10秒、Pro: 60秒)があるため、長時間のSSE接続にはVercel以外のホスティング(Railway、Fly.io等)を検討してください。QClaude Codeにリアルタイム機能のテストを書かせるには?
ASocket.IOのテストは
socket.io-clientでサーバーに接続してイベントの送受信を検証します。SSEのテストはsupertestでRoute Handlerを呼び、レスポンスストリームを読み取ります。Claude Codeに「Socket.IOのjoinRoom→sendMessage→message受信のE2Eテストを書いて」と依頼すると、接続・認証・イベント検証まで含んだテストコードを生成できます。QWebTransportは使うべきですか?
AWebTransportはHTTP/3(QUIC)ベースで双方向ストリーム+信頼性なしのデータグラムを提供します。2026年4月時点ではChromium系とFirefoxが対応済みで、Safariのみ未実装(Interop 2026で対応予定)です。ブラウザカバレッジは約90%ですが、エコシステム(ライブラリ・ホスティング対応)がまだ成熟しておらず、本格採用は2027年以降の見通しです。現時点ではWebSocket/SSEを使い、WebTransportは将来の選択肢として把握しておくのが妥当です。
まとめ
- WebSocket vs SSE: 双方向ならWebSocket(Socket.IO)、サーバーpush主体ならSSE。迷ったらSSEから
- Socket.IO + TypeScript: 4つの型インターフェースで型安全なイベント設計。Redis Adapterで水平スケーリング
- Hono SSE:
streamSSE()で軽量なサーバーpush実装 - Next.js SSE: Route Handlersで既存プロジェクトに手軽にリアルタイム機能を追加。
force-dynamicとX-Accel-Buffering: noを忘れずに - 接続管理: React Hook(useWebSocket / useSSE)で状態管理。指数バックオフ+jitterで安定した再接続
- CLAUDE.md: プロトコル選定・型安全・認証・接続管理のルールを書いて品質を担保
Next.jsとの統合はClaude Code × Next.js完全ガイド、API開発はClaude Code × API開発自動化ガイド、認証連携はClaude Code × 認証/認可実装ガイドもあわせてご覧ください。

