목차

🔌 Socket.IO 실시간 통신

Phaser Baduk Metaverse 프로젝트의 Socket.IO를 사용한 실시간 통신 구현에 대해 설명합니다.


1. Socket.IO란 무엇인가요?

Socket.IO는 웹 브라우저와 서버 간의 실시간 양방향 통신을 가능하게 해주는 JavaScript 라이브러리입니다. 채팅, 게임, 실시간 알림 등에 사용됩니다.

주요 특징:

일반적인 사용 사례:


2. Socket.IO vs 일반 HTTP

HTTP 통신의 한계:

Socket.IO의 장점:


3. 서버 측 구현

1) 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"]
    }
});

설명:

2) 정적 파일 서빙 설정

// 정적 파일 서빙
app.use(express.static(path.join(__dirname, 'public')));

설명:

3) 게임 상태 저장소 설정

// 게임 상태 저장소
const games = new Map();
const players = new Map();

설명:

4) Socket.IO 연결 이벤트 처리

// Socket.IO 이벤트 처리
io.on('connection', (socket) => {
    console.log('새로운 클라이언트 연결:', socket.id);
 
    // 플레이어 정보 저장
    players.set(socket.id, {
        id: socket.id,
        name: 'Anonymous',
        currentGame: null,
        color: null
    });

설명:

5) 게임 참가 이벤트 처리

    // 게임 참가
    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);

설명:

6) 플레이어 추가 및 게임 시작

        // 게임에 플레이어 추가
        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}에 참가했습니다.`);
        }

설명:

7) 관전자 추가

        } else {
            // 관전자로 추가
            game.spectators.push({
                id: socket.id,
                name: playerName || 'Spectator'
            });
 
            socket.join(gameId);
            socket.emit('spectator-joined', {
                gameId: gameId,
                message: '관전자로 참가했습니다.'
            });
 
            console.log(`관전자 ${playerName}이 게임 ${gameId}에 참가했습니다.`);
        }
    });

설명:

8) 게임 이동 처리

    // 게임 이동
    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} 돌을 놓았습니다.`);
    });

설명:

9) 연결 해제 처리

    // 연결 해제
    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);
    });
});

설명:


4. 클라이언트 측 구현

1) Socket.IO 클라이언트 설정

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>

설명:

2) 클라이언트 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 = '서버와의 연결이 끊어짐';
});

설명:

3) 게임 참가 기능

// 게임 참가 함수
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();
});

설명:

4) 게임 이동 처리

// 이동 전송 함수
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);
});

설명:

5) 플레이어 퇴장 처리

// 플레이어 퇴장 처리
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;
});

설명:


5. 실습 예제

1) 간단한 채팅 애플리케이션

서버 코드 (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>

2) 실행 방법

# 필요한 패키지 설치
npm install express socket.io
 
# 서버 실행
node chat-server.js

테스트 방법:


6. 고급 기능

1) 방(Room) 기능

// 방 생성 및 참가
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
        });
    }
});

2) 개인 메시지

// 특정 사용자에게 메시지 전송
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
        });
    }
});

3) 연결 상태 확인

// 연결 상태 확인
setInterval(() => {
    if (!socket.connected) {
        console.log('서버와의 연결이 끊어졌습니다. 재연결 시도 중...');
    }
}, 5000);
 
// 재연결 시도
socket.on('reconnect', () => {
    console.log('서버에 재연결되었습니다.');
});

7. 성능 최적화

1) 이벤트 제한

// 클라이언트당 이벤트 제한
const rateLimit = require('socket.io-rate-limiter');
const limiter = rateLimit({
    points: 10,        // 최대 이벤트 수
    duration: 1,       // 시간 (초)
    errorMessage: '너무 많은 요청이 발생했습니다.'
});
 
io.use(limiter);

2) 메모리 관리

// 연결 해제 시 메모리 정리
socket.on('disconnect', () => {
    // 게임 상태 정리
    if (socket.currentGame) {
        cleanupGame(socket.currentGame);
    }
 
    // 플레이어 정보 정리
    delete players[socket.id];
});

8. 보안 고려사항

1) 입력 검증

// 메시지 길이 제한
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
    });
});

2) 인증

// 토큰 기반 인증
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: '인증에 실패했습니다.' });
    }
});

9. 디버깅 및 로깅

1) 디버그 모드 활성화

// 서버 측 디버그 활성화
const io = socketIo(server, {
    cors: {
        origin: "*",
        methods: ["GET", "POST"]
    },
    debug: true
});

2) 로깅

// 연결 로깅
io.on('connection', (socket) => {
    console.log(`새로운 연결: ${socket.id}`);
 
    socket.on('disconnect', () => {
        console.log(`연결 해제: ${socket.id}`);
    });
 
    // 모든 이벤트 로깅
    socket.onAny((eventName, ...args) => {
        console.log(`이벤트 ${eventName}:`, args);
    });
});

10. 다음 단계

Socket.IO 기본을 배웠다면 다음을 학습해보세요:

추천 학습 순서:

이 페이지는 자동으로 생성되었습니다.