홍풍 프로젝트를 개발하면서 도전적이면서도 흥미로웠던 부분이 실시간 기능이었다.

연습실 예약 시스템에서 실시간으로 현재 이용 상태를 확인할 수 있다면, 사용자들이 헛걸음하는 일을 크게 줄일 수 있을 것 같았다. 하지만 당시에는 WebSocket이나 실시간 통신에 대한 경험이 전혀 없어서 막막했다.

실시간 기능의 필요성

홍풍의 핵심 문제는 연습실이 지하에 있다는 점이었다. 예약을 했지만 실제로는 다른 사람이 사용 중이거나, 예약 시간이 변경되었는데 알 수 없는 상황이 자주 발생했다.

기존의 HTTP 요청 기반 시스템으로는 이런 실시간 상황 변화를 감지하기 어려웠다. 사용자가 페이지를 새로고침하거나 수동으로 상태를 확인해야만 최신 정보를 알 수 있었다.

WebSocket과 실시간 통신의 이해

실시간 통신을 구현하기 전에 먼저 WebSocket이 무엇인지 이해해야 했다.

HTTP vs WebSocket

기존의 HTTP 통신은 클라이언트가 요청을 보내면 서버가 응답을 보내는 단방향 통신이었다. 하지만 실시간 기능을 위해서는 서버가 클라이언트에게 자발적으로 데이터를 보낼 수 있어야 했다.

WebSocket은 HTTP와 달리 양방향 통신을 지원한다. 한 번 연결이 수립되면 서버와 클라이언트가 자유롭게 데이터를 주고받을 수 있다. 이는 실시간 채팅, 게임, 실시간 알림 등에 필수적인 기술이다.

WebSocket의 동작 방식

WebSocket 연결은 HTTP 핸드셰이크로 시작한다. 클라이언트가 WebSocket 업그레이드 요청을 보내면, 서버가 이를 승인하고 프로토콜을 WebSocket으로 전환한다. 이후에는 HTTP의 오버헤드 없이 효율적인 양방향 통신이 가능하다.


// WebSocket 연결 과정
// 1. HTTP 핸드셰이크
GET /socket.io/?EIO=4&transport=websocket HTTP/1.1
Upgrade: websocket
Connection: Upgrade

// 2. 서버 응답
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade

// 3. 이후 WebSocket 프로토콜로 통신

실시간 통신을 구현하기 위해 여러 옵션을 고려했다. WebSocket, Server-Sent Events, Polling 등이 있었지만, Socket.IO를 선택한 이유는 명확했다.

Socket.IO는 자동으로 재연결을 시도하여 네트워크 불안정 상황에서도 안정적인 연결을 유지한다. WebSocket이 지원되지 않는 환경에서도 HTTP long-polling으로 자동 전환되어 안정적인 연결을 보장한다. 그리고 기존의 HTTP 요청-응답 패턴과 달리, 이벤트 기반으로 자연스러운 실시간 통신이 가능했다.

NestJS 서버 측 구현

NestJS에서는 Socket.IO 게이트웨이를 사용해서 실시간 통신을 구현했다.

// reservation.gateway.ts
import { WebSocketGateway, WebSocketServer, SubscribeMessage } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway({
  namespace: '/reservation',
  cors: {
    origin: "*",
    methods: ["GET", "POST"]
  }
})
export class ReservationGateway {
  @WebSocketServer()
  server: Server;

  handleConnection(client: Socket) {
    console.log('클라이언트 연결됨:', client.id);
  }

  handleDisconnect(client: Socket) {
    console.log('클라이언트 연결 해제:', client.id);
  }

  // 예약 목록 업데이트 시 모든 클라이언트에게 전송
  broadcastReservationUpdate(sessionList: Session[]) {
    this.server.emit('reservationsFetched', JSON.stringify(sessionList));
  }
}

WebSocket 이벤트 처리