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 구현의 주요 고려사항:

  1. 기본 구현

    • 연결 관리
    • 메시지 처리
    • 에러 처리
  2. 실시간 기능

    • 채팅 시스템
    • 알림 시스템
    • 브로드캐스팅
  3. 성능과 확장성

    • 연결 상태 관리
    • 재연결 처리
    • 메시지 큐잉
  4. 보안

    • 인증과 권한
    • 메시지 검증
    • 속도 제한
  5. 모니터링

    • 연결 상태
    • 성능 메트릭
    • 오류 추적

results matching ""

    No results matching ""