Computer >> 컴퓨터 >  >> 프로그래밍 >> Redis

Node.js, Socket.IO, Redis로 확장 가능한 실시간 멀티플레이어 틱택토 게임 만들기

이 튜토리얼에서는 Node.js, Socket.IO, Redis를 활용해 실시간 멀티플레이어 틱택토(Tic-Tac-Toe) 게임을 만들어 보겠습니다. 두 명의 플레이어가 서로 다른 브라우저 탭에서 접속해 번갈아 가며 수를 두고, 실시간으로 게임 진행 상황이 동기화되는 게임입니다. 특히 Redis를 사용해 여러 WebSocket 서버 간에 게임 상태를 동기화함으로써, 애플리케이션이 수평적으로 확장 가능하도록 설계합니다.

튜토리얼을 끝까지 따라 하면 완전히 작동하는 실시간 게임을 완성할 수 있고, WebSocket과 Redis를 활용해 확장 가능한 실시간 애플리케이션을 구축하는 방법도 탄탄하게 익힐 수 있습니다.

학습 목표

  • Socket.IO를 이용한 실시간 양방향 통신 구현 방법

  • Redis Pub/Sub으로 여러 클라이언트 간 게임 상태를 동기화하는 방법

  • 확장 가능한 WebSocket 서버 아키텍처 구성 방법

사전 준비 사항

시작하기 전에 다음 항목들이 준비되어 있는지 확인하세요.

  • Node.js (v16 이상)

  • Redis

  • Docker (선택 사항 — 컨테이너로 Redis 실행 시)

  • JavaScript, Node.js, WebSocket에 대한 기본 지식

목차

  • 프로젝트 개요

  • 1단계: 개발 환경 설정

  • 2단계: 프로젝트 초기화

  • 3단계: Redis와 함께 WebSocket 서버 구현

  • 4단계: React 프론트엔드 인터페이스 구현

  • 5단계: 애플리케이션 실행

  • 6단계: Redis 메시지 실시간 모니터링

  • 데모

  • 마무리

프로젝트 개요

이번에 만들 실시간 틱택토 게임은 다음과 같은 기능을 제공합니다.

  • 두 명의 플레이어가 접속해 대국을 진행할 수 있습니다.

  • 게임 보드가 서로 다른 브라우저 탭 간에 실시간으로 갱신됩니다.

  • 승자가 결정되거나 보드가 가득 차면 무승부로 판정하여 결과를 알려줍니다.

사용하는 기술 스택은 다음과 같습니다.

  • Node.js + Socket.IO: WebSocket 연결 처리

  • Redis Pub/Sub: 클라이언트 간 게임 상태 동기화 관리

1단계: 개발 환경 설정

Node.js 설치

먼저 시스템에 Node.js가 설치되어 있는지 확인합니다.

node -v

설치되어 있지 않다면 Node.js 공식 사이트에서 다운로드하여 설치하세요.

Redis 설치

Redis는 로컬에 직접 설치하거나 Docker 컨테이너로 실행할 수 있습니다.

macOS (Homebrew 사용)

아래 명령어를 실행하기 전에 Homebrew가 설치되어 있어야 합니다.

brew install redis
brew services start redis

Redis가 정상적으로 실행 중인지 확인합니다.

redis-cli ping

다음과 같은 응답이 나오면 성공입니다.

PONG

Docker로 Redis 실행

docker run --name redis-server -p 6379:6379 -d redis

실행 여부는 아래 명령어로 확인할 수 있습니다.

docker exec -it redis-server redis-cli ping

2단계: 프로젝트 초기화

1. 프로젝트 디렉터리 생성

mkdir tic-tac-toe
cd tic-tac-toe
npm init -y

2. 의존성 설치

npm install express socket.io redis dotenv

3. 환경 변수 설정

프로젝트 루트에 .env 파일을 만들고 다음 내용을 추가합니다.

PORT=3000
REDIS_HOST=localhost
REDIS_PORT=6379

3단계: Redis와 함께 WebSocket 서버 구현

이 단계에서는 Node.js, Socket.IO, Redis를 조합해 실시간 게임 상호작용을 처리하는 WebSocket 서버를 구축합니다. 이 서버는 게임 상태를 관리하고, 플레이어의 착수를 처리하며, Redis Pub/Sub을 통해 여러 클라이언트 간 동기화를 보장합니다.

