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

Application-level ping/pong

RFC ping/pong (opcode 0x9/0xA) 은 브라우저 API 에서 안 보여 — 브라우저가 처리하지만 trigger 못 해. 그래서 직접 만들어. 30초마다 {type: 'ping'} 보내고; 몇 초 안에 {type: 'pong'} 기대; 안 오면 code 4000 으로 close 하고 재연결 logic 한테 넘겨. 브라우저가 못 알아채는 silently-dead connection (NAT timeout, lid 닫힘) 잡아.

Type 별 message router

Message 가 type field 가지면 route 해. 몇 개 type 엔 flat switch 도 ok; registry (Map<type, handler>) 가 더 잘 scale. router 는 cross-cutting concern (logging, validation, error handling) 추가하기 자연스러운 자리.

Compose, 상속하지 마

재연결, heartbeat, routing 은 독립적인 관심사. 한 거대 클래스 대신 base socket 위에 작은 composable wrapper 로 짜. cwkPippa 의 adapter 가 정확히 이 모양 — 좁은 boundary, 좁은 책임.

Code

재연결 + heartbeat + router 가진 robust 클라·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

이전 lesson 의 ReconnectingWebSocket 위에 RobustSocket 을 compose 해서 재연결 + heartbeat + routing 한 번에 처리하는 wrapper 만들어. 5개 message type 등록 후 60초 네트워크 빼봐. 클라가 재연결, message 옳게 route, 절대 unhandled error 안 던져야 해.

Progress

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

댓글 0

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

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