Phaser Baduk Metaverse 프로젝트의 Socket.IO
를 사용한 실시간 통신 구현에 대해 설명합니다.
Socket.IO
는 웹 브라우저와 서버 간의 실시간 양방향 통신을 가능하게 해주는 JavaScript
라이브러리입니다. 채팅, 게임, 실시간 알림 등에 사용됩니다.
주요 특징:
WebSocket
, HTTP Long Polling
등).Room
) 기능으로 그룹 통신 가능.일반적인 사용 사례:
HTTP 통신의 한계:
Socket.IO의 장점:
먼저 필요한 모듈들을 불러오고 서버를 설정합니다:
const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const path = require('path'); const app = express(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: "*", methods: ["GET", "POST"] } });
설명:
express
- 웹 서버 프레임워크http
- HTTP
서버 생성socket.io
- Socket.IO
서버 생성cors
- 다른 도메인에서의 접근 허용// 정적 파일 서빙 app.use(express.static(path.join(__dirname, 'public')));
설명:
public
폴더의 파일들을 웹에서 접근 가능하게 합니다.HTML
, CSS
, JavaScript
파일들을 클라이언트에게 제공합니다.// 게임 상태 저장소 const games = new Map(); const players = new Map();
설명:
Map
- 키-값 쌍으로 데이터를 저장하는 자료구조입니다.games
- 현재 진행 중인 게임들을 저장합니다.players
- 현재 접속 중인 플레이어들을 저장합니다.// Socket.IO 이벤트 처리 io.on('connection', (socket) => { console.log('새로운 클라이언트 연결:', socket.id); // 플레이어 정보 저장 players.set(socket.id, { id: socket.id, name: 'Anonymous', currentGame: null, color: null });
설명:
io.on('connection')
- 새로운 클라이언트가 연결될 때 실행됩니다.socket.id
- 각 클라이언트의 고유 ID입니다.players.set()
- 플레이어 정보를 저장합니다.// 게임 참가 socket.on('join-game', (data) => { const { gameId, playerName } = data; const player = players.get(socket.id); if (!games.has(gameId)) { games.set(gameId, { id: gameId, players: [], gameState: null, spectators: [], createdAt: Date.now() }); } const game = games.get(gameId);
설명:
socket.on('join-game')
- 클라이언트가 게임 참가 요청을 보낼 때 실행됩니다.data
- 클라이언트가 보낸 데이터 (gameId
, playerName
)입니다.// 게임에 플레이어 추가 if (game.players.length < 2) { const color = game.players.length === 0 ? 'black' : 'white'; player.currentGame = gameId; player.color = color; player.name = playerName || `Player ${color}`; game.players.push({ id: socket.id, name: player.name, color: color }); socket.join(gameId); // 게임 시작 알림 if (game.players.length === 2) { io.to(gameId).emit('game-start', { gameId: gameId, players: game.players }); } else { socket.emit('waiting-for-player', { gameId: gameId, currentPlayers: game.players.length }); } console.log(`플레이어 ${player.name}이 게임 ${gameId}에 참가했습니다.`); }
설명:
socket.join(gameId)
- 특정 게임방에 소켓을 추가합니다.io.to(gameId).emit()
- 특정 방의 모든 클라이언트에게 메시지를 전송합니다.socket.emit()
- 현재 클라이언트에게만 메시지를 전송합니다.} else { // 관전자로 추가 game.spectators.push({ id: socket.id, name: playerName || 'Spectator' }); socket.join(gameId); socket.emit('spectator-joined', { gameId: gameId, message: '관전자로 참가했습니다.' }); console.log(`관전자 ${playerName}이 게임 ${gameId}에 참가했습니다.`); } });
설명:
// 게임 이동 socket.on('make-move', (data) => { const { gameId, x, y, color } = data; const game = games.get(gameId); if (!game) { socket.emit('error', { message: '게임을 찾을 수 없습니다.' }); return; } // 현재 플레이어의 차례인지 확인 const currentPlayer = game.players.find(p => p.id === socket.id); if (!currentPlayer || currentPlayer.color !== color) { socket.emit('error', { message: '당신의 차례가 아닙니다.' }); return; } // 이동을 모든 플레이어에게 전송 io.to(gameId).emit('move-made', { x: x, y: y, color: color, player: currentPlayer.name }); console.log(`${currentPlayer.name}이 (${x}, ${y})에 ${color} 돌을 놓았습니다.`); });
설명:
socket.on('make-move')
- 클라이언트가 이동 요청을 보낼 때 실행됩니다.io.to(gameId).emit()
- 모든 플레이어에게 이동 정보를 전송합니다.// 연결 해제 socket.on('disconnect', () => { console.log('클라이언트 연결 해제:', socket.id); const player = players.get(socket.id); if (player && player.currentGame) { const game = games.get(player.currentGame); if (game) { // 플레이어를 게임에서 제거 game.players = game.players.filter(p => p.id !== socket.id); game.spectators = game.spectators.filter(s => s.id !== socket.id); // 게임이 비어있으면 삭제 if (game.players.length === 0 && game.spectators.length === 0) { games.delete(player.currentGame); console.log(`게임 ${player.currentGame}이 삭제되었습니다.`); } else { // 다른 플레이어들에게 알림 io.to(player.currentGame).emit('player-left', { playerId: socket.id, playerName: player.name }); } } } // 플레이어 정보 삭제 players.delete(socket.id); }); });
설명:
socket.on('disconnect')
- 클라이언트 연결이 끊어질 때 실행됩니다.
HTML
파일에서 Socket.IO
클라이언트를 설정합니다:
<!DOCTYPE html> <html> <head> <title>바둑 게임</title> <script src="/socket.io/socket.io.js"></script> </head> <body> <div id="game-container"> <h1>바둑 게임</h1> <div id="game-info"></div> <div id="game-board"></div> </div> <script src="js/game.js"></script> </body> </html>
설명:
/socket.io/socket.io.js
- Socket.IO
클라이언트 라이브러리입니다.game.js
- 게임 로직을 담은 JavaScript
파일입니다.// Socket.IO 클라이언트 연결 const socket = io(); // 연결 상태 확인 socket.on('connect', () => { console.log('서버에 연결되었습니다.'); document.getElementById('game-info').innerHTML = '서버에 연결됨'; }); socket.on('disconnect', () => { console.log('서버와의 연결이 끊어졌습니다.'); document.getElementById('game-info').innerHTML = '서버와의 연결이 끊어짐'; });
설명:
io()
- Socket.IO
클라이언트 객체를 생성합니다.socket.on('connect')
- 서버에 연결될 때 실행됩니다.socket.on('disconnect')
- 서버와의 연결이 끊어질 때 실행됩니다.// 게임 참가 함수 function joinGame(gameId, playerName) { socket.emit('join-game', { gameId: gameId, playerName: playerName }); } // 게임 참가 이벤트 처리 socket.on('waiting-for-player', (data) => { console.log('다른 플레이어를 기다리는 중...'); document.getElementById('game-info').innerHTML = `게임 ${data.gameId}에 참가했습니다. 다른 플레이어를 기다리는 중...`; }); socket.on('game-start', (data) => { console.log('게임이 시작되었습니다!'); document.getElementById('game-info').innerHTML = `게임이 시작되었습니다! 플레이어: ${data.players.map(p => p.name).join(', ')}`; // 게임 보드 초기화 initializeGameBoard(); });
설명:
socket.emit('join-game')
- 서버에 게임 참가를 요청합니다.socket.on('waiting-for-player')
- 다른 플레이어를 기다리는 상태입니다.socket.on('game-start')
- 게임이 시작될 때 실행됩니다.// 이동 전송 함수 function makeMove(x, y, color) { socket.emit('make-move', { gameId: currentGameId, x: x, y: y, color: color }); } // 이동 수신 처리 socket.on('move-made', (data) => { console.log(`${data.player}이 (${data.x}, ${data.y})에 ${data.color} 돌을 놓았습니다.`); // 게임 보드에 돌 표시 placeStone(data.x, data.y, data.color); // 턴 변경 updateTurn(data.color === 'black' ? 'white' : 'black'); }); // 에러 처리 socket.on('error', (data) => { console.error('에러:', data.message); alert(data.message); });
설명:
socket.emit('make-move')
- 서버에 이동을 요청합니다.socket.on('move-made')
- 다른 플레이어의 이동을 받을 때 실행됩니다.placeStone()
- 게임 보드에 돌을 표시하는 함수입니다.updateTurn()
- 턴을 변경하는 함수입니다.// 플레이어 퇴장 처리 socket.on('player-left', (data) => { console.log(`${data.playerName}이 게임을 떠났습니다.`); document.getElementById('game-info').innerHTML = `${data.playerName}이 게임을 떠났습니다.`; }); // 관전자 참가 처리 socket.on('spectator-joined', (data) => { console.log(data.message); document.getElementById('game-info').innerHTML = data.message; });
설명:
socket.on('player-left')
- 다른 플레이어가 게임을 떠날 때 실행됩니다.socket.on('spectator-joined')
- 관전자로 참가할 때 실행됩니다.
서버 코드 (chat-server.js
):
const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const app = express(); const server = http.createServer(app); const io = socketIo(server); // 정적 파일 서빙 app.use(express.static('public')); // Socket.IO 연결 처리 io.on('connection', (socket) => { console.log('새로운 사용자 연결:', socket.id); // 사용자 입장 socket.on('join', (username) => { socket.username = username; io.emit('user-joined', username); console.log(`${username}이 입장했습니다.`); }); // 메시지 전송 socket.on('message', (message) => { io.emit('message', { username: socket.username, message: message, time: new Date().toLocaleTimeString() }); }); // 연결 해제 socket.on('disconnect', () => { if (socket.username) { io.emit('user-left', socket.username); console.log(`${socket.username}이 퇴장했습니다.`); } }); }); const PORT = 3000; server.listen(PORT, () => { console.log(`채팅 서버가 포트 ${PORT}에서 실행 중입니다.`); });
클라이언트 코드 (public/index.html
):
<!DOCTYPE html> <html> <head> <title>실시간 채팅</title> <script src="/socket.io/socket.io.js"></script> <style> #chat-container { width: 400px; height: 300px; border: 1px solid #ccc; overflow-y: scroll; padding: 10px; } #message-input { width: 300px; margin-top: 10px; } </style> </head> <body> <h1>실시간 채팅</h1> <input type="text" id="username" placeholder="사용자명" /> <button onclick="join()">입장</button> <br><br> <div id="chat-container"></div> <input type="text" id="message-input" placeholder="메시지를 입력하세요" /> <button onclick="sendMessage()">전송</button> <script> const socket = io(); const chatContainer = document.getElementById('chat-container'); const messageInput = document.getElementById('message-input'); function join() { const username = document.getElementById('username').value; if (username.trim()) { socket.emit('join', username); document.getElementById('username').disabled = true; } } function sendMessage() { const message = messageInput.value; if (message.trim()) { socket.emit('message', message); messageInput.value = ''; } } socket.on('user-joined', (username) => { addMessage(`시스템: ${username}이 입장했습니다.`); }); socket.on('user-left', (username) => { addMessage(`시스템: ${username}이 퇴장했습니다.`); }); socket.on('message', (data) => { addMessage(`${data.username}: ${data.message} (${data.time})`); }); function addMessage(message) { const div = document.createElement('div'); div.textContent = message; chatContainer.appendChild(div); chatContainer.scrollTop = chatContainer.scrollHeight; } messageInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { sendMessage(); } }); </script> </body> </html>
# 필요한 패키지 설치 npm install express socket.io # 서버 실행 node chat-server.js
테스트 방법:
http://localhost:3000
접속// 방 생성 및 참가 socket.on('join-room', (roomId) => { socket.join(roomId); socket.roomId = roomId; console.log(`방 ${roomId}에 참가했습니다.`); }); // 방에만 메시지 전송 socket.on('room-message', (message) => { if (socket.roomId) { io.to(socket.roomId).emit('message', { username: socket.username, message: message }); } });
// 특정 사용자에게 메시지 전송 socket.on('private-message', (data) => { const targetSocket = io.sockets.sockets.get(data.to); if (targetSocket) { targetSocket.emit('private-message', { from: socket.username, message: data.message }); } });
// 연결 상태 확인 setInterval(() => { if (!socket.connected) { console.log('서버와의 연결이 끊어졌습니다. 재연결 시도 중...'); } }, 5000); // 재연결 시도 socket.on('reconnect', () => { console.log('서버에 재연결되었습니다.'); });
// 클라이언트당 이벤트 제한 const rateLimit = require('socket.io-rate-limiter'); const limiter = rateLimit({ points: 10, // 최대 이벤트 수 duration: 1, // 시간 (초) errorMessage: '너무 많은 요청이 발생했습니다.' }); io.use(limiter);
// 연결 해제 시 메모리 정리 socket.on('disconnect', () => { // 게임 상태 정리 if (socket.currentGame) { cleanupGame(socket.currentGame); } // 플레이어 정보 정리 delete players[socket.id]; });
// 메시지 길이 제한 socket.on('message', (message) => { if (typeof message !== 'string' || message.length > 1000) { socket.emit('error', { message: '메시지가 너무 깁니다.' }); return; } // XSS 방지 const sanitizedMessage = message.replace(/<script>/gi, ''); io.emit('message', { username: socket.username, message: sanitizedMessage }); });
// 토큰 기반 인증 socket.on('authenticate', (token) => { try { const decoded = jwt.verify(token, process.env.JWT_SECRET); socket.userId = decoded.userId; socket.emit('authenticated'); } catch (error) { socket.emit('error', { message: '인증에 실패했습니다.' }); } });
// 서버 측 디버그 활성화 const io = socketIo(server, { cors: { origin: "*", methods: ["GET", "POST"] }, debug: true });
// 연결 로깅 io.on('connection', (socket) => { console.log(`새로운 연결: ${socket.id}`); socket.on('disconnect', () => { console.log(`연결 해제: ${socket.id}`); }); // 모든 이벤트 로깅 socket.onAny((eventName, ...args) => { console.log(`이벤트 ${eventName}:`, args); }); });
Socket.IO
기본을 배웠다면 다음을 학습해보세요:
추천 학습 순서:
— 이 페이지는 자동으로 생성되었습니다.