Node.js 인터뷰 질문 24

질문: Node.js에서 WebSocket을 어떻게 구현하고 사용하나요?

답변:

WebSocket은 HTTP와 달리 클라이언트와 서버 간에 지속적인 양방향 연결을 제공하는 통신 프로토콜입니다. Node.js에서는 WebSocket을 구현하기 위한 여러 라이브러리가 있으며, 가장 널리 사용되는 것은 wssocket.io입니다.

WebSocket의 주요 특징

  1. 양방향 통신: 클라이언트와 서버가 동시에 데이터를 주고받을 수 있습니다.
  2. 지속적인 연결: HTTP와 달리 연결이 지속되어 요청마다 새로운 연결을 설정할 필요가 없습니다.
  3. 실시간 통신: 낮은 지연 시간으로 실시간 데이터 전송이 가능합니다.
  4. 적은 오버헤드: HTTP 헤더와 같은 불필요한 오버헤드 없이 데이터를 전송합니다.

ws 라이브러리를 사용한 기본 WebSocket 구현

ws는 WebSocket 프로토콜의 순수한 Node.js 구현으로, 클라이언트와 서버 모두에서 작동합니다.

설치

npm install ws

서버 구현

const WebSocket = require("ws");

// WebSocket 서버 생성
const wss = new WebSocket.Server({ port: 8080 });

// 연결 이벤트 처리
wss.on("connection", (ws) => {
  console.log("클라이언트가 연결되었습니다.");

  // 클라이언트로부터 메시지 수신
  ws.on("message", (message) => {
    console.log(`수신한 메시지: ${message}`);

    // 클라이언트에게 메시지 전송
    ws.send(`서버에서 메시지를 받았습니다: ${message}`);
  });

  // 연결 종료 이벤트 처리
  ws.on("close", () => {
    console.log("클라이언트 연결이 종료되었습니다.");
  });

  // 초기 메시지 전송
  ws.send("WebSocket 서버에 연결되었습니다!");
});

console.log("WebSocket 서버가 포트 8080에서 실행 중입니다.");

브라우저 클라이언트

// 브라우저에서 실행되는 클라이언트 코드
const socket = new WebSocket("ws://localhost:8080");

// 연결 이벤트
socket.addEventListener("open", (event) => {
  console.log("서버에 연결되었습니다.");
  socket.send("안녕하세요, 서버!");
});

// 메시지 수신 이벤트
socket.addEventListener("message", (event) => {
  console.log(`서버로부터 메시지 수신: ${event.data}`);
});

// 오류 이벤트
socket.addEventListener("error", (event) => {
  console.error("WebSocket 오류:", event);
});

// 연결 종료 이벤트
socket.addEventListener("close", (event) => {
  console.log("서버와의 연결이 종료되었습니다.");
});

Node.js 클라이언트

const WebSocket = require("ws");

// WebSocket 클라이언트 생성
const ws = new WebSocket("ws://localhost:8080");

ws.on("open", function open() {
  console.log("서버에 연결되었습니다.");
  ws.send("안녕하세요, 클라이언트에서 보냅니다!");
});

ws.on("message", function incoming(data) {
  console.log(`서버로부터 수신: ${data}`);
});

ws.on("close", () => {
  console.log("연결이 종료되었습니다.");
});

모든 클라이언트에게 브로드캐스팅

wss.on("connection", (ws) => {
  ws.on("message", (message) => {
    console.log(`수신한 메시지: ${message}`);

    // 모든 클라이언트에게 메시지 브로드캐스팅
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(`브로드캐스트: ${message}`);
      }
    });
  });
});

socket.io를 사용한 고급 WebSocket 구현

socket.io는 WebSocket의 추상화 계층을 제공하며, 폴백 메커니즘(일부 환경에서 WebSocket을 사용할 수 없을 때 대체 방법으로 폴링 등을 사용)과 추가 기능을 제공합니다.

설치

npm install socket.io

서버 구현 (Express 사용)

const express = require("express");
const http = require("http");
const { Server } = require("socket.io");

// Express 앱과 HTTP 서버 생성
const app = express();
const server = http.createServer(app);
const io = new Server(server);

// 정적 파일 제공
app.use(express.static("public"));