코드의 각 부분을 하나씩 살펴보며 전체 구조가 어떻게 맞물리는지 이해해 보겠습니다.

서버 코드 작성

server.js 파일을 생성하고 아래 코드를 입력합니다.

import dotenv from 'dotenv';
import express from 'express';
import http from 'http';
import { Server } from 'socket.io';
import { createClient } from 'redis';
dotenv.config(); // .env 파일에서 환경 변수 로드
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
 cors: {
 origin: "https://localhost:5173",
 methods: ["GET", "POST"],
 }
});
  • dotenv: 포트나 키 같은 민감한 정보를 안전하게 관리하기 위해 .env 파일에서 환경 변수를 불러옵니다.

  • express: HTTP 요청을 처리하는 기본 Express 서버를 구성합니다.

  • http: Node 내장 http 모듈로 HTTP 서버를 생성하며, 이 서버를 Socket.IO와 함께 사용해 WebSocket 통신을 처리합니다.

  • Socket.IO: 서버와 클라이언트 간 실시간 양방향 통신을 가능하게 하는 라이브러리입니다.

  • CORS 설정: localhost:5173에서 실행되는 프론트엔드의 교차 출처 요청을 허용합니다.

다음으로, Redis 퍼블리셔(publisher)와 서브스크라이버(subscriber) 클라이언트를 생성하기 위해 server.js에 아래 코드를 추가합니다.

// Redis 클라이언트 초기화
const pubClient = createClient();
const subClient = createClient();
await pubClient.connect();
await subClient.connect();

Redis는 연결된 클라이언트들 간의 실시간 데이터 동기화를 담당합니다.

  • pubClient: 메시지(게임 상태 업데이트 등)를 발행(publish)하는 데 사용됩니다.

  • subClient: 메시지를 구독(subscribe)하여 업데이트를 수신 대기합니다.

  • connect(): Redis 서버와의 연결을 설정합니다.

이 패러다임에서 한 클라이언트는 업데이트를 발행하고, 다른 클라이언트는 업데이트를 구독합니다. 구독 모드의 Redis 클라이언트는 메시지 수신만 가능하기 때문에, 클라이언트를 분리하면 블로킹(blocking) 문제를 피할 수 있습니다.

게임 업데이트를 위한 Redis 채널을 구독하려면 server.js에 아래 코드를 추가합니다.

// 게임 업데이트용 Redis 채널 구독
await subClient.subscribe('game-moves', (message) => {
 gameState = JSON.parse(message);
 io.emit('gameState', gameState);
});
  • subClient.subscribe: game-moves 채널의 메시지를 수신 대기합니다.

  • 플레이어가 새 수를 둘 때마다 Redis에서 게임 상태가 갱신되고, 연결된 모든 클라이언트에 새 상태가 전달됩니다.

  • message 매개변수에는 문자열 형태의 게임 상태가 담겨 있으며, 이를 JavaScript 객체로 파싱한 후 Socket.IO로 갱신된 상태를 브로드캐스트합니다.

다음은 게임 상태와 관련 함수를 정의하는 코드입니다. server.js에 추가하세요.

// 초기 게임 상태 정의
let gameState = {
 board: Array(9).fill(null),
 xIsNext: true,
};
// 게임 리셋 함수
function resetGame() {
 gameState = {
 board: Array(9).fill(null),
 xIsNext: true,
 };
}
  • gameState: 현재 보드 상태와 누구 차례인지(xIsNext)를 추적합니다.

    • 보드는 9개 칸으로 이루어진 배열로 표현되며, 각 칸에는 'X', 'O' 또는 null이 들어갑니다.

    • xIsNext 플래그는 어느 플레이어의 차례인지 결정합니다.

  • resetGame(): 보드와 차례 표시를 초기 상태로 되돌려 새 게임을 시작할 수 있도록 합니다.

WebSocket 연결을 처리하려면 아래 코드를 server.js에 추가합니다.

