728x90
반응형
1. 웹소켓이란
웹소켓은 클라이언트와 서버 간의 양방향 실시간 통신을 가능하게 하는 프로토콜입니다. HTTP와 달리 연결이 유지되며, 서버에서 클라이언트로 데이터를 푸시할 수 있습니다.
1.1 HTTP vs WebSocket
HTTP:
클라이언트 → 요청 → 서버
클라이언트 ← 응답 ← 서버
(연결 종료)
WebSocket:
클라이언트 ←→ 양방향 통신 ←→ 서버
(연결 유지)
1.2 웹소켓 사용 사례
- 실시간 채팅
- 실시간 알림
- 주식/암호화폐 가격 업데이트
- 멀티플레이어 게임
- 협업 도구 (실시간 문서 편집)
2. ws 라이브러리
Node.js에서 가장 많이 사용되는 WebSocket 라이브러리입니다.
npm install ws
2.1 기본 서버
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('새 클라이언트 연결');
// 메시지 수신
ws.on('message', (message) => {
console.log('받은 메시지:', message.toString());
// 에코
ws.send(`서버 응답: ${message}`);
});
// 연결 종료
ws.on('close', () => {
console.log('클라이언트 연결 종료');
});
// 에러 처리
ws.on('error', (error) => {
console.error('에러:', error);
});
// 연결 환영 메시지
ws.send('WebSocket 서버에 연결되었습니다!');
});
console.log('WebSocket 서버 실행 중: ws://localhost:8080');
2.2 클라이언트
// Node.js 클라이언트
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8080');
ws.on('open', () => {
console.log('서버에 연결됨');
ws.send('안녕하세요!');
});
ws.on('message', (data) => {
console.log('서버 메시지:', data.toString());
});
ws.on('close', () => {
console.log('연결 종료');
});
ws.on('error', (error) => {
console.error('에러:', error);
});
<!-- 브라우저 클라이언트 -->
<script>
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
console.log('연결됨');
ws.send('안녕하세요!');
};
ws.onmessage = (event) => {
console.log('메시지:', event.data);
};
ws.onclose = () => {
console.log('연결 종료');
};
ws.onerror = (error) => {
console.error('에러:', error);
};
</script>
3. Express와 통합
const express = require('express');
const { createServer } = require('http');
const WebSocket = require('ws');
const app = express();
const server = createServer(app);
const wss = new WebSocket.Server({ server });
// HTTP 라우트
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
// WebSocket 연결
wss.on('connection', (ws) => {
console.log('WebSocket 연결');
ws.on('message', (message) => {
console.log('메시지:', message.toString());
});
});
server.listen(3000, () => {
console.log('서버 실행 중: http://localhost:3000');
});
반응형
4. 브로드캐스트
4.1 전체 클라이언트에 전송
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// 모든 클라이언트에게 브로드캐스트
function broadcast(data) {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
}
wss.on('connection', (ws) => {
ws.on('message', (message) => {
// 받은 메시지를 모든 클라이언트에게 전송
broadcast(message.toString());
});
});
4.2 자신을 제외하고 브로드캐스트
function broadcastExcludeSender(sender, data) {
wss.clients.forEach((client) => {
if (client !== sender && client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
}
wss.on('connection', (ws) => {
ws.on('message', (message) => {
broadcastExcludeSender(ws, message.toString());
});
});
5. 채팅 서버 구현
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// 클라이언트 관리
const clients = new Map();
function broadcast(message, excludeId = null) {
const data = JSON.stringify(message);
wss.clients.forEach((client) => {
const clientInfo = clients.get(client);
if (client.readyState === WebSocket.OPEN && clientInfo?.id !== excludeId) {
client.send(data);
}
});
}
wss.on('connection', (ws) => {
const clientId = Date.now().toString();
let username = `User${clientId.slice(-4)}`;
clients.set(ws, { id: clientId, username });
// 입장 알림
broadcast({
type: 'system',
message: `${username}님이 입장했습니다.`
});
// 현재 참가자 수 전송
ws.send(JSON.stringify({
type: 'info',
userCount: wss.clients.size
}));
ws.on('message', (data) => {
try {
const message = JSON.parse(data);
switch (message.type) {
case 'setUsername':
const oldName = username;
username = message.username;
clients.get(ws).username = username;
broadcast({
type: 'system',
message: `${oldName}님이 ${username}(으)로 이름을 변경했습니다.`
});
break;
case 'chat':
broadcast({
type: 'chat',
username,
message: message.text,
timestamp: new Date().toISOString()
});
break;
case 'typing':
broadcast({
type: 'typing',
username
}, clientId);
break;
}
} catch (e) {
console.error('메시지 파싱 오류:', e);
}
});
ws.on('close', () => {
clients.delete(ws);
broadcast({
type: 'system',
message: `${username}님이 퇴장했습니다.`
});
});
});
console.log('채팅 서버 실행 중: ws://localhost:8080');
6. 방(Room) 기능
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// 방 관리
const rooms = new Map();
function joinRoom(ws, roomId) {
if (!rooms.has(roomId)) {
rooms.set(roomId, new Set());
}
rooms.get(roomId).add(ws);
ws.roomId = roomId;
}
function leaveRoom(ws) {
if (ws.roomId && rooms.has(ws.roomId)) {
rooms.get(ws.roomId).delete(ws);
if (rooms.get(ws.roomId).size === 0) {
rooms.delete(ws.roomId);
}
}
}
function broadcastToRoom(roomId, message, excludeWs = null) {
if (!rooms.has(roomId)) return;
rooms.get(roomId).forEach((client) => {
if (client !== excludeWs && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
}
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const message = JSON.parse(data);
switch (message.type) {
case 'join':
leaveRoom(ws);
joinRoom(ws, message.roomId);
ws.send(JSON.stringify({
type: 'joined',
roomId: message.roomId
}));
broadcastToRoom(message.roomId, {
type: 'userJoined',
userId: ws.id
}, ws);
break;
case 'message':
broadcastToRoom(ws.roomId, {
type: 'message',
text: message.text,
userId: ws.id
});
break;
case 'leave':
broadcastToRoom(ws.roomId, {
type: 'userLeft',
userId: ws.id
}, ws);
leaveRoom(ws);
break;
}
});
ws.on('close', () => {
if (ws.roomId) {
broadcastToRoom(ws.roomId, {
type: 'userLeft',
userId: ws.id
});
leaveRoom(ws);
}
});
});
7. 인증
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');
const url = require('url');
const SECRET = 'your-secret-key';
const wss = new WebSocket.Server({
port: 8080,
verifyClient: (info, callback) => {
// URL에서 토큰 추출
const params = new URLSearchParams(url.parse(info.req.url).query);
const token = params.get('token');
if (!token) {
callback(false, 401, 'Unauthorized');
return;
}
try {
const decoded = jwt.verify(token, SECRET);
info.req.user = decoded;
callback(true);
} catch (e) {
callback(false, 401, 'Invalid token');
}
}
});
wss.on('connection', (ws, req) => {
// 인증된 사용자 정보
const user = req.user;
console.log(`${user.username} 연결됨`);
ws.on('message', (message) => {
console.log(`${user.username}: ${message}`);
});
});
8. 핑/퐁 (연결 유지)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// 연결 상태 확인 간격 (30초)
const HEARTBEAT_INTERVAL = 30000;
function heartbeat() {
this.isAlive = true;
}
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', heartbeat);
ws.on('message', (message) => {
console.log('받은 메시지:', message.toString());
});
});
// 주기적으로 연결 상태 확인
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
console.log('응답 없는 클라이언트 종료');
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, HEARTBEAT_INTERVAL);
wss.on('close', () => {
clearInterval(interval);
});
9. 바이너리 데이터 전송
const WebSocket = require('ws');
const fs = require('fs');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
// 바이너리 타입 설정
ws.binaryType = 'arraybuffer';
// 파일 전송
ws.on('message', (data, isBinary) => {
if (isBinary) {
// 바이너리 데이터 처리
const buffer = Buffer.from(data);
fs.writeFileSync('received-file.bin', buffer);
ws.send('파일 수신 완료');
} else {
// 텍스트 메시지 처리
console.log('텍스트:', data.toString());
}
});
// 이미지 전송
const imageBuffer = fs.readFileSync('image.png');
ws.send(imageBuffer);
});
10. 재연결 로직 (클라이언트)
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.reconnectInterval = options.reconnectInterval || 3000;
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
this.reconnectAttempts = 0;
this.handlers = { open: [], message: [], close: [], error: [] };
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = (e) => {
console.log('연결됨');
this.reconnectAttempts = 0;
this.handlers.open.forEach(h => h(e));
};
this.ws.onmessage = (e) => {
this.handlers.message.forEach(h => h(e));
};
this.ws.onclose = (e) => {
console.log('연결 종료');
this.handlers.close.forEach(h => h(e));
this.reconnect();
};
this.ws.onerror = (e) => {
console.error('에러');
this.handlers.error.forEach(h => h(e));
};
}
reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('최대 재연결 시도 횟수 초과');
return;
}
this.reconnectAttempts++;
console.log(`재연결 시도 ${this.reconnectAttempts}...`);
setTimeout(() => {
this.connect();
}, this.reconnectInterval);
}
on(event, handler) {
if (this.handlers[event]) {
this.handlers[event].push(handler);
}
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
}
}
close() {
this.ws.close();
}
}
// 사용
const ws = new ReconnectingWebSocket('ws://localhost:8080');
ws.on('message', (e) => {
console.log('메시지:', e.data);
});
결론
WebSocket은 실시간 양방향 통신이 필요한 애플리케이션에 필수적인 프로토콜입니다. Node.js의 ws 라이브러리로 쉽게 WebSocket 서버를 구축할 수 있습니다. 브로드캐스트, 방 기능, 인증, 핑/퐁 연결 유지 등을 구현하여 안정적인 실시간 서비스를 만들 수 있습니다. 프로덕션에서는 Socket.IO나 기타 고수준 라이브러리를 사용하면 더 많은 기능을 활용할 수 있습니다.
728x90
반응형
'Node.js' 카테고리의 다른 글
| Node.js의 인증 및 인가(Authentication and Authorization) (0) | 2026.03.13 |
|---|---|
| Node.js의 GraphQL API 만들기 (0) | 2026.03.12 |
| Node.js의 RESTful API 만들기 (0) | 2026.03.12 |
| Node.js의 서버 사이드 렌더링(SSR) (0) | 2026.03.11 |
| Node.js의 Fastify 프레임워크 (1) | 2026.03.11 |