WebSocket 완전 정복 · 2부. 중급: 프로토콜 내부와 설계 패턴 · 16편 중 9편
이전 편: 8편 — 메시지 설계
8편의 봉투에 seq를 넣어 둔 이유가 이번 편에서 드러납니다. 연결은 반드시 끊긴다는 전제에서 출발해, 수천 클라이언트가 한꺼번에 돌아오는 것을 막는 지수 백오프와 지터, 재연결하면 안 되는 경우를 코드에서 가르는 법, 그리고 끊긴 동안 놓친 이벤트를 seq로 되찾는 복구 방식을 다룹니다.
연결은 반드시 끊긴다
연결은 반드시 끊깁니다(모바일 네트워크 전환, 절전, 서버 배포). 핵심은 지수 백오프 + 지터. 서버 재시작 시 수천 클라이언트가 동시에 재접속하는 thundering herd를 막습니다.
지수 백오프와 지터
ts
class ReconnectingSocket {
private ws?: WebSocket;
private attempt = 0;
private lastSeq = 0;
private closedByUser = false;
constructor(private url: string, private onEvent: (msg: any) => void) {
this.connect();
}
private connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.attempt = 0;
// 상태 복구: 마지막 seq 이후를 요청
this.ws!.send(JSON.stringify({ type: "resume", since: this.lastSeq }));
};
this.ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (typeof msg.seq === "number") {
if (msg.seq <= this.lastSeq) return; // 중복 무시
this.lastSeq = msg.seq;
}
this.onEvent(msg);
};
this.ws.onclose = (e) => {
if (this.closedByUser) return;
if (e.code >= 4000 && e.code < 4100) return; // 예: 인증 실패 → 재연결 금지
const base = Math.min(30_000, 500 * 2 ** this.attempt++);
const delay = Math.random() * base; // full jitter
setTimeout(() => this.connect(), delay);
};
}
close() { this.closedByUser = true; this.ws?.close(1000); }
}재연결 트리거와 끊긴 동안의 데이터 복구
- 브라우저에선
online/offline,visibilitychange이벤트 활용. - 끊긴 동안의 데이터 복구 방식: 스냅샷(전체 재수신), seq 재전송(빠진 것만), 하이브리드(짧은 단절은 재전송, 긴 단절은 스냅샷).
- 재전송하려면 서버가 최근 이벤트를 보관해야 함 → Redis Streams, Kafka 등 로그형 저장소.
더 깊이
- 운영을 패턴으로 — Health Check, Correlation ID, Leader Election: 재연결은 클라이언트 쪽 이야기이고, 서버 쪽에서는 배포 때 연결을 얼마나 기다렸다 끊을지(드레이닝)와 프로브 주기가 같은 문제의 반대편입니다. WebSocket처럼 오래 사는 연결을 가진 프로세스의 종료 대기를 운영 패턴으로 묶어 봅니다.
- 12편 — 수평 확장: Pub/Sub 백플레인과 무중단 배포: 재전송용 저장소로 언급한 Redis Streams·Kafka가 서버 여러 대의 백플레인 선택지로 이어지고, 무중단 배포 절차에서 이 편의 지터 재연결이 다시 등장합니다.