본문 바로가기
C.W.K.
Stream
Lesson 08 of 08 · published

실전 패턴

~13 min · browser, heartbeat, router

Level 0Poller
0 XP0/60 lessons0/10 achievements
0/120 XP to next level120 XP to go0% complete

애플리케이션 수준의 ping/pong

opcode 0x9 와 0xA 를 쓰는 RFC ping/pong 은 브라우저 API 에 드러나지 않아. 브라우저가 응답은 처리하지만 애플리케이션에서 시작할 수 없지. 그래서 직접 심박을 만들어야 해. 30초마다 {type: 'ping'} 을 보내고 몇 초 안에 {type: 'pong'} 이 오길 기다려. 오지 않으면 코드 4000 으로 연결을 닫고 재연결 로직에 맡겨. NAT 시간 제한이나 노트북 덮개 닫힘처럼 브라우저가 알아채지 못한 채 죽은 연결을 찾아낼 수 있어.

type 별 메시지 라우터

메시지에 type 필드가 있으면 그 값에 따라 보내면 돼. 종류가 몇 개뿐이라면 평평한 switch 도 괜찮지만, 처리기 등록소인 Map<type, handler> 가 늘어나기 쉬워. 라우터는 로깅, 검증, 오류 처리처럼 여러 메시지에 걸친 관심사를 넣기에도 자연스러운 자리야.

상속 대신 조합해

재연결, 심박, 라우팅은 서로 독립된 관심사야. 거대한 클래스 하나보다 기본 소켓 위에 작고 조합 가능한 래퍼를 쌓아. cwkPippa 의 어댑터도 정확히 이런 모양이야. 경계도 좁고 책임도 좁지.

Code

재연결·심박·라우터를 갖춘 견고한 클라이언트·javascript
class RobustSocket extends EventTarget {
  constructor(url) {
    super();
    this.url = url;
    this.handlers = new Map();      // type -> fn
    this.pingTimer = null;
    this.pongTimer = null;
    this._connect();
  }

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

  send(type, data) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, data }));
    }
  }

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

    this.ws.addEventListener('open', () => {
      this._startHeartbeat();
      this.dispatchEvent(new Event('connected'));
    });

    this.ws.addEventListener('message', (e) => {
      let msg;
      try { msg = JSON.parse(e.data); } catch { return; }
      if (msg.type === 'pong') return clearTimeout(this.pongTimer);
      const handler = this.handlers.get(msg.type);
      if (handler) handler(msg.data);
      else this.dispatchEvent(new MessageEvent('unhandled', { data: msg }));
    });

    this.ws.addEventListener('close', () => {
      this._stopHeartbeat();
      // Reconnection logic from previous lesson goes here.
    });
  }

  _startHeartbeat() {
    this.pingTimer = setInterval(() => {
      if (this.ws?.readyState !== WebSocket.OPEN) return;
      this.ws.send(JSON.stringify({ type: 'ping' }));
      this.pongTimer = setTimeout(() => {
        // No pong in 5s; assume dead.
        this.ws.close(4000, 'heartbeat timeout');
      }, 5_000);
    }, 30_000);
  }

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

// Usage
const ws = new RobustSocket('wss://api.example.com/ws');
ws.on('chat.message', (m) => renderMessage(m));
ws.on('user.joined',  (u) => addUserToSidebar(u));
ws.send('chat.message', { room: 'general', text: 'hi' });

External links

Exercise

이전 레슨의 ReconnectingWebSocket 위에 RobustSocket 을 조합해 재연결, 심박, 라우팅을 함께 처리하는 래퍼를 만들어. 메시지 종류 5개를 등록한 뒤 네트워크를 60초 동안 끊어 봐. 클라이언트가 다시 연결되고 메시지를 올바르게 보내며 처리되지 않은 오류를 던지지 않아야 해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.