본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 3장: WebSocket — 양방향 실시간 통신
2026년 3월 22일·아키텍처·

3장: WebSocket — 양방향 실시간 통신

WebSocket의 핸드셰이크, 프레이밍 구조, 양방향 통신의 강점과 상태 관리의 복잡성을 분석합니다. AI 채팅에서의 생성 중단, Socket.IO, 스케일링 전략을 다룹니다.

17분753자10개 섹션
streamingai
공유
streaming-ai3 / 10
12345678910
이전2장: SSE(Server-Sent Events) 심층 분석다음4장: gRPC Streaming — 고성능 백엔드 통신

학습 목표

  • WebSocket 핸드셰이크와 프레이밍 프로토콜의 동작 원리를 이해합니다
  • 양방향 통신이 AI 시스템에서 필요한 구체적 시나리오를 파악합니다
  • 유상태 연결 관리의 복잡성과 해결 전략을 학습합니다
  • Socket.IO의 역할과 트레이드오프를 분석합니다
  • 커넥션 풀, 스티키 세션, 스케일링 패턴을 다룹니다

WebSocket의 탄생 배경

2장에서 살펴본 SSE는 단방향이라는 근본적 한계가 있습니다. 서버에서 클라이언트로만 데이터가 흐르죠. 그런데 실시간 AI 채팅에서는 다음과 같은 상황이 발생합니다.

  • 사용자가 AI의 응답 생성 중에 중단을 요청합니다
  • 여러 사용자가 동시에 같은 AI 세션을 관찰하고 개입합니다
  • 음성 대화에서 사용자의 발화와 AI 응답이 동시에 흐릅니다

이런 시나리오에서는 클라이언트와 서버가 동시에 데이터를 주고받을 수 있는 양방향 통신이 필요합니다. WebSocket은 바로 이 문제를 해결하기 위해 설계된 프로토콜입니다.

핸드셰이크: HTTP에서 WebSocket으로

WebSocket 연결은 일반 HTTP 요청으로 시작되어, 프로토콜 업그레이드를 통해 WebSocket 연결로 전환됩니다.

WebSocket 핸드셰이크
text
[클라이언트 요청]
GET /ws/chat HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
 
[서버 응답]
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

핸드셰이크가 완료되면, 같은 TCP 연결 위에서 양쪽이 자유롭게 메시지를 보낼 수 있습니다. HTTP의 요청-응답 패턴에서 완전히 벗어나는 것입니다.

Info

Sec-WebSocket-Key와 Sec-WebSocket-Accept는 보안을 위한 것이 아닙니다. 프록시가 일반 HTTP 응답을 WebSocket 응답으로 오해하는 것을 방지하기 위한 프로토콜 검증 메커니즘입니다.

프레이밍 구조

WebSocket은 자체 프레이밍(Framing) 프로토콜을 가지고 있습니다. HTTP와 달리 매우 경량입니다.

WebSocket 프레임 구조
text
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |          (16/64)              |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Masking-key (0 or 4 bytes)                                |
+-------------------------------+-------------------------------+
|                    Payload Data                                |
+---------------------------------------------------------------+

최소 오버헤드는 2바이트(마스킹 없는 서버 to 클라이언트)에 불과합니다. SSE의 data: 접두사와 \n\n 구분자보다 효율적이며, 바이너리 데이터도 직접 전송할 수 있습니다.

프레임 타입Opcode용도
Text0x1UTF-8 텍스트 전송
Binary0x2바이너리 데이터 전송
Close0x8연결 종료
Ping0x9연결 유지 확인 (서버 발신)
Pong0xAPing에 대한 응답

AI 채팅에서의 양방향 통신

WebSocket이 AI 시스템에서 빛나는 구체적인 시나리오를 살펴보겠습니다.

생성 중단 (Cancel Generation)

사용자가 AI의 응답 도중 "중단" 버튼을 누르는 경우입니다. SSE에서는 연결을 끊는 것만 가능하지만, WebSocket에서는 중단 메시지를 보내고 서버가 정리 작업을 수행한 뒤 확인 응답을 보낼 수 있습니다.

