네트워크 환경 변화
브라우저/앱 상태 변화
중간 인프라의 제약
서버 운영에 따른 종료
WebSocket이 종료될 때 발생하는 CloseEvent를 통해 연결이 어떻게 끊어졌는지 파악할 수 있다.
WebSocket 종료 시 반환되는 코드는 RFC 6455에 정의되어있다.
| 코드 | 의미 |
| 1000 (Normal Closure) | 정상 종료 |
| 1001 | 엔드포인트가 “떠나간” 상황. 예: 서버가 다운되거나 브라우저가 페이지에서 나간 경우 |
| 1002 | 프로토콜 오류 |
| 1003 | - 허용되지 않은 데이터 유형을 수신해서 종료. - 예: 텍스트만 이해하는 엔드포인트가 바이너리 메시지를 받은 경우 |
| 1004 | 예약된 코드로, 현재는 의미가 없으며 향후 정의될 수 있음 |
| 1005 (No Status Recieved) | - 상태 코드 없이 종료되었음을 나타내기 위한 예약 값 - close() 메서드를 파라미터 없이 호출 |
| 1006 (Abnormal Closure) | - 클로즈 프레임 없이 비정상 종료된 경우를 나타내는 예약 값 - 예: 네트워크 에러, 타임아웃 등 |
정상적인 끊김일 때는 추가적인 조치가 필요하지는 않지만, ABNORMAL_CLOSURE일 때는 소켓 재연결을 시도해야 한다.
const NORMAL_CLOSURE = 1000;const ABNORMAL_CLOSURE = 1006;socket.onclose = (e: CloseEvent) => {if (e.code === NORMAL_CLOSURE)
세부적인 코드 구분이 필요 없다면 wasClean 플래그가 더 간편하다.
socket.onclose = (e: CloseEvent) => {if (e.wasClean) {console.log('[Chat] WS Closed');} else {console.log
연결이 끊어지는 즉시 WebSocket을 재설정하는 방식이다. 재시도 사이에 대기 시간이 없으며, 가능한 한 빠르게 연결을 복구해 다운타임을 최소화하는 것을 목표로 한다.
이 방식의 장점은 단기적인 끊김이 발생했을 때 사용자가 거의 중단을 느끼지 않을 정도로 빠르게 복구된다는 점이다. 또한 구현이 간단해, close 이벤트를 감지해 곧바로 재연결을 수행하도록 최소한의 코드로 처리할 수 있다.
하지만 네트워크 장애나 서버 장애처럼 대규모 중단이 발생한 상황에서는 모든 클라이언트가 동시에 재연결을 시도해 서버에 과도한 부하를 줄 위험이 있다. 이러한 경우, 즉시 재연결보다는 일정한 랜덤 지연이나 백오프 전략을 병행하는 것이 바람직하다.
연결이 끊어진 직후에는 서버나 클라이언트 측에서 문제를 복구할 약간의 시간적 여유를 두는 것이 중요하다. 모든 클라이언트가 즉시 재연결을 시도하면 서버에 과도한 부하가 걸릴 수 있으므로, 클라이언트별로 랜덤한 시간차를 두고 재연결을 시도하도록 한다. 이를 통해 서버의 부담을 줄이고 보다 안정적인 재연결 과정을 보장할 수 있다.
WebSocket 프로토콜(RFC 6455)에서도 비정상 종료 이후 다음과 같은 지침을 제시한다.
다만, 서비스 성격에 따라 긴 지연이 적절하지 않을 수 있다. 예를 들어 실시간성이 중요한 서비스라면 첫 번째 재연결 시도를 0초~1초 사이의 랜덤한 간격으로 두는 것도 효과적이다.
const MAX_TIME_INTERVAL = 1000;const reconnectTimeInterval = Math.random() * MAX_TIME_INTERVAL;const handleWebSocketClose = (e: CloseEvent) => {//...
재연결 요청이 연속적으로 실패하는 상황이라면, 재연결 시도 간의 간격을 점진적으로 늘리는 지수 백오프 방식을 적용하는 것이 좋다.
지수 백오프란 서버에 반복적으로 요청을 보낼 때 각 요청 사이의 대기 시간을 단계적으로 늘려가는 방법을 말한다. 서버가 일시적으로 응답할 수 없는 상태에서 클라이언트가 일정한 주기로 계속 요청을 보내면, 이는 서버 리소스의 낭비로 이어진다. 특히 서버가 과도한 트래픽으로 연결 장애를 겪는 경우, 즉각적인 재요청은 문제를 더욱 악화시킬 수 있다.
따라서 요청이 실패할 때마다 다음 재시도까지의 간격을 늘림으로써, 서버의 부담을 줄이고 보다 안정적이고 효율적인 재연결을 이끌어낼 수 있다.
const MAX_TIME_INTERVAL = 1000;let reconnectTimeInterval = Math.random() * MAX_TIME_INTERVAL;const handleWebSocketOpen = () => {console.log('[Chat] WS Connected'
다만 이 방식은 초기 재연결 속도가 느려질 수 있고, 백오프 주기가 길어지는 동안 실시간 데이터 갱신을 놓칠 위험이 있다는 단점이 있다.
지수 백오프를 적용해 재연결 시도 간 간격을 점진적으로 늘려가는 것과 함께, 최대 재연결 시도 횟수를 제한하여 무한 반복을 방지하고 안정적인 연결 복구를 관리한다. 서버가 장기간 사용할 수 없는 상태에서 무한히 재연결을 시도하면 불필요한 리소스 낭비와 서버 과부하를 유발할 수 있다.
따라서 일정 횟수 이상 재연결이 실패하면 시도를 중단하도록 하여, 불필요한 루프를 막고 시스템 안정성을 확보하는 것이 중요하다.
const MAX_TIME_INTERVAL = 1000; // 1초로 설정const MAX_RECONNECT_ATTEMPTS = 5;let reconnectTimeInterval = Math.random() * MAX_TIME_INTERVAL;let reconnectAttempts = 0;
일부 호스팅 환경에서는 1분 이상 데이터 송수신이 없을 경우 WebSocket 연결이 자동 종료되는 문제가 발생한다. WebSocket 프로토콜에는 기본적으로 ping/pong 메커니즘이 존재하지만, 브라우저 환경에서는 이 기능이 내부적으로 처리되므로 개발자가 직접 제어할 수 없다.
따라서 애플리케이션 레벨에서 주기적으로 ping 메시지를 보내고, pong 응답을 수신하는 방식을 직접 구현하여 연결 상태를 유지할 필요가 있다.
// 클라이언트let webSocket: WebSocket | null = null;const DEFAULT_PING_INTERVAL = 50000; // 50초 (1분 타임아웃 전에 전송)let pingTimeout: ReturnType<typeof setTimeout> | null = null;
// 서버 (Node.js 예시)const WebSocket = require('ws');const wss = new WebSocket.Server({ server });wss.on('connection', async (ws,
나아가 일정 시간 동안 pong 응답을 받지 못할 경우, 클라이언트가 연결이 단절되었다고 판단해 자동으로 재연결을 시도하거나, 사용자에게 네트워크 오류 메시지를 표시하도록 확장할 수도 있다.
WebSocket 프로토콜 자체에는 자동 재연결 기능이 존재하지 않는다. 따라서 연결이 끊어졌을 때, CloseEvent를 감지하여 직접 재연결 로직을 구현해야 한다. 보통 setTimeout()이나 재귀 함수를 이용해 지수 백오프 전략을 함께 적용한다.
Socket.IO는 기본적으로 자동 재연결 기능을 제공한다. 또한 다음과 같이 옵션을 지정하면 재연결 동작을 세밀하게 제어할 수 있다.
const socket = io({reconnection: true,reconnectionDelay: 1000,reconnectionDelayMax: 5000,reconnectionAttempts: 5,});
Stomp.js는 WebSocket 위에서 동작하는 메시징 프로토콜로, 기본적으로 자동 재연결 기능을 내장하고 있지 않다. 대신 @stomp/stompjs 라이브러리를 사용하면 자동 재연결 옵션을 설정하여 유사한 동작을 구현할 수 있다.
새로고침 감지 및 재연결
페이지 새로고침 시 JavaScript 메모리가 초기화되어 WebSocket 객체가 사라진다. PerformanceNavigationTiming API를 통해 새로고침을 감지하고 즉시 재연결을 수행한다.
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;if (navigation.type === 'reload') {console.log('Page reloaded - Reconnecting WebSocket...');// 새로고침 감지, 웹소켓 재연결
백그라운드 전환 처리
모바일 환경에서 앱 전환 시 연결 상태를 명시적으로 관리한다. visibilitychange 이벤트를 활용해 백그라운드 진입/복귀 시 적절한 동작을 수행한다.
document.addEventListener('visibilitychange', () => {if (document.hidden) {console.log('App moved to background');// 필요시 연결 종료 또는 유지} else {
이렇게 하면 일상적인 네트워크 오류 외의 특수 상황에서도 WebSocket 연결을 안정적으로 유지할 수 있다.
WebSocket 재연결 시 가장 중요한 것은 클라이언트와 서버의 상태를 동기화하는 것이다. 연결이 끊어진 동안 놓친 메시지나 이벤트를 복구해야 한다.
클라이언트가 마지막으로 받은 메시지의 타임스탬프를 저장하고, 재연결 시 그 이후의 데이터를 요청한다.
let lastMessageTimestamp: number = 0;const handleWebSocketOpen = () => {// 재연결 시 놓친 메시지 요청if (lastMessageTimestamp > 0) {socket.send(
서버에서 각 메시지에 순차적인 번호를 부여하고, 클라이언트가 누락된 메시지를 식별한다.
let lastSequenceNumber: number = 0;const handleWebSocketMessage = (e: MessageEvent) => {const data = JSON.parse(e.data);
간단한 애플리케이션의 경우, 재연결 시 전체 상태를 다시 받아오는 것이 가장 확실하다.
const handleWebSocketOpen = () => {console.log('[WS] Connected - Requesting full state');socket.send(JSON.stringify({ type: 'get_state' }));};
재연결 과정에서 사용자에게 적절한 피드백을 제공하는 것이 중요하다.
enum ConnectionState {CONNECTED = 'connected',CONNECTING = 'connecting',DISCONNECTED = 'disconnected',RECONNECTING = 'reconnecting',FAILED = 'failed',}
연결이 끊어진 동안 보내려던 메시지를 큐에 저장하고, 재연결 시 전송한다.
const messageQueue: string[] = [];let isConnected = false;const sendMessage = (message: string) => {if (isConnected &&
let reconnectAttempt = 0;const MAX_RECONNECT_ATTEMPTS = 5;const handleWebSocketClose = (e: CloseEvent) => {if (e.wasClean) return;
브라우저의 네트워크 상태를 모니터링하여 선제적으로 대응할 수 있다.
// 네트워크 연결 상태 감지// 네트워크가 복구될 때 트리거window.addEventListener('online', () => {console.log('[Network] Back online - Attempting reconnection');if (!socket || socket.readyState === WebSocket.CLOSED)
// 네트워크 타입 변경 감지 (실험적 기능)if ('connection' in navigator) {const connection = (navigator as any).connection;connection.addEventListener('change', () => {
서버는 재연결된 클라이언트를 이전 세션과 연결할 수 있어야 한다.
// 클라이언트const sessionId = localStorage.getItem('ws_session_id') || crypto.randomUUID();localStorage.setItem('ws_session_id', sessionId);const handleWebSocketOpen = () =>
// 서버 (Node.js 예시)const sessions = new Map(); // sessionId -> user datawss.on('connection', (ws) => {ws.on('message', (message) =>
서버 재시작 시 클라이언트에게 미리 알려 준비할 시간을 준다.
// 서버process.on('SIGTERM', () => {console.log('SIGTERM received, closing connections gracefully...');// 모든 클라이언트에게 종료 예정 알림wss.clients.forEach((client) =>
// 클라이언트const handleWebSocketMessage = (e: MessageEvent) => {const data = JSON.parse(e.data);if (data.type === 'server_shutting_down')
| 1007 |
| - 메시지 내에서 일치하지 않는 데이터를 수신. - 예: 텍스트 메시지 안에 비 UTF-8 데이터가 포함된 경우 |
| 1008 | 정책 위반. 다른 적합한 원인이 없거나, 정책 세부 사항을 숨길 필요가 있을 때 사용 |
| 1009 | 메시지 크기가 너무 커서 처리할 수 없음 |
| 1010 | - 클라이언트가 서버가 하나 이상의 확장을 협상할 것으로 예상했지만, 서버의 WebSocket 핸드셰이크 응답에 포함되지 않아 종료. - reason 필드에 필요한 확장 목록을 서술 |
| 1011 | 서버가 요청을 완료하는 데 방해가 되는 예기치 않은 상황(서버 내부 오류 등)에 직면하여 연결 종료 |
| 1015 | TLS 핸드셰이크 실패(예: 서버 인증서를 검증할 수 없음 등)로 인해 연결 종료되었음을 나타내는 예약 값 |