Socket.IO와 Firebase, 파키스탄 결제 게이트웨이로 실시간 멀티플레이어 게임을 만든 방법

발행: (2026년 6월 11일 AM 05:03 GMT+9)
11 분 소요
원문: Dev.to

Source: Dev.to

Socket.IO, Firebase, 그리고 파키스탄 결제 게이트웨이로 실시간 멀티플레이어 게임을 만든 이야기

작성자: Faiz Ullah — 풀스택 개발자이자 DG Technology 설립자

멀티플레이어 게임을 떠올리면 대형 스튜디오와 거대한 팀을 생각하곤 합니다. 하지만 실시간으로 돈을 주고받고, 치트 방지가 가능한 멀티플레이어 플랫폼은 아키텍처를 깊이 이해하는 한 명의 엔지니어가 충분히 만들 수 있습니다. 여기서는 Ludo Battle이라는 실시간 멀티플레이어 루도 토너먼트 플랫폼을 처음부터 끝까지 직접 구축한 과정을 소개합니다. 웹소켓 게임 엔진, 안드로이드 APK, 결제 연동까지 모두 포함합니다.

본 글에서는 어려웠던 부분, 중요한 설계 결정, 그리고 실시간 애플리케이션 전반에 적용 가능한 교훈을 짚어보겠습니다.

목표

  • 2~4명의 플레이어가 서브초 단위 동기화로 실시간 루도 경기 진행
  • 로컬 결제 게이트웨이를 통해 실제 금액 입출금 가능
  • 치트 불가 – 주사위, 움직임, 결과 모두 조작 불가
  • 웹과 네이티브 안드로이드 앱 모두에서 동작
  • 파키스탄 모바일 네트워크(엄격한 NAT와 불안정한 연결)에서도 정상 작동

마지막 제약이 많은 설계 결정을 이끌어냈습니다. 이상적인 환경을 위한 개발은 쉽지만, 파키스탄의 실제 4G 환경을 겨냥한 개발이 진정한 엔지니어링이었습니다.

가장 중요한 원칙

실제 금전이 오가는 게임에서 가장 중요한 원칙: 클라이언트는 렌더링만 담당하고, 결정권은 절대 갖지 않는다.

브라우저가 주사위 결과를 스스로 결정한다면, 치터는 개발자 도구를 열어 매번 6을 굴릴 수 있습니다. 따라서 Ludo Battle에서는 서버가 모든 게임 상태의 진실된 소스가 됩니다. 클라이언트는 “주사위를 굴리고 싶다”, “이 토큰을 움직이고 싶다”와 같은 의도만 전송하고, 실제 결과는 서버가 판단합니다.

주사위 굴리기 예시 (서버)

import { randomInt } from 'crypto';

function rollDice() {
  return randomInt(1, 7);  // 1–6, cryptographically secure
}

Math.random()은 예측 가능하고 악용될 수 있지만, crypto.randomInt는 그렇지 않습니다. 금전이 걸린 상황에서는 이 차이가 매우 중요합니다.

움직임 검증

플레이어가 game:move 이벤트를 보내면 서버는 현재 보드 상태에서 가능한 움직임을 계산하고, 불법·불가능한 움직임은 거부합니다. 클라이언트가 불법적인 움직임을 강제로 적용할 방법이 없습니다.

Socket.IO 기반 실시간 통신

전체 실시간 경험은 Socket.IO 위에서 이루어집니다. 이벤트를 명확한 네임스페이스로 구분해 코드 가독성을 유지했습니다.

room:create   room:join   room:leave   room:spectate
game:roll     game:move
chat:send     chat:msg
voice:join    voice:signal   voice:leave

room은 바로 Socket.IO 과 매핑되므로, 특정 플레이어에게만 상태나 채팅을 전송하는 것이 한 줄 코드로 가능합니다.

io.to(`room:${roomId}`).emit('chat:msg', message);

이 구조 덕분에 게임 로직, 채팅, 음성 통신이 깨끗하게 분리되면서도 같은 연결을 공유합니다. 별도의 소켓을 만들 필요도, 불필요한 오버헤드도 없습니다.

P2P 음성 채팅

플레이어가 게임 중에 대화하기를 원했습니다. 서버를 거쳐 음성을 라우팅하는 방식은 비용도 많이 들고 지연도 큽니다. 대신 WebRTC 기반 P2P 방식을 채택했습니다. 서버는 단지 시그널링만 담당하고, 실제 오디오 스트림은 플레이어 간에 직접 흐릅니다.

문제점 & 해결

