Claude Code × リアルタイムアプリ開発実践ガイド|WebSocket・SSE・Socket.IO・Hono・接続管理パターン

Claude Code × リアルタイムアプリ開発実践ガイド|WebSocket・SSE・Socket.IO・Hono・接続管理パターン AI開発

チャット、通知、ダッシュボード、コラボレーション編集——リアルタイム通信が必要な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-dynamicX-Accel-Buffering: noを忘れずに
  • 接続管理: React Hook(useWebSocket / useSSE)で状態管理。指数バックオフ+jitterで安定した再接続
  • CLAUDE.md: プロトコル選定・型安全・認証・接続管理のルールを書いて品質を担保

Next.jsとの統合はClaude Code × Next.js完全ガイド、API開発はClaude Code × API開発自動化ガイド、認証連携はClaude Code × 認証/認可実装ガイドもあわせてご覧ください。