// Socket.io 연결 처리
io.on("connection", (socket) => {
  console.log("사용자가 연결되었습니다:", socket.id);

  // 클라이언트에게 환영 메시지 전송
  socket.emit("welcome", { message: "Socket.io 서버에 연결되었습니다!" });

  // 채팅 메시지 수신 및 브로드캐스팅
  socket.on("chat message", (msg) => {
    console.log(`메시지: ${msg.text} (${socket.id}로부터)`);
    io.emit("chat message", {
      text: msg.text,
      senderId: socket.id,
      timestamp: new Date(),
    });
  });

  // 방 참여
  socket.on("join room", (room) => {
    socket.join(room);
    console.log(`${socket.id}${room} 방에 참여했습니다.`);

    // 해당 방에만 메시지 전송
    io.to(room).emit("room notification", {
      message: `사용자 ${socket.id}가 방에 참여했습니다.`,
    });
  });

  // 연결 종료 처리
  socket.on("disconnect", () => {
    console.log("사용자 연결이 종료되었습니다:", socket.id);
  });
});

// 서버 시작
server.listen(3000, () => {
  console.log("서버가 포트 3000에서 실행 중입니다.");
});

브라우저 클라이언트 (Socket.io)

<!DOCTYPE html>
<html>
  <head>
    <title>Socket.io 채팅</title>
    <script src="/socket.io/socket.io.js"></script>
    <script>
      document.addEventListener("DOMContentLoaded", () => {
        // Socket.io 서버에 연결
        const socket = io();

        // 환영 메시지 수신
        socket.on("welcome", (data) => {
          console.log(data.message);
          addMessage("시스템", data.message);
        });

        // 채팅 메시지 수신
        socket.on("chat message", (msg) => {
          addMessage(msg.senderId, msg.text);
        });

        // 방 알림 수신
        socket.on("room notification", (data) => {
          addMessage("방 알림", data.message);
        });

        // 메시지 전송 폼 처리
        document.getElementById("chat-form").addEventListener("submit", (e) => {
          e.preventDefault();
          const input = document.getElementById("message-input");
          const messageText = input.value.trim();

          if (messageText) {
            // 메시지 전송
            socket.emit("chat message", { text: messageText });
            input.value = "";
          }
        });

        // 방 참여 버튼 처리
        document.getElementById("join-room").addEventListener("click", () => {
          const roomName = document.getElementById("room-input").value.trim();
          if (roomName) {
            socket.emit("join room", roomName);
          }
        });

        // 메시지 표시 함수
        function addMessage(sender, text) {
          const messagesDiv = document.getElementById("messages");
          const messageElement = document.createElement("div");
          messageElement.textContent = `${sender}: ${text}`;
          messagesDiv.appendChild(messageElement);
          messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }
      });
    </script>
  </head>
  <body>
    <h1>Socket.io 채팅</h1>

    <div
      id="messages"
      style="height: 300px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px;"
    ></div>

    <form id="chat-form">
      <input
        id="message-input"
        type="text"
        placeholder="메시지 입력..."
        autocomplete="off"
      />
      <button type="submit">전송</button>
    </form>

    <div style="margin-top: 20px;">
      <input id="room-input" type="text" placeholder="방 이름 입력..." />
      <button id="join-room">방 참여</button>
    </div>
  </body>
</html>

WebSocket vs Socket.io

WebSocket(ws 라이브러리)

장점:

  • 가볍고 빠름
  • WebSocket 프로토콜 표준을 구현
  • 더 낮은 오버헤드와 메모리 사용량
  • 클라이언트측에서도 브라우저 내장 API 사용 가능

단점:

  • 기본 기능만 제공 (수동으로 더 많은 기능 구현 필요)
  • 폴백 메커니즘이 없음
  • 프록시나 로드 밸런서에 따라 연결이 종료될 수 있음

Socket.io

장점:

  • 신뢰성 있는 연결 (폴백 메커니즘 제공)
  • 자동 재연결 및 이벤트 버퍼링
  • 내장된 룸(방) 및 네임스페이스 기능
  • 브로드캐스팅 쉽게 구현
  • 미들웨어 지원
  • 바이너리 데이터 지원

단점:

  • 추가적인 오버헤드(더 큰 클라이언트 라이브러리)
  • WebSocket보다 더 많은 리소스 사용
  • 일부 사용 사례에서는 기능이 과도할 수 있음