websocket-chat-client.ts
typescript
interface WSMessage {
  type: "chat" | "cancel" | "token" | "done" | "error";
  payload: string;
  requestId: string;
}
 
class AIChatClient {
  private ws: WebSocket;
  private currentRequestId: string | null = null;
 
  constructor(url: string) {
    this.ws = new WebSocket(url);
    this.ws.onmessage = this.handleMessage.bind(this);
  }
 
  sendMessage(content: string) {
    this.currentRequestId = crypto.randomUUID();
    const msg: WSMessage = {
      type: "chat",
      payload: content,
      requestId: this.currentRequestId,
    };
    this.ws.send(JSON.stringify(msg));
  }
 
  cancelGeneration() {
    if (this.currentRequestId) {
      const msg: WSMessage = {
        type: "cancel",
        payload: "",
        requestId: this.currentRequestId,
      };
      this.ws.send(JSON.stringify(msg));
    }
  }
 
  private handleMessage(event: MessageEvent) {
    const msg: WSMessage = JSON.parse(event.data);
 
    switch (msg.type) {
      case "token":
        appendTokenToUI(msg.payload);
        break;
      case "done":
        finishResponse();
        break;
      case "error":
        showError(msg.payload);
        break;
    }
  }
}

실시간 협업 AI 세션

여러 사용자가 동시에 AI와 대화하며, 서로의 입력과 AI 응답을 실시간으로 관찰하는 시나리오입니다. 화상 회의에서 AI 비서가 참여하는 경우를 생각해볼 수 있습니다.

상태 관리의 복잡성

WebSocket의 양방향 통신은 강력하지만, 대가가 따릅니다. 연결이 유상태라는 점입니다.

연결 수명주기 관리

websocket-server-lifecycle.ts
typescript
import { WebSocketServer, WebSocket } from "ws";
 
const wss = new WebSocketServer({ port: 8080 });
 
// 연결 상태 저장소
const connections = new Map<
  string,
  {
    ws: WebSocket;
    userId: string;
    sessionId: string;
    lastPing: number;
  }
>();
 
wss.on("connection", (ws, request) => {
  const connectionId = crypto.randomUUID();
 
  connections.set(connectionId, {
    ws,
    userId: extractUserId(request),
    sessionId: extractSessionId(request),
    lastPing: Date.now(),
  });
 
  // 하트비트 설정
  const heartbeat = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.ping();
    }
  }, 30000);
 
  ws.on("pong", () => {
    const conn = connections.get(connectionId);
    if (conn) conn.lastPing = Date.now();
  });
 
  ws.on("close", () => {
    clearInterval(heartbeat);
    connections.delete(connectionId);
    cleanupSession(connectionId);
  });
 
  ws.on("error", (error) => {
    console.error(`Connection ${connectionId} error:`, error);
    clearInterval(heartbeat);
    connections.delete(connectionId);
  });
});
 
// 좀비 연결 정리 (60초 이상 무응답)
setInterval(() => {
  const now = Date.now();
  for (const [id, conn] of connections) {
    if (now - conn.lastPing > 60000) {
      conn.ws.terminate();
      connections.delete(id);
    }
  }
}, 30000);
Warning

WebSocket 연결은 서버 메모리를 점유합니다. 하트비트를 통한 좀비 연결 감지, 연결 수 제한, 메모리 모니터링은 프로덕션에서 필수입니다. 서버 재시작 시 모든 연결이 끊어지므로, 클라이언트 측 재연결 로직도 반드시 구현해야 합니다.

Socket.IO: 추상화의 트레이드오프

Socket.IO는 WebSocket을 감싸는 가장 인기 있는 라이브러리입니다. 자동 재연결, 룸(Room), 네임스페이스, 폴백 전송(HTTP Long-Polling) 등을 제공합니다.

socketio-ai-chat.ts
typescript
import { Server } from "socket.io";
 
const io = new Server(server, {
  cors: { origin: "*" },
  transports: ["websocket", "polling"],
});
 
// 네임스페이스로 AI 채팅 분리
const aiChat = io.of("/ai-chat");
 