io.on('connection', (socket) => {
 console.log('New client connected:', socket.id);
 // 새로 연결된 클라이언트에게 현재 게임 상태 전송
 socket.emit('gameState', gameState);
  • io.on('connection') 이벤트는 새 클라이언트가 접속할 때 발생합니다.

  • socket.id: 접속한 각 클라이언트의 고유 식별자입니다.

  • 새 클라이언트가 현재 보드를 바로 볼 수 있도록, 접속 즉시 현재 gameState를 전송합니다.

플레이어의 착수를 처리하는 코드입니다.

 // 플레이어 착수 처리
 socket.on('makeMove', (index) => {
 // 이미 채워진 칸이거나 게임이 종료된 경우 착수 방지
 if (gameState.board[index] || calculateWinner(gameState.board)) return;
 // 보드 갱신 및 차례 변경
 gameState.board[index] = gameState.xIsNext ? 'X' : 'O';
 gameState.xIsNext = !gameState.xIsNext;
 // 갱신된 게임 상태를 Redis에 발행
 pubClient.publish('game-moves', JSON.stringify(gameState));
 io.emit('gameState', gameState);
 });
  • makeMove: 플레이어가 칸을 클릭하면 발생하는 이벤트입니다.

    • 유효성 검사: 해당 칸이 이미 차 있거나 게임이 종료되었는지 먼저 확인합니다.

    • 게임 상태 갱신: 착수가 유효하면 보드를 업데이트하고 차례를 넘깁니다.

  • 갱신된 게임 상태는 다음과 같이 처리됩니다.

    1. Redis에 발행: 서버의 모든 인스턴스가 동일한 상태를 유지하도록 보장합니다.

    2. 모든 클라이언트에 브로드캐스트: 모든 플레이어의 게임 보드가 즉시 갱신됩니다.

게임 재시작을 처리하는 코드입니다.

// 게임 재시작 처리
socket.on('restartGame', () => {
 resetGame();
 io.emit('gameState', gameState);
});

클라이언트 연결 종료를 처리하는 코드입니다.

 socket.on('disconnect', () => {
 console.log('Client disconnected:', socket.id);
 });
});

마지막으로 게임 로직을 처리하는 함수들을 server.js에 추가합니다.

// 승자 확인 함수
function calculateWinner(board) {
 const lines = [
 [0, 1, 2], [3, 4, 5], [6, 7, 8],
 [0, 3, 6], [1, 4, 7], [2, 5, 8],
 [0, 4, 8], [2, 4, 6]
 ];
 for (let [a, b, c] of lines) {
 if (board[a] && board[a] === board[b] && board[a] === board[c]) {
 return board[a];
 }
 }
 return null;
}
function isBoardFull(board) {
 return board.every((cell) => cell !== null);
}
  • calculateWinner(): 보드에 승리 조합이 있는지 확인합니다.

  • isBoardFull(): 모든 칸이 채워졌는지, 즉 무승부인지 확인합니다.

4단계: React 프론트엔드 인터페이스 구현

이 단계에서는 틱택토 게임을 위한 간단하고 인터랙티브한 React 프론트엔드를 만듭니다. 플레이어가 WebSocket 서버에 접속해 수를 두고, 게임 보드가 실시간으로 갱신되는 것을 볼 수 있습니다.

App.jsx에 다음 코드를 추가합니다.

import React, { useEffect, useState } from 'react';
import io from 'socket.io-client';
const socket = io('https://localhost:3000');
function App() {
 const [gameState, setGameState] = useState({
 board: Array(9).fill(null),
 xIsNext: true,
 winner: null
 });
 useEffect(() => {
 socket.on('gameState', (state) => {
 setGameState(state);
 });
 return () => socket.off('gameState');
 }, []);
 const handleClick = (index) => {
 if (gameState.board[index] || gameState.winner) return;
 socket.emit('makeMove', index);
 };
 const renderCell = (index) => (
 <button onClick={() => handleClick(index)}>{gameState.board[index]}</button>
 );
 return (
 <div>
 <h1>Multiplayer Tic-Tac-Toe</h1>
 <div className="board">
 {[...Array(9)].map((_, i) => renderCell(i))}
 </div>
 <button onClick={() => socket.emit('restartGame')}>Restart Game</button>
 </div>
 );
}
export default App;

