728x90
반응형
1. Socket.IO란
Socket.IO는 실시간 양방향 이벤트 기반 통신을 위한 라이브러리입니다. WebSocket을 기반으로 하지만, 폴백 메커니즘을 제공하여 WebSocket을 지원하지 않는 환경에서도 동작합니다.
1.1 Socket.IO vs WebSocket
Socket.IO 장점:
- 자동 재연결
- 폴백 메커니즘 (long-polling 등)
- 방(Room) 기능 내장
- 네임스페이스
- 이벤트 기반 통신
- 브로드캐스트 기능
- 바이너리 스트리밍
2. 설치 및 기본 설정
npm install socket.io socket.io-client
2.1 기본 서버
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
origin: 'http://localhost:3000',
methods: ['GET', 'POST']
}
});
io.on('connection', (socket) => {
console.log('클라이언트 연결:', socket.id);
// 이벤트 수신
socket.on('message', (data) => {
console.log('메시지:', data);
});
// 이벤트 전송
socket.emit('welcome', { message: '환영합니다!' });
// 연결 종료
socket.on('disconnect', (reason) => {
console.log('연결 종료:', socket.id, reason);
});
});
httpServer.listen(3000, () => {
console.log('서버 실행 중: http://localhost:3000');
});
2.2 클라이언트
// Node.js 클라이언트
const { io } = require('socket.io-client');
const socket = io('http://localhost:3000');
socket.on('connect', () => {
console.log('연결됨:', socket.id);
socket.emit('message', { text: '안녕하세요!' });
});
socket.on('welcome', (data) => {
console.log('서버 메시지:', data.message);
});
socket.on('disconnect', () => {
console.log('연결 종료');
});
<!-- 브라우저 클라이언트 -->
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
socket.on('connect', () => {
console.log('연결됨:', socket.id);
});
socket.on('welcome', (data) => {
console.log('환영 메시지:', data.message);
});
// 메시지 전송
function sendMessage(text) {
socket.emit('message', { text });
}
</script>
3. 이벤트
3.1 이벤트 전송
// 서버
io.on('connection', (socket) => {
// 특정 클라이언트에게 전송
socket.emit('event', { data: 'value' });
// 모든 클라이언트에게 전송 (자신 포함)
io.emit('event', { data: 'value' });
// 자신을 제외한 모든 클라이언트
socket.broadcast.emit('event', { data: 'value' });
});
3.2 이벤트 수신과 응답
// 서버 - 콜백으로 응답
io.on('connection', (socket) => {
socket.on('getData', (params, callback) => {
const data = { result: 'success', items: [1, 2, 3] };
callback(data);
});
});
// 클라이언트 - 콜백 받기
socket.emit('getData', { id: 1 }, (response) => {
console.log('응답:', response);
});
3.3 async/await 사용
// 서버
io.on('connection', (socket) => {
socket.on('fetchUser', async (userId, callback) => {
try {
const user = await findUser(userId);
callback({ success: true, user });
} catch (err) {
callback({ success: false, error: err.message });
}
});
});
// 클라이언트 (Promise 래핑)
function emitAsync(event, data) {
return new Promise((resolve) => {
socket.emit(event, data, resolve);
});
}
const response = await emitAsync('fetchUser', 123);
console.log(response);
반응형
4. 방(Rooms)
4.1 방 참가/퇴장
io.on('connection', (socket) => {
// 방 참가
socket.on('joinRoom', (roomId) => {
socket.join(roomId);
console.log(`${socket.id}가 ${roomId} 방에 참가`);
// 방의 다른 사람들에게 알림
socket.to(roomId).emit('userJoined', { id: socket.id });
});
// 방 퇴장
socket.on('leaveRoom', (roomId) => {
socket.leave(roomId);
socket.to(roomId).emit('userLeft', { id: socket.id });
});
// 방에 메시지 전송
socket.on('roomMessage', ({ roomId, message }) => {
io.to(roomId).emit('message', {
from: socket.id,
message
});
});
});
4.2 방 정보 조회
io.on('connection', (socket) => {
// 방의 모든 소켓
socket.on('getRoomMembers', async (roomId, callback) => {
const sockets = await io.in(roomId).fetchSockets();
const members = sockets.map(s => ({ id: s.id }));
callback(members);
});
// 현재 참여 중인 방 목록
socket.on('getMyRooms', (callback) => {
callback(Array.from(socket.rooms));
});
});
5. 네임스페이스
const { Server } = require('socket.io');
const io = new Server(httpServer);
// 기본 네임스페이스 (/)
io.on('connection', (socket) => {
console.log('기본 네임스페이스 연결');
});
// 커스텀 네임스페이스
const chatNs = io.of('/chat');
chatNs.on('connection', (socket) => {
console.log('채팅 네임스페이스 연결');
socket.on('message', (msg) => {
chatNs.emit('message', msg);
});
});
const adminNs = io.of('/admin');
adminNs.use((socket, next) => {
// 관리자 인증
if (socket.handshake.auth.token === 'admin-token') {
next();
} else {
next(new Error('Unauthorized'));
}
});
adminNs.on('connection', (socket) => {
console.log('관리자 네임스페이스 연결');
});
// 클라이언트에서 네임스페이스 연결
const chatSocket = io('/chat');
const adminSocket = io('/admin', {
auth: { token: 'admin-token' }
});
6. 미들웨어
// 연결 미들웨어
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication required'));
}
try {
const user = verifyToken(token);
socket.user = user;
next();
} catch (err) {
next(new Error('Invalid token'));
}
});
// 특정 네임스페이스에만 적용
io.of('/admin').use((socket, next) => {
if (socket.user?.role !== 'admin') {
return next(new Error('Admin access required'));
}
next();
});
io.on('connection', (socket) => {
console.log('인증된 사용자:', socket.user.name);
});
7. 채팅 애플리케이션
7.1 서버
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer);
app.use(express.static('public'));
// 사용자 관리
const users = new Map();
io.on('connection', (socket) => {
console.log('연결:', socket.id);
// 로그인
socket.on('login', ({ username }, callback) => {
if (Array.from(users.values()).includes(username)) {
callback({ success: false, error: '이미 사용 중인 이름입니다' });
return;
}
users.set(socket.id, username);
socket.username = username;
callback({ success: true });
// 입장 알림
socket.broadcast.emit('userJoined', {
username,
users: Array.from(users.values())
});
});
// 채팅 메시지
socket.on('chatMessage', (message) => {
io.emit('chatMessage', {
username: socket.username,
message,
timestamp: new Date().toISOString()
});
});
// 타이핑 표시
socket.on('typing', () => {
socket.broadcast.emit('typing', { username: socket.username });
});
socket.on('stopTyping', () => {
socket.broadcast.emit('stopTyping', { username: socket.username });
});
// 연결 종료
socket.on('disconnect', () => {
const username = users.get(socket.id);
users.delete(socket.id);
if (username) {
io.emit('userLeft', {
username,
users: Array.from(users.values())
});
}
});
});
httpServer.listen(3000);
7.2 클라이언트
<!DOCTYPE html>
<html>
<head>
<title>채팅</title>
<style>
#messages { height: 300px; overflow-y: auto; border: 1px solid #ccc; }
.message { padding: 5px; }
.system { color: gray; font-style: italic; }
</style>
</head>
<body>
<div id="loginForm">
<input type="text" id="username" placeholder="이름 입력">
<button onclick="login()">입장</button>
</div>
<div id="chatRoom" style="display: none;">
<div id="users">접속자: <span id="userList"></span></div>
<div id="messages"></div>
<div id="typing"></div>
<input type="text" id="messageInput" onkeyup="handleTyping(event)">
<button onclick="sendMessage()">전송</button>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
let typingTimeout;
function login() {
const username = document.getElementById('username').value;
socket.emit('login', { username }, (response) => {
if (response.success) {
document.getElementById('loginForm').style.display = 'none';
document.getElementById('chatRoom').style.display = 'block';
} else {
alert(response.error);
}
});
}
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (message) {
socket.emit('chatMessage', message);
input.value = '';
socket.emit('stopTyping');
}
}
function handleTyping(e) {
if (e.key === 'Enter') {
sendMessage();
return;
}
socket.emit('typing');
clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
socket.emit('stopTyping');
}, 1000);
}
socket.on('chatMessage', (data) => {
const messages = document.getElementById('messages');
messages.innerHTML += `
<div class="message">
<strong>${data.username}:</strong> ${data.message}
</div>
`;
messages.scrollTop = messages.scrollHeight;
});
socket.on('userJoined', (data) => {
const messages = document.getElementById('messages');
messages.innerHTML += `
<div class="message system">${data.username}님이 입장했습니다.</div>
`;
document.getElementById('userList').textContent = data.users.join(', ');
});
socket.on('userLeft', (data) => {
const messages = document.getElementById('messages');
messages.innerHTML += `
<div class="message system">${data.username}님이 퇴장했습니다.</div>
`;
document.getElementById('userList').textContent = data.users.join(', ');
});
socket.on('typing', (data) => {
document.getElementById('typing').textContent = `${data.username}님이 입력 중...`;
});
socket.on('stopTyping', () => {
document.getElementById('typing').textContent = '';
});
</script>
</body>
</html>
8. 스케일링 (Redis 어댑터)
npm install @socket.io/redis-adapter redis
const { createClient } = require('redis');
const { createAdapter } = require('@socket.io/redis-adapter');
const { Server } = require('socket.io');
const io = new Server(httpServer);
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
io.adapter(createAdapter(pubClient, subClient));
console.log('Redis 어댑터 연결됨');
});
// 여러 서버 인스턴스에서 동일하게 동작
io.on('connection', (socket) => {
socket.on('message', (msg) => {
// 모든 서버 인스턴스의 클라이언트에게 전송
io.emit('message', msg);
});
});
9. 에러 처리
io.on('connection', (socket) => {
// 이벤트 에러 처리
socket.on('riskyOperation', async (data, callback) => {
try {
const result = await performOperation(data);
callback({ success: true, result });
} catch (err) {
callback({ success: false, error: err.message });
}
});
// 연결 에러
socket.on('error', (err) => {
console.error('소켓 에러:', err);
});
});
// 서버 레벨 에러
io.engine.on('connection_error', (err) => {
console.error('연결 에러:', err);
});
결론
Socket.IO는 실시간 통신을 위한 강력한 라이브러리입니다. 자동 재연결, 방(Room), 네임스페이스, 이벤트 기반 통신 등 WebSocket보다 더 많은 기능을 제공합니다. 미들웨어로 인증을 구현하고, Redis 어댑터로 여러 서버에 스케일링할 수 있습니다. 채팅, 실시간 알림, 협업 도구 등 다양한 실시간 애플리케이션에 적합합니다.
728x90
반응형
'Node.js' 카테고리의 다른 글
| Node.js의 JSON 웹 토큰(JWT) 사용법 (0) | 2026.03.14 |
|---|---|
| Node.js의 인증 및 인가(Authentication and Authorization) (0) | 2026.03.13 |
| Node.js의 웹소켓(WebSocket) 사용법 (0) | 2026.03.13 |
| Node.js의 GraphQL API 만들기 (0) | 2026.03.12 |
| Node.js의 RESTful API 만들기 (0) | 2026.03.12 |