실시간 화면에서 연결이 끊기면 처음에는 onclose에서 다시 연결하면 된다고 생각했다. 실제로는 네트워크가 잠깐 흔들린 경우, 서버가 명시적으로 연결을 닫은 경우, 인증이 만료된 경우가 모두 달랐다. 재접속에 성공해도 끊긴 동안 놓친 데이터를 어떻게 복구할지가 남았다.
2026년 WebTransport가 Baseline Newly available에 들어왔다는 소식을 보며 WebSocket과 무엇이 다른지 다시 정리했다. 새 전송 방식이 재접속 문제를 자동으로 해결해 주지는 않지만, 데이터 성격에 맞는 전달 방식을 고를 선택지가 늘어난다는 점은 흥미로웠다.
하나의 순서 있는 통로와 여러 전송 방식
WebSocket은 하나의 연결에서 순서가 보장된 양방향 메시지 흐름을 제공한다. 앞의 패킷이 지연되면 뒤의 데이터도 기다리는 특성이 있다. 주문 이벤트처럼 순서와 누락 방지가 중요할 때 이해하기 쉬운 모델이다.
WebTransport는 HTTP/3와 QUIC 위에서 bidirectional·unidirectional stream과 unreliable datagram을 제공한다.
| 데이터 | 어울리는 방식 | 이유 |
|---|---|---|
| 인증·구독 명령 | reliable stream | 순서와 전달 확인 필요 |
| 주문 상태 변화 | reliable stream | 중간 이벤트 유실 위험 |
| 최신 마우스 위치 | datagram | 오래된 값보다 최신 값 중요 |
| 주기적 지연 측정 | datagram | 일부 유실 허용 가능 |
| 일반 브라우저 폴백 | WebSocket | 지원 범위가 더 넓음 |
datagram이 빠르다는 이유로 모든 이벤트를 옮기면 안 된다. 유실되거나 순서가 바뀔 수 있으므로, 최신 값 하나로 이전 값을 대체할 수 있는 데이터인지 먼저 봐야 한다.
기능 감지와 연결
간단한 연결 선택 코드는 다음과 같이 만들 수 있다.
async function connectRealtime() {
if ('WebTransport' in window) {
try {
const transport = new WebTransport(
'https://api.example.com/realtime'
);
await transport.ready;
return { kind: 'webtransport', transport };
} catch (error) {
console.info('WebTransport unavailable', error.name);
}
}
return {
kind: 'websocket',
socket: new WebSocket('wss://api.example.com/realtime')
};
}
API 존재만으로 서버와 네트워크 경로가 HTTP/3 WebTransport를 허용한다는 뜻은 아니다. 연결 실패 시 WebSocket으로 돌아가는 경로를 실제 환경에서 확인해야 한다.
재접속을 이벤트가 아니라 상태로 보기
close → connect()만 쓰면 여러 이벤트가 겹쳐 연결이 두 개 생기거나, 인증 오류에도 무한 재시도할 수 있다. 연결 상태를 제한된 값으로 두면 다음 행동을 정하기 쉽다.
const transitions = {
idle: { START: 'connecting' },
connecting:{ OPEN: 'ready', FAIL: 'retrying', STOP: 'closed' },
ready: { STALE: 'stale', CLOSE: 'retrying', STOP: 'closed' },
stale: { SNAPSHOT_OK: 'ready', CLOSE: 'retrying', STOP: 'closed' },
retrying: { RETRY: 'connecting', AUTH_FAIL: 'closed', STOP: 'closed' },
closed: {}
};
function transition(state, event) {
return transitions[state]?.[event] ?? state;
}
stale 상태를 따로 둔 이유는 소켓이 열려 있다는 것과 데이터가 최신이라는 것이 다르기 때문이다. heartbeat가 오래됐거나 sequence가 건너뛰었다면 화면에 오래된 값을 확정처럼 보여 주지 않고 snapshot을 다시 요청한다.
마지막 sequence = 184
새 이벤트 sequence = 189
→ stale 전환
→ REST 또는 reliable stream으로 snapshot 요청
→ snapshot sequence >= 189 확인
→ 구독 재개 후 ready
다시 연결할 때 지켜야 할 것
재시도 간격은 지수 백오프와 jitter를 사용해 많은 클라이언트가 동시에 서버로 몰리지 않게 한다. 하지만 인증 거부, 지원하지 않는 프로토콜, 사용자의 명시적 로그아웃은 자동 재시도 대상에서 제외한다.
function retryDelay(attempt) {
const capped = Math.min(30000, 500 * 2 ** attempt);
return capped * (0.5 + Math.random() * 0.5);
}
연결이 새로 열렸다고 바로 ready로 두지 않고 다음을 확인한다.
- 인증과 구독이 다시 완료됐는가?
- 마지막 처리 sequence 이후 이벤트를 복구했는가?
- snapshot과 실시간 이벤트의 기준 시점이 맞는가?
- 이전 연결의 timer와 listener를 정리했는가?
- 사용자가 로그아웃했다면 재접속을 중지했는가?
WebTransport는 reliable stream과 datagram을 한 연결에서 선택할 수 있게 해 준다. 그러나 사용자에게 최신 상태를 보여 주는 책임은 전송 API 밖에 있다. WebSocket이든 WebTransport든 연결 상태, 데이터 신선도, 복구 상태를 분리한 상태 머신이 있어야 “다시 연결됨”을 “정상 복구됨”과 구분할 수 있었다.