WebSocket은 HTTP와 달리 서버와 클라이언트 간 양방향 실시간 통신 을 가능하게 합니다. 채팅, 실시간 알림, 주식 시세 같은 기능에 필수적인 기술이지만, 연결 관리와 재연결 로직을 직접 구현해야 하는 부분이 있습니다.

기본 사용법

JS
// WebSocket 연결
const ws = new WebSocket("wss://echo.websocket.org");

// 연결 성공
ws.addEventListener("open", () => {
  console.log("연결됨!");
  ws.send("안녕하세요!");
});

// 메시지 수신
ws.addEventListener("message", (event) => {
  console.log("받음:", event.data);
});

// 연결 종료
ws.addEventListener("close", (event) => {
  console.log(`종료: 코드=${event.code}, 이유=${event.reason}`);
});

// 에러
ws.addEventListener("error", (event) => {
  console.error("WebSocket 에러:", event);
});

연결 상태 확인

JS
// readyState 상수
WebSocket.CONNECTING; // 0 — 연결 중
WebSocket.OPEN;       // 1 — 연결됨
WebSocket.CLOSING;    // 2 — 닫는 중
WebSocket.CLOSED;     // 3 — 닫힘

// 상태 확인 후 전송
function safeSend(ws, data) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(data);
  } else {
    console.warn("WebSocket이 열려있지 않음:", ws.readyState);
  }
}

데이터 전송 형식

JS
// 문자열 전송
ws.send("텍스트 메시지");

// JSON 전송 (가장 일반적)
ws.send(JSON.stringify({
  type: "chat",
  payload: { message: "안녕!", room: "general" },
}));

// 바이너리 전송
ws.binaryType = "arraybuffer"; // 또는 "blob"
ws.send(new ArrayBuffer(8));

// 수신 시 타입 확인
ws.addEventListener("message", (event) => {
  if (typeof event.data === "string") {
    const msg = JSON.parse(event.data);
    handleMessage(msg);
  } else {
    handleBinary(event.data);
  }
});

자동 재연결 구현

프로덕션에서 WebSocket을 사용하려면 자동 재연결은 필수입니다.

JS
class ReconnectingWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.maxRetries = options.maxRetries ?? 10;
    this.retryDelay = options.retryDelay ?? 1000;
    this.maxDelay = options.maxDelay ?? 30000;
    this.retryCount = 0;
    this.handlers = { open: [], message: [], close: [], error: [] };

    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.addEventListener("open", (e) => {
      console.log("WebSocket 연결됨");
      this.retryCount = 0; // 성공 시 카운터 리셋
      this.handlers.open.forEach((fn) => fn(e));
    });

    this.ws.addEventListener("message", (e) => {
      this.handlers.message.forEach((fn) => fn(e));
    });

    this.ws.addEventListener("close", (e) => {
      this.handlers.close.forEach((fn) => fn(e));
      if (e.code !== 1000) { // 정상 종료가 아니면 재연결
        this.scheduleReconnect();
      }
    });

    this.ws.addEventListener("error", (e) => {
      this.handlers.error.forEach((fn) => fn(e));
    });
  }

  scheduleReconnect() {
    if (this.retryCount >= this.maxRetries) {
      console.error("최대 재연결 시도 초과");
      return;
    }

    // 지수 백오프 + 지터
    const delay = Math.min(
      this.retryDelay * Math.pow(2, this.retryCount) + Math.random() * 1000,
      this.maxDelay
    );

    console.log(`${delay}ms 후 재연결 시도 (${this.retryCount + 1}/${this.maxRetries})`);
    setTimeout(() => {
      this.retryCount++;
      this.connect();
    }, delay);
  }

  on(event, handler) {
    this.handlers[event]?.push(handler);
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(typeof data === "string" ? data : JSON.stringify(data));
    }
  }

  close() {
    this.ws.close(1000, "정상 종료");
  }
}

하트비트 구현

서버와 클라이언트 간 연결이 살아있는지 주기적으로 확인합니다.

JS
class WebSocketWithHeartbeat extends ReconnectingWebSocket {
  constructor(url, options = {}) {
    super(url, options);
    this.heartbeatInterval = options.heartbeatInterval ?? 30000;
    this.heartbeatTimeout = options.heartbeatTimeout ?? 10000;
    this.pingTimer = null;
    this.pongTimer = null;
  }

  connect() {
    super.connect();

    this.ws.addEventListener("open", () => {
      this.startHeartbeat();
    });

    this.ws.addEventListener("close", () => {
      this.stopHeartbeat();
    });

    this.ws.addEventListener("message", (e) => {
      const data = JSON.parse(e.data);
      if (data.type === "pong") {
        clearTimeout(this.pongTimer); // pong 수신 → 연결 정상
      }
    });
  }

  startHeartbeat() {
    this.pingTimer = setInterval(() => {
      this.send({ type: "ping" });

      // pong 응답 대기
      this.pongTimer = setTimeout(() => {
        console.warn("Pong 미수신 — 연결 끊김으로 판단");
        this.ws.close();
      }, this.heartbeatTimeout);
    }, this.heartbeatInterval);
  }

  stopHeartbeat() {
    clearInterval(this.pingTimer);
    clearTimeout(this.pongTimer);
  }
}

메시지 라우팅 패턴

JS
class MessageRouter {
  constructor(ws) {
    this.ws = ws;
    this.handlers = new Map();

    ws.on("message", (event) => {
      const msg = JSON.parse(event.data);
      const handler = this.handlers.get(msg.type);
      if (handler) {
        handler(msg.payload);
      } else {
        console.warn("알 수 없는 메시지 타입:", msg.type);
      }
    });
  }

  on(type, handler) {
    this.handlers.set(type, handler);
  }

  send(type, payload) {
    this.ws.send(JSON.stringify({ type, payload }));
  }
}

// 사용
const ws = new ReconnectingWebSocket("wss://chat.example.com");
const router = new MessageRouter(ws);

router.on("chat", (payload) => {
  appendMessage(payload.user, payload.text);
});

router.on("userJoined", (payload) => {
  showNotification(`${payload.name}님이 입장했습니다.`);
});

router.send("chat", { text: "안녕하세요!", room: "general" });

연결 종료 코드

코드의미
1000정상 종료
1001서버/클라이언트가 떠남
1006비정상 종료 (네트워크 끊김)
1008정책 위반
1011서버 에러

WebSocket vs HTTP 폴링 vs SSE

기능WebSocketHTTP 폴링SSE
방향양방향클라이언트→서버서버→클라이언트
연결지속매번 새로지속
프로토콜ws://http://http://
적합한 용도채팅, 게임간단한 업데이트알림, 피드

**기억하기 **: WebSocket은 양방향 실시간 통신에 적합하지만, 재연결과 하트비트를 직접 구현해야 합니다. 단방향이면 SSE가 더 간단하고, 실시간성이 필요 없으면 HTTP 폴링이 가장 단순합니다.

댓글 로딩 중...