aiChat.on("connection", (socket) => {
  // 룸을 활용한 세션 관리
  socket.on("join-session", (sessionId: string) => {
    socket.join(sessionId);
  });
 
  socket.on("chat-message", async (data) => {
    const { sessionId, message } = data;
 
    // 같은 세션의 모든 참여자에게 사용자 메시지 브로드캐스트
    aiChat.to(sessionId).emit("user-message", {
      userId: socket.id,
      message,
    });
 
    // AI 응답 스트리밍
    for await (const token of streamLLMResponse(message)) {
      aiChat.to(sessionId).emit("ai-token", { token });
    }
 
    aiChat.to(sessionId).emit("ai-done");
  });
 
  socket.on("cancel-generation", (data) => {
    cancelLLMRequest(data.sessionId);
    aiChat.to(data.sessionId).emit("generation-cancelled");
  });
});

Socket.IO를 사용해야 할 때와 피해야 할 때

사용 적합사용 부적합
룸 기반 그룹 통신이 필요할 때단순한 서버 to 클라이언트 스트리밍
다양한 브라우저 호환성이 필요할 때성능이 최우선일 때
빠른 프로토타이핑네이티브 앱 클라이언트가 주력일 때
자동 재연결 + 상태 복구가 중요할 때경량 연결이 필요할 때
Tip

Socket.IO는 자체 프로토콜을 사용하므로, 반드시 Socket.IO 클라이언트 라이브러리가 필요합니다. 표준 WebSocket 클라이언트와는 호환되지 않는다는 점을 유의하세요.

스케일링 전략

WebSocket의 유상태 특성은 스케일링을 복잡하게 만듭니다. 연결이 특정 서버에 묶여 있기 때문입니다.

스티키 세션 (Sticky Session)

로드밸런서가 같은 클라이언트의 요청을 항상 같은 서버로 보내는 방식입니다.

Redis Pub/Sub를 통한 서버 간 메시지 전달

여러 서버에 분산된 클라이언트들에게 메시지를 브로드캐스트해야 할 때, Redis Pub/Sub를 활용합니다.

redis-pubsub-scaling.ts
typescript
import { createClient } from "redis";
import { WebSocketServer } from "ws";
 
const publisher = createClient();
const subscriber = createClient();
 
await publisher.connect();
await subscriber.connect();
 
const wss = new WebSocketServer({ port: 8080 });
const localConnections = new Map<string, WebSocket>();
 
// 로컬 WebSocket 메시지를 Redis로 발행
wss.on("connection", (ws) => {
  const id = crypto.randomUUID();
  localConnections.set(id, ws);
 
  ws.on("message", async (data) => {
    const msg = JSON.parse(data.toString());
    // 다른 서버의 클라이언트에게도 전달
    await publisher.publish(
      `session:${msg.sessionId}`,
      JSON.stringify(msg)
    );
  });
 
  ws.on("close", () => localConnections.delete(id));
});
 
// Redis에서 메시지를 수신하여 로컬 클라이언트에 전달
await subscriber.pSubscribe("session:*", (message) => {
  const msg = JSON.parse(message);
  for (const [, ws] of localConnections) {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(message);
    }
  }
});

연결 드레이닝 (Connection Draining)

서버를 배포하거나 스케일 다운할 때, 기존 연결을 안전하게 종료해야 합니다.