대부분 파키스탄 모바일 사용자는 엄격한 NAT 뒤에 있어 직접 P2P 연결이 차단됩니다. 이를 해결하기 위해 TURN 서버를 도입해 직접 연결이 불가능할 때만 중계하도록 했습니다. 세션당 TURN 인증 정보를 받아와서 다음과 같이 흐름을 구성했습니다.

  1. 직접 연결 시도 (STUN)
  2. 차단되면 TURN을 통해 중계
  3. 어느 경우든 플레이어는 대화 가능

이 차이가 “내 컴퓨터에서는 동작한다”는 음성 채팅과, 남부 펀자브의 농부가 4G에서 실제로 사용할 수 있는 음성 채팅을 구분합니다.

결제 흐름 (JazzCash, EasyPaisa)

돈을 다루면 위험도 따라옵니다. 저는 호스팅된 리다이렉트 결제 흐름을 선택해 카드·지갑 정보가 서버에 절대 저장되지 않도록 했습니다.

흐름

  1. 사용자가 “입금” 버튼을 누른다.
  2. 백엔드가 서명된 페이로드를 생성해 반환한다.
  3. 사용자는 JazzCash/EasyPaisa의 보안 페이지로 리다이렉트돼 승인을 진행한다.
  4. 결제 게이트웨이가 콜백을 내 서버에 전송한다.
  5. 서버는 HMAC 서명을 검증한 뒤, 실제로 1루피를 입금한다.

HMAC 검증 단계가 핵심입니다. 서버는 콜백이 진짜 게이트웨이에서 온 것인지, 누군가 위조한 것이 아닌지를 확인합니다. 검증을 생략하면 플랫폼이 금방 고갈됩니다.

테스트 모드

게이트웨이 인증 정보가 없을 때는 입금이 즉시 반영되도록 하여, 실제 돈을 쓰지 않고도 로컬에서 전체 경제 흐름을 테스트할 수 있었습니다.

프론트엔드 & 안드로이드 배포

프론트엔드는 React + TypeScript + Vite로 구현했습니다. 전체를 네이티브 안드로이드 앱으로 만들고 싶었지만, 코드를 전면 재작성하고 싶지는 않았습니다. 그래서 Capacitor를 사용해 웹 빌드를 실제 APK로 래핑했습니다. 이를 통해 Play Store 심사 없이 직접 배포가 가능했습니다.

안드로이드 특수 이슈

안드로이드는 보안되지 않은 WebSocket 연결을 차단합니다. 따라서 앱은 반드시 HTTPS 백엔드와 통신해야 하며, 그렇지 않으면 APK 내부에서 소켓이 조용히 실패합니다. 이런 플랫폼‑특화 문제를 발견하고 해결하는 과정이 실제 배포 경험을 쌓게 해줍니다.

핵심 교훈

  • 클라이언트를 절대 신뢰하지 말라. 이 원칙 하나만으로도 많은 공격을 차단할 수 있습니다.
  • 최악의 네트워크를 가정하고 설계하라. 파키스탄 4G를 기준으로 만든 앱은 어디서든 견고합니다.
  • 보안은 사후에 추가하는 기능이 아니다. crypto.randomInt, HMAC 검증, 서버‑주도 로직은 처음부터 설계에 포함돼야 합니다.
  • 관심사 분리가 확장성을 보장한다. 깔끔한 이벤트 네임스페이스와 계층화된 로직 덕분에 복잡한 실시간 시스템도 유지보수가 쉽습니다.
LayerTechnology
FrontendReact, TypeScript, Vite, Tailwind, Zustand
BackendNode.js, Express, Socket.IO
AuthFirebase Phone OTP + JWT
PaymentsJazzCash, EasyPaisa (HMAC‑verified)
VoiceWebRTC + TURN
MobileCapacitor → Android APK

Ludo Battle를 만들면서 깨달은 점은, ‘데모’와 ‘제품’ 사이의 차이는 사용자가 절대 보지 못하는 부분—즉, 안티치트, 결제 검증, 네트워크 복원력—에 있다는 것입니다. 누구든 게임 보드 자체는 그릴 수 있지만, 실제 네트워크 환경에서 공정하고, 안전하며, 신뢰할 수 있게 만드는 것이 진짜 작업입니다.

실시간, 금전 처리, 혹은 모바일 프로젝트를 진행 중이거나 아키텍처에 대해 이야기하고 싶다면 언제든지 연락 주세요.


Faiz Ullah
DG Technology
faizullah.pk · 📧 work@faizullah.pk · 💻 github.com/faizullahpk

이 글이 도움이 되었다면 여기와 GitHub에서 저

0 조회
Back to Blog

관련 글

더 보기 »

Eidentic 소개

Today we're releasing Eidentic, an open-source TypeScript SDK for building AI agents with self-improving memory and the production fundamentals built in — not b...

Typescript의 타입

Introdução Tipos são uma forma de definir a “forma” ou o contrato dos dados que estamos usando no código. Pensando em Javascript puro, ele é dinâmico: você pode...