WebSocket 클라이언트 — 연결 관리, 재연결, 하트비트 구현
WebSocket은 HTTP와 달리 서버와 클라이언트 간 양방향 실시간 통신 을 가능하게 합니다. 채팅, 실시간 알림, 주식 시세 같은 기능에 필수적인 기술이지만, 연결 관리와 재연결 로직을 직접 구현해야 하는 부분이 있습니다.
기본 사용법
// WebSocket 연결
const ws = new WebSocket("wss://echo.websocket.org");
// 연결 성공
ws.addEventListener("open", () => {
console.log("연결됨!");
ws.send("안녕하세요!");
});
// 메시지 수신
ws.addEventListener("message", (event) => {
console.log("받음:", event.data);
});
// 연결 종료
ws.addEventListener("close", (event) => {
console.log(`종료: 코드=${event.code}, 이유=${event.reason}`);
});
// 에러
ws.addEventListener("error", (event) => {
console.error("WebSocket 에러:", event);
});
연결 상태 확인
// readyState 상수
WebSocket.CONNECTING; // 0 — 연결 중
WebSocket.OPEN; // 1 — 연결됨
WebSocket.CLOSING; // 2 — 닫는 중
WebSocket.CLOSED; // 3 — 닫힘
// 상태 확인 후 전송
function safeSend(ws, data) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(data);
} else {
console.warn("WebSocket이 열려있지 않음:", ws.readyState);
}
}
데이터 전송 형식
// 문자열 전송
ws.send("텍스트 메시지");
// JSON 전송 (가장 일반적)
ws.send(JSON.stringify({
type: "chat",
payload: { message: "안녕!", room: "general" },
}));
// 바이너리 전송
ws.binaryType = "arraybuffer"; // 또는 "blob"
ws.send(new ArrayBuffer(8));
// 수신 시 타입 확인
ws.addEventListener("message", (event) => {
if (typeof event.data === "string") {
const msg = JSON.parse(event.data);
handleMessage(msg);
} else {
handleBinary(event.data);
}
});
자동 재연결 구현
프로덕션에서 WebSocket을 사용하려면 자동 재연결은 필수입니다.
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.maxRetries = options.maxRetries ?? 10;
this.retryDelay = options.retryDelay ?? 1000;
this.maxDelay = options.maxDelay ?? 30000;
this.retryCount = 0;
this.handlers = { open: [], message: [], close: [], error: [] };
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.addEventListener("open", (e) => {
console.log("WebSocket 연결됨");
this.retryCount = 0; // 성공 시 카운터 리셋
this.handlers.open.forEach((fn) => fn(e));
});
this.ws.addEventListener("message", (e) => {
this.handlers.message.forEach((fn) => fn(e));
});
this.ws.addEventListener("close", (e) => {
this.handlers.close.forEach((fn) => fn(e));
if (e.code !== 1000) { // 정상 종료가 아니면 재연결
this.scheduleReconnect();
}
});
this.ws.addEventListener("error", (e) => {
this.handlers.error.forEach((fn) => fn(e));
});
}
scheduleReconnect() {
if (this.retryCount >= this.maxRetries) {
console.error("최대 재연결 시도 초과");
return;
}
// 지수 백오프 + 지터
const delay = Math.min(
this.retryDelay * Math.pow(2, this.retryCount) + Math.random() * 1000,
this.maxDelay
);
console.log(`${delay}ms 후 재연결 시도 (${this.retryCount + 1}/${this.maxRetries})`);
setTimeout(() => {
this.retryCount++;
this.connect();
}, delay);
}
on(event, handler) {
this.handlers[event]?.push(handler);
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(typeof data === "string" ? data : JSON.stringify(data));
}
}
close() {
this.ws.close(1000, "정상 종료");
}
}
하트비트 구현
서버와 클라이언트 간 연결이 살아있는지 주기적으로 확인합니다.
class WebSocketWithHeartbeat extends ReconnectingWebSocket {
constructor(url, options = {}) {
super(url, options);
this.heartbeatInterval = options.heartbeatInterval ?? 30000;
this.heartbeatTimeout = options.heartbeatTimeout ?? 10000;
this.pingTimer = null;
this.pongTimer = null;
}
connect() {
super.connect();
this.ws.addEventListener("open", () => {
this.startHeartbeat();
});
this.ws.addEventListener("close", () => {
this.stopHeartbeat();
});
this.ws.addEventListener("message", (e) => {
const data = JSON.parse(e.data);
if (data.type === "pong") {
clearTimeout(this.pongTimer); // pong 수신 → 연결 정상
}
});
}
startHeartbeat() {
this.pingTimer = setInterval(() => {
this.send({ type: "ping" });
// pong 응답 대기
this.pongTimer = setTimeout(() => {
console.warn("Pong 미수신 — 연결 끊김으로 판단");
this.ws.close();
}, this.heartbeatTimeout);
}, this.heartbeatInterval);
}
stopHeartbeat() {
clearInterval(this.pingTimer);
clearTimeout(this.pongTimer);
}
}
메시지 라우팅 패턴
class MessageRouter {
constructor(ws) {
this.ws = ws;
this.handlers = new Map();
ws.on("message", (event) => {
const msg = JSON.parse(event.data);
const handler = this.handlers.get(msg.type);
if (handler) {
handler(msg.payload);
} else {
console.warn("알 수 없는 메시지 타입:", msg.type);
}
});
}
on(type, handler) {
this.handlers.set(type, handler);
}
send(type, payload) {
this.ws.send(JSON.stringify({ type, payload }));
}
}
// 사용
const ws = new ReconnectingWebSocket("wss://chat.example.com");
const router = new MessageRouter(ws);
router.on("chat", (payload) => {
appendMessage(payload.user, payload.text);
});
router.on("userJoined", (payload) => {
showNotification(`${payload.name}님이 입장했습니다.`);
});
router.send("chat", { text: "안녕하세요!", room: "general" });
연결 종료 코드
| 코드 | 의미 |
|---|---|
| 1000 | 정상 종료 |
| 1001 | 서버/클라이언트가 떠남 |
| 1006 | 비정상 종료 (네트워크 끊김) |
| 1008 | 정책 위반 |
| 1011 | 서버 에러 |
WebSocket vs HTTP 폴링 vs SSE
| 기능 | WebSocket | HTTP 폴링 | SSE |
|---|---|---|---|
| 방향 | 양방향 | 클라이언트→서버 | 서버→클라이언트 |
| 연결 | 지속 | 매번 새로 | 지속 |
| 프로토콜 | ws:// | http:// | http:// |
| 적합한 용도 | 채팅, 게임 | 간단한 업데이트 | 알림, 피드 |
**기억하기 **: WebSocket은 양방향 실시간 통신에 적합하지만, 재연결과 하트비트를 직접 구현해야 합니다. 단방향이면 SSE가 더 간단하고, 실시간성이 필요 없으면 HTTP 폴링이 가장 단순합니다.
댓글 로딩 중...