Node.js 인터뷰 질문 94
질문: Node.js에서 WebSocket을 구현하는 방법과 실시간 통신의 주요 고려사항에 대해 설명해주세요.
답변:
Node.js에서 WebSocket을 사용한 실시간 통신 구현 방법과 주요 고려사항을 살펴보겠습니다.
1. 기본 WebSocket 서버 구현
// websocket-server.js
const WebSocket = require("ws");
const http = require("http");
// HTTP 서버 생성
const server = http.createServer();
// WebSocket 서버 생성
const wss = new WebSocket.Server({ server });
// 연결된 클라이언트 관리
const clients = new Map();
// 연결 이벤트 처리
wss.on("connection", (ws, req) => {
const clientId = generateClientId();
clients.set(clientId, ws);
console.log(`클라이언트 연결됨: ${clientId}`);
// 클라이언트로부터 메시지 수신
ws.on("message", (message) => {
try {
const data = JSON.parse(message);
handleMessage(clientId, data);
} catch (error) {
console.error("메시지 처리 오류:", error);
}
});
// 연결 종료 처리
ws.on("close", () => {
console.log(`클라이언트 연결 종료: ${clientId}`);
clients.delete(clientId);
});
// 에러 처리
ws.on("error", (error) => {
console.error(`클라이언트 에러: ${clientId}`, error);
clients.delete(clientId);
});
// 초기 상태 전송
ws.send(
JSON.stringify({
type: "connected",
clientId,
})
);
});
// 메시지 처리 함수
function handleMessage(clientId, data) {
switch (data.type) {
case "broadcast":
broadcastMessage(clientId, data.message);
break;
case "private":
sendPrivateMessage(data.targetId, data.message);
break;
default:
console.warn("알 수 없는 메시지 타입:", data.type);
}
}
// 브로드캐스트 메시지 전송
function broadcastMessage(senderId, message) {
const broadcastData = JSON.stringify({
type: "broadcast",
senderId,
message,
});
clients.forEach((client, clientId) => {
if (clientId !== senderId && client.readyState === WebSocket.OPEN) {
client.send(broadcastData);
}
});
}
// 개인 메시지 전송
function sendPrivateMessage(targetId, message) {
const targetClient = clients.get(targetId);
if (targetClient && targetClient.readyState === WebSocket.OPEN) {
targetClient.send(
JSON.stringify({
type: "private",
message,
})
);
}
}
// 서버 시작
const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
console.log(`WebSocket 서버가 ${PORT} 포트에서 실행 중입니다`);
});
2. 실시간 채팅 구현
// chat-server.js
const WebSocket = require("ws");
const Redis = require("ioredis");
const redis = new Redis();
class ChatServer {
constructor(server) {
this.wss = new WebSocket.Server({ server });
this.rooms = new Map();
this.setupWebSocket();
}
setupWebSocket() {
this.wss.on("connection", (ws) => {
let currentRoom = null;
let username = null;
ws.on("message", async (message) => {
try {
const data = JSON.parse(message);
switch (data.type) {
case "join":
await this.handleJoin(ws, data.room, data.username);
currentRoom = data.room;
username = data.username;
break;
case "message":
if (currentRoom && username) {
await this.handleMessage(currentRoom, username, data.content);
}
break;
case "leave":
if (currentRoom) {
await this.handleLeave(ws, currentRoom, username);
currentRoom = null;
}
break;
}
} catch (error) {
console.error("메시지 처리 오류:", error);
ws.send(
JSON.stringify({
type: "error",
message: "메시지 처리 중 오류가 발생했습니다",
})
);
}
});
ws.on("close", async () => {
if (currentRoom && username) {
await this.handleLeave(ws, currentRoom, username);
}
});
});
}
async handleJoin(ws, room, username) {
// 룸 생성 또는 가져오기
if (!this.rooms.has(room)) {
this.rooms.set(room, new Set());
}
const roomClients = this.rooms.get(room);
roomClients.add(ws);
// Redis에 사용자 추가
await redis.sadd(`chat:room:${room}:users`, username);
// 입장 메시지 브로드캐스트
this.broadcast(room, {
type: "system",
content: `${username}님이 입장했습니다`,
});
// 이전 메시지 히스토리 전송
const history = await this.getChatHistory(room);
ws.send(
JSON.stringify({
type: "history",
messages: history,
})
);
}
async handleMessage(room, username, content) {
const message = {
type: "message",
username,
content,
timestamp: Date.now(),
};
// 메시지 저장
await this.saveChatMessage(room, message);
// 메시지 브로드캐스트
this.broadcast(room, message);
}
async handleLeave(ws, room, username) {
const roomClients = this.rooms.get(room);
if (roomClients) {
roomClients.delete(ws);
if (roomClients.size === 0) {
this.rooms.delete(room);
}
}
// Redis에서 사용자 제거
await redis.srem(`chat:room:${room}:users`, username);
// 퇴장 메시지 브로드캐스트
this.broadcast(room, {
type: "system",
content: `${username}님이 퇴장했습니다`,
});
}
broadcast(room, message) {
const roomClients = this.rooms.get(room);
if (!roomClients) return;
const messageStr = JSON.stringify(message);
roomClients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(messageStr);
}
});
}
async saveChatMessage(room, message) {
await redis.lpush(`chat:room:${room}:messages`, JSON.stringify(message));
// 최대 100개 메시지만 유지
await redis.ltrim(`chat:room:${room}:messages`, 0, 99);
}
async getChatHistory(room) {
const messages = await redis.lrange(`chat:room:${room}:messages`, 0, -1);
return messages.map((msg) => JSON.parse(msg));
}
}
module.exports = ChatServer;
3. 실시간 알림 시스템
// notification-server.js
class NotificationServer {
constructor(wss) {
this.wss = wss;
this.subscriptions = new Map();
}
// 사용자별 알림 구독
subscribe(userId, ws) {
if (!this.subscriptions.has(userId)) {
this.subscriptions.set(userId, new Set());
}
this.subscriptions.get(userId).add(ws);
}
// 구독 해제
unsubscribe(userId, ws) {
const userSubs = this.subscriptions.get(userId);
if (userSubs) {
userSubs.delete(ws);
if (userSubs.size === 0) {
this.subscriptions.delete(userId);
}
}
}
// 알림 전송
sendNotification(userId, notification) {
const userSubs = this.subscriptions.get(userId);
if (!userSubs) return;
const message = JSON.stringify({
type: "notification",
data: notification,
});
userSubs.forEach((ws) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
});
}
// 브로드캐스트 알림
broadcastNotification(notification) {
this.wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(
JSON.stringify({
type: "notification",
data: notification,
})
);
}
});
}
}
// 사용 예시
const notificationServer = new NotificationServer(wss);
// 사용자 연결 시
wss.on("connection", (ws, req) => {
const userId = authenticateUser(req);
if (userId) {
notificationServer.subscribe(userId, ws);
ws.on("close", () => {
notificationServer.unsubscribe(userId, ws);
});
}
});
// 알림 발송
function sendUserNotification(userId, message) {
notificationServer.sendNotification(userId, {
message,
timestamp: Date.now(),
});
}
4. 연결 상태 관리
// connection-manager.js
class ConnectionManager {
constructor(ws) {
this.ws = ws;
this.pingInterval = null;
this.setupHeartbeat();
}
setupHeartbeat() {
// 30초마다 ping 전송
this.pingInterval = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.ping();
}
}, 30000);
// Pong 응답 처리
this.ws.on("pong", () => {
this.ws.isAlive = true;
});
// 연결 상태 확인
const healthCheck = setInterval(() => {
if (this.ws.isAlive === false) {
clearInterval(this.pingInterval);
clearInterval(healthCheck);
return this.ws.terminate();
}
this.ws.isAlive = false;
}, 31000);
// 정리
this.ws.on("close", () => {
clearInterval(this.pingInterval);
clearInterval(healthCheck);
});
}
// 재연결 로직
static handleReconnection(ws) {
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
ws.on("close", () => {
if (reconnectAttempts < maxReconnectAttempts) {
setTimeout(() => {
reconnectAttempts++;
// 재연결 시도
const newWs = new WebSocket(ws.url);
// ... 재연결 로직
}, Math.min(1000 * Math.pow(2, reconnectAttempts), 30000));
}
});
}
}
요약
Node.js WebSocket 구현의 주요 고려사항:
기본 구현
- 연결 관리
- 메시지 처리
- 에러 처리
실시간 기능
- 채팅 시스템
- 알림 시스템
- 브로드캐스팅
성능과 확장성
- 연결 상태 관리
- 재연결 처리
- 메시지 큐잉
보안
- 인증과 권한
- 메시지 검증
- 속도 제한
모니터링
- 연결 상태
- 성능 메트릭
- 오류 추적