WebSocket 보안 고려사항

  1. 인증 및 권한 부여:

    // 인증 구현 예시
    wss.on("connection", (ws, req) => {
      // 인증 토큰 확인
      const token = new URL(req.url, "http://localhost").searchParams.get(
        "token"
      );
    
      if (!validateToken(token)) {
        ws.close(1008, "인증 실패");
        return;
      }
    
      // 인증된 사용자 정보 저장
      ws.user = verifyToken(token);
    
      // 나머지 로직...
    });
    
  2. 입력 유효성 검사:

    ws.on("message", (message) => {
      try {
        const data = JSON.parse(message);
    
        // 입력 유효성 검사
        if (!validateMessage(data)) {
          ws.send(JSON.stringify({ error: "잘못된 메시지 형식" }));
          return;
        }
    
        // 메시지 처리...
      } catch (e) {
        ws.send(JSON.stringify({ error: "잘못된 JSON 형식" }));
      }
    });
    
  3. 속도 제한 구현:

    const connections = new Map();
    
    wss.on("connection", (ws, req) => {
      const ip = req.socket.remoteAddress;
    
      // 연결 속도 제한
      const currentTime = Date.now();
      const connectionData = connections.get(ip) || {
        count: 0,
        lastConnection: 0,
      };
    
      if (
        currentTime - connectionData.lastConnection < 1000 &&
        connectionData.count > 5
      ) {
        ws.close(1008, "속도 제한 초과");
        return;
      }
    
      connections.set(ip, {
        count: connectionData.count + 1,
        lastConnection: currentTime,
      });
    
      // 연결 설정...
    });
    

실시간 애플리케이션 예제: 실시간 대시보드

// 서버 (Socket.io)
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");

const app = express();
const server = http.createServer(app);
const io = new Server(server);

app.use(express.static("public"));

// 시스템 메트릭 모니터링 (가상 데이터)
function getSystemMetrics() {
  return {
    cpu: Math.floor(Math.random() * 100),
    memory: Math.floor(Math.random() * 100),
    network: {
      in: Math.floor(Math.random() * 1000),
      out: Math.floor(Math.random() * 1000),
    },
    users: Math.floor(Math.random() * 200),
    timestamp: new Date(),
  };
}

io.on("connection", (socket) => {
  console.log("대시보드 클라이언트가 연결되었습니다.");

  // 초기 데이터 전송
  socket.emit("metrics", getSystemMetrics());

  // 주기적으로 업데이트된 메트릭 전송
  const interval = setInterval(() => {
    socket.emit("metrics", getSystemMetrics());
  }, 2000);

  // 연결 종료 시 인터벌 정리
  socket.on("disconnect", () => {
    clearInterval(interval);
    console.log("대시보드 클라이언트 연결이 종료되었습니다.");
  });
});

server.listen(3000, () => {
  console.log("대시보드 서버가 포트 3000에서 실행 중입니다.");
});

WebSocket 성능 최적화

  1. 메시지 압축: 대용량 데이터 전송 시 압축 활용

    const WebSocket = require("ws");
    const zlib = require("zlib");
    
    const wss = new WebSocket.Server({ port: 8080 });
    
    wss.on("connection", (ws) => {
      ws.on("message", (message) => {
        // 클라이언트로부터 압축된 메시지 수신 및 압축 해제
        zlib.inflate(message, (err, decompressed) => {
          if (err) return console.error("압축 해제 오류:", err);
    
          console.log(`압축 해제된 메시지: ${decompressed}`);
    
          // 응답 메시지 압축
          zlib.deflate("압축된 응답 메시지", (err, compressed) => {
            if (err) return console.error("압축 오류:", err);
            ws.send(compressed);
          });
        });
      });
    });
    
  2. 연결 상태 확인 (Ping/Pong): 연결 상태 유지 및 불필요한 재연결 방지

    const WebSocket = require("ws");
    const wss = new WebSocket.Server({ port: 8080 });
    
    function heartbeat() {
      this.isAlive = true;
    }
    
    wss.on("connection", (ws) => {
      ws.isAlive = true;
      ws.on("pong", heartbeat);
    });
    
    // 주기적으로 연결 상태 확인
    const interval = setInterval(() => {
      wss.clients.forEach((ws) => {
        if (ws.isAlive === false) return ws.terminate();
    
        ws.isAlive = false;
        ws.ping();
      });
    }, 30000);
    
    wss.on("close", () => {
      clearInterval(interval);
    });
    

WebSocket은 실시간 애플리케이션, 채팅, 게임, 대시보드, 알림 시스템 등 다양한 사용 사례에서 Node.js와 함께 강력한 솔루션을 제공합니다. 요구사항과 사용 사례에 따라 순수한 WebSocket 구현(ws)을 사용하거나 더 많은 기능을 제공하는 Socket.io를 선택할 수 있습니다.

results matching ""

    No results matching ""