React 앱의 주요 구성 요소를 정리하면 다음과 같습니다.

  • WebSocket 연결: socket.io-client를 사용해 프론트엔드가 서버에 접속합니다.

  • 상태 관리: 게임 상태(gameState)는 React의 useState로 관리되며 다음을 포함합니다.

    • board (9개 칸)

    • 현재 차례를 나타내는 xIsNext 플래그

    • winner 상태

  • 실시간 업데이트: useEffect 훅이 다음 작업을 수행합니다.

    • 서버로부터 gameState 업데이트를 수신 대기합니다.

    • 변경이 감지되면 로컬 게임 상태를 갱신합니다.

    • 컴포넌트가 언마운트될 때 WebSocket 리스너를 정리(cleanup)합니다.

  • 착수 처리: handleClick 함수가 다음을 수행합니다.

    • 칸이 이미 차 있거나 승자가 결정된 경우 착수를 막습니다.

    • 클릭한 칸의 인덱스와 함께 makeMove 이벤트를 서버로 전송합니다.

  • 게임 보드 렌더링: renderCell 함수가 보드의 각 칸에 대한 버튼을 생성하며, 보드는 3x3 그리드로 표시됩니다.

  • 게임 재시작: "Restart Game" 버튼이 restartGame 이벤트를 발생시켜 모든 플레이어의 게임 보드를 초기화합니다.

  • 사용자 인터페이스: 플레이어가 번갈아 수를 두고 실시간 업데이트를 확인할 수 있는 간결하고 인터랙티브한 레이아웃입니다.

5단계: 애플리케이션 실행

백엔드 시작

백엔드 서버를 시작하려면 새 터미널 창을 열고 다음 명령어를 실행합니다.

cd tic-tac-toe
npm start

프론트엔드 시작

React 프론트엔드 서버를 시작하려면 또 다른 새 터미널 창을 열고 아래 명령어를 실행합니다. 백엔드와 프론트엔드는 동시에 실행되어야 하므로 같은 터미널을 사용하지 마세요.

cd tic-tac-toe-client
npm run dev

게임 접속

브라우저를 열고 다음 주소로 접속합니다.

https://localhost:5173

6단계: Redis 메시지 실시간 모니터링

게임이 실행되는 동안 Redis 메시지를 확인하면 게임 상태가 실시간으로 어떻게 갱신되는지 볼 수 있습니다.

터미널을 열고 다음을 실행합니다.

redis-cli
SUBSCRIBE game-moves

그러면 다음과 같은 게임 업데이트가 표시됩니다.

1) "message"
2) "game-moves"
3) "{\"board\":[\"X\",null,\"O\",null,\"X\",null,null,null,null],\"xIsNext\":false}"

수를 두거나 게임 상태가 변경될 때마다 서버는 갱신된 게임 상태를 game-moves 채널에 발행합니다. redis-cli를 사용하면 게임이 진행되는 동안 이러한 업데이트를 실시간으로 모니터링할 수 있습니다.

데모

데모에서는 틱택토 게임이 로컬에서 실행되는 모습을 볼 수 있으며, 플레이어들이 번갈아 수를 둘 때마다 실시간으로 화면이 갱신되는 과정을 확인할 수 있습니다.

게임플레이를 통해 차례 전환, 보드 갱신, 게임 결과(승자 또는 무승부) 알림 등의 기능이 자연스럽게 작동하는 것을 볼 수 있습니다. 이는 WebSocket 통신을 활용해 부드럽고 인터랙티브한 경험을 제공하는 방식을 잘 보여줍니다.

마무리

축하합니다! Node.js, Socket.IO, Redis를 사용해 실시간 멀티플레이어 틱택토 게임을 성공적으로 완성했습니다. 이번 튜토리얼에서 배운 내용은 다음과 같습니다.

  • Socket.IO를 활용한 실시간 WebSocket 통신

  • Redis Pub/Sub을 활용한 게임 상태 관리

  • React로 반응형 프론트엔드 구축

다음 단계

  • 플레이어 인증 기능 추가

  • 채팅 기능 구현

  • 확장성을 위해 클라우드 프로바이더에 애플리케이션 배포

즐거운 코딩 되세요!

freeCodeCamp의 오픈 소스 커리큘럼은 4만 명 이상의 사람들이 개발자로 취업하는 데 도움을 주었습니다. 무료로 코딩을 배우고 싶다면 지금 바로 시작해 보세요.