connection-draining.ts
typescript
// 그레이스풀 셧다운
process.on("SIGTERM", async () => {
  // 1. 로드밸런서에서 제거 (새 연결 차단)
  await deregisterFromLoadBalancer();
 
  // 2. 기존 연결에 종료 예고
  for (const [id, conn] of connections) {
    conn.ws.send(
      JSON.stringify({
        type: "server-shutdown",
        reconnectAfter: 5000,
      })
    );
  }
 
  // 3. 진행 중인 스트리밍이 완료될 때까지 대기 (최대 30초)
  const deadline = Date.now() + 30000;
  while (connections.size > 0 && Date.now() < deadline) {
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
 
  // 4. 남은 연결 강제 종료
  for (const [, conn] of connections) {
    conn.ws.close(1001, "Server shutting down");
  }
 
  process.exit(0);
});

SSE vs WebSocket: 최종 비교

기준SSEWebSocket
통신 방향단방향양방향
프로토콜HTTP자체 (HTTP 업그레이드)
데이터 형식텍스트텍스트 + 바이너리
자동 재연결내장직접 구현
상태 관리무상태유상태
스케일링단순복잡
프록시 호환우수추가 설정 필요
오버헤드약간 높음 (텍스트)매우 낮음 (2바이트)
AI 적합 시나리오토큰 스트리밍실시간 채팅, 협업, 음성
Info

"둘 중 하나"가 아닙니다. 프로덕션 AI 시스템에서는 LLM 토큰 출력에 SSE를, 사용자 간 실시간 협업에 WebSocket을 함께 사용하는 하이브리드 접근이 일반적입니다.


정리

이번 장에서는 WebSocket의 프로토콜 구조부터 AI 시스템에서의 활용, 그리고 스케일링 전략까지를 살펴보았습니다.

  • WebSocket은 HTTP 업그레이드를 통해 양방향 실시간 통신을 제공합니다
  • 생성 중단, 실시간 협업 등 클라이언트가 서버에 실시간으로 메시지를 보내야 하는 시나리오에서 강점을 발휘합니다
  • 유상태 연결은 하트비트, 좀비 감지, 연결 드레이닝 등의 관리 부담을 수반합니다
  • 스케일링에는 스티키 세션과 Redis Pub/Sub 같은 추가 인프라가 필요합니다
  • Socket.IO는 편리한 추상화를 제공하지만, 표준 WebSocket과 호환되지 않는 점에 유의해야 합니다

다음 장에서는 클라이언트-서버 통신을 넘어, 백엔드 서비스 간 고성능 통신을 위한 gRPC Streaming을 다룹니다. HTTP/2 기반의 4가지 스트리밍 모드와 Protobuf 직렬화가 마이크로서비스 추론 파이프라인에서 어떻게 활용되는지 살펴보겠습니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#streaming#ai

관련 글

아키텍처

4장: gRPC Streaming — 고성능 백엔드 통신

HTTP/2 기반 gRPC의 4가지 스트리밍 모드, Protobuf 직렬화, 마이크로서비스 간 추론 파이프라인 구현을 다룹니다. gRPC-Web의 제약과 Python/Go 구현 예제를 포함합니다.

2026년 3월 24일·16분
아키텍처

2장: SSE(Server-Sent Events) 심층 분석

HTTP 기반 단방향 스트리밍 프로토콜인 SSE의 동작 원리, EventSource API, 자동 재연결 메커니즘을 분석하고 Next.js와 FastAPI에서의 LLM 토큰 스트리밍 구현을 다룹니다.

2026년 3월 20일·14분
아키텍처

5장: 스트리밍 LLM 응답 처리

OpenAI, Anthropic, Google의 스트리밍 API 차이를 비교하고, 구조화된 출력의 파셜 파싱, React 스트리밍 UI 렌더링, Vercel AI SDK 활용법을 다룹니다.

2026년 3월 26일·15분
이전 글2장: SSE(Server-Sent Events) 심층 분석
다음 글4장: gRPC Streaming — 고성능 백엔드 통신

댓글

목차

약 17분 남음
  • 학습 목표
  • WebSocket의 탄생 배경
  • 핸드셰이크: HTTP에서 WebSocket으로
  • 프레이밍 구조
  • AI 채팅에서의 양방향 통신
    • 생성 중단 (Cancel Generation)
    • 실시간 협업 AI 세션
  • 상태 관리의 복잡성
    • 연결 수명주기 관리
  • Socket.IO: 추상화의 트레이드오프
    • Socket.IO를 사용해야 할 때와 피해야 할 때
  • 스케일링 전략
    • 스티키 세션 (Sticky Session)
    • Redis Pub/Sub를 통한 서버 간 메시지 전달
    • 연결 드레이닝 (Connection Draining)
  • SSE vs WebSocket: 최종 비교
  • 정리