Node.js 인터뷰 질문 24
질문: Node.js에서 WebSocket을 어떻게 구현하고 사용하나요?
답변:
WebSocket은 HTTP와 달리 클라이언트와 서버 간에 지속적인 양방향 연결을 제공하는 통신 프로토콜입니다. Node.js에서는 WebSocket을 구현하기 위한 여러 라이브러리가 있으며, 가장 널리 사용되는 것은 ws
와 socket.io
입니다.
WebSocket의 주요 특징
- 양방향 통신: 클라이언트와 서버가 동시에 데이터를 주고받을 수 있습니다.
- 지속적인 연결: HTTP와 달리 연결이 지속되어 요청마다 새로운 연결을 설정할 필요가 없습니다.
- 실시간 통신: 낮은 지연 시간으로 실시간 데이터 전송이 가능합니다.
- 적은 오버헤드: 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 보안 고려사항
인증 및 권한 부여:
// 인증 구현 예시 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); // 나머지 로직... });
입력 유효성 검사:
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 형식" })); } });
속도 제한 구현:
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 성능 최적화
메시지 압축: 대용량 데이터 전송 시 압축 활용
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); }); }); }); });
연결 상태 확인 (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를 선택할 수 있습니다.