서버리스 실시간 매치메이킹 — Cloudflare Pages Functions · Supabase
여러 명이 실시간으로 붙는 대결 게임을 개인적으로 만들고 있습니다. 플레이어가 “게임 찾기” 를 누르면 비슷한 실력의 상대를 찾아 붙여 줘야 하고, 이걸 매치메이킹이라고 합니다.
매치메이킹은 보통 상시 켜져 있는 서버가 맡아서, 대기열을 들고 있다가 주기적으로 훑어서 짝을 맞추는 구조라 아무도 게임을 안 하는 시간에도 그 서버는 떠 있어야 합니다.
개인 프로젝트에 그걸 24시간 띄워 두고 싶지는 않아서, Cloudflare Pages Functions(요청이 올 때만 잠깐 실행되는 서버리스 함수 환경)와 Supabase(PostgreSQL 에 실시간 구독과 인증을 얹은 백엔드 서비스)로 서버 없이 붙였습니다.
매치메이킹의 핵심은 대기열과 주기적 매칭
플레이어가 “게임 찾기” 를 누르면 대기열(matchmaking_queue) 테이블에 행이 하나 생기고, 2초마다
도는 함수가 대기열을 훑어 짝을 맞춥니다.
플레이어 A "게임 찾기" → 대기열에 INSERT
플레이어 B "게임 찾기" → 대기열에 INSERT
↓ (2초마다)
매칭 함수: 대기열 조회 → 실력순 정렬 → ±범위 안에서 짝 → 매치 생성 → 대기열에서 제거
↓
Supabase Realtime 브로드캐스트 → 두 클라이언트가 매치 화면으로 이동
이 함수를 부르는 건 대기 중인 클라이언트라, 대기열에 들어가는 순간 한 번 부르고 그 뒤로는 짝이 잡힐 때까지 2초 간격으로 다시 부릅니다. 그래서 호출될 때만 실행되는 Cloudflare Pages Functions 에 그대로 올릴 수 있고, 대기열이 비면 부르는 쪽도 없으니 함수는 한 번도 안 뜹니다.
대기가 길어지면 매칭 범위 확대
실력이 정확히 같은 상대는 드물어서 매칭 범위(±rating_range)를 대기 시간에 따라 넓힙니다.
처음에는 “실력 ±50” 안에서만 찾다가, 30초를 기다려도 짝이 없으면 ±100, ±200 으로 넓힙니다. 우선은 최대한 비슷한 상대를 붙이려 하고, 오래 못 찾으면 “덜 비슷해도 일단 붙여주는” 쪽으로 타협하는 겁니다.
너무 엄격하면 매칭이 안 되고 너무 느슨하면 실력 차가 큰 게임이 나오니, 그 사이를 대기 시간으로 조절합니다.
게임 종류마다 인원과 라운드가 달라서 그것도 같이 들고 있어야 합니다.
const GAME_CONFIG = {
game_a: { min_players: 2, max_players: 2, total_rounds: 5 },
game_b: { min_players: 2, max_players: 4, total_rounds: 5 },
game_c: { min_players: 2, max_players: 4, total_rounds: 7 },
}
2인 대결도 있고 최대 4인이 붙는 것도 있어서 대기열도 게임 종류별로 나눠서 매칭합니다.
매칭됐다는 걸 어떻게 알리나 — Supabase Realtime
함수가 짝을 맞춰 매치를 만들어도, 두 플레이어의 브라우저가 그걸 알아야 게임 화면으로 넘어갈 수 있으니, 이걸 알리는 데 Supabase Realtime 을 씁니다.
클라이언트는 매칭 함수를 2초마다 부르는 동안 Realtime 채널도 같이 구독하고 있습니다. 함수가 매치를 만들면 Supabase 가 그 변경을 구독 중인 클라이언트에 밀어주니, 부르는 쪽은 매칭을 돌리기만 하고 “나 매칭됐나?” 는 물어보지 않습니다. 응답을 기다렸다 확인하는 게 아니라 결과가 먼저 도착하는 구조입니다.
동시에 두 번 매칭되는 걸 막기
여기서 한 번 사고가 나기도 했습니다. 대기 중인 클라이언트가 저마다 2초 간격으로 부르니 호출이 서로 겹치고 거기에 방금 들어온 플레이어가 바로 부른 호출까지 얹히면, 같은 플레이어가 두 매치에 들어갈 수 있습니다.
막는 자리는 대기열 테이블이고, matchmaking_queue 에 (user_id, game_type_id) 유니크 제약을
걸어 두고 대기열에 들어갈 때는 onConflict 를 지정한 upsert 를 쓰니, 한 사람이 같은 게임
대기열에 두 줄로 들어가지 않습니다.
그리고 매치를 만든 함수가 곧바로 그 플레이어를 대기열에서 지우니, 다음 호출이 대기열을 다시 읽을 때는 이미 없어서 두 번 잡히지 않습니다.
상시 서버 없는 실시간 — 주기 실행 + Realtime 구독
상시 서버 없이 실시간 매치메이킹을 만드는 건 결국 두 가지를 나눠 맡기는 일이었습니다. 대기열 처리는 대기 중인 클라이언트가 2초 간격으로 부르는 함수가 하고, 결과를 알리는 건 Realtime 구독이 합니다. 서버가 계속 돌면서 소켓을 붙들고 있던 자리를 관리형 서비스 두 개로 대신했습니다.
실시간 기능을 붙인다고 소켓 서버부터 띄울 필요는 없습니다. 짧은 주기로 도는 함수로 처리하고 결과는 구독으로 밀어 주면, 상시 서버 없이도 실시간 매칭이 웬만큼은 됩니다.
댓글 0