한국에서 324ms → 14ms: 게임 서버를 서울에 둔 이유
Cloudflare Durable Objects 하나로 전 세계 방을 받으려 했지만 한국에서 왕복 지연이 324ms였다. 월 5달러 서울 VM을 더해 14ms로 줄인 과정과 Salmonholic·AvoidNow의 서버 구성을 정리했다.

Salmonholic은 고양이 열 마리가 한 방에서 싸우는 게임이다. 체력, 안전 구역, 고양이끼리의 전투, 카드 선택은 전부 서버가 결정한다. 브라우저는 내 입력을 먼저 화면에 반영하고, 서버 결과가 오면 그에 맞춰 위치를 보정한다. 이런 구조에서 왕복 지연(RTT)은 곧 손맛이다. 이 글은 한국에서 그 숫자가 324ms로 나왔을 때 무엇을 했는지에 대한 기록이다.
처음 계획: Durable Objects 하나로 전 세계를
처음 설계는 단순했다. Cloudflare Workers의 Durable Objects(DO)에서 방 하나를 객체 하나로 만든다. 방 객체가 초당 15번 게임 상태를 계산하고 참가자들에게 보낸다. 서버를 따로 관리할 필요가 없고, 이론상 사용자와 가까운 곳에 방이 생긴다.
2026년 9월 21일, DO 방 서버를 처음 배포하고 로컬에서 10봇 경기를 돌렸을 때는 모든 게 좋았다. 서버 틱 간격 p99가 70ms 안쪽이었고, 틱 하나를 계산하는 데 평균 0.15ms밖에 걸리지 않았다. 문제는 서울의 내 컴퓨터에서 실제 배포 서버에 붙었을 때였다.
한국에서는 미국을 거쳐 갔다
서울에서 Godot 테스트 클라이언트로 온라인 경기를 끝까지 돌려 보니 결과가 이랬다.
| 측정 | 값 |
|---|---|
| RTT 중앙값(p50) | 324ms |
| RTT p99 | 440ms |
| 스냅샷 수신 | 초당 15.18회 |
| 보정 오차 평균 | 1.1px |
게임은 돌아갔다. 스냅샷도 제때 왔고 위치 보정도 매끄러웠다. 하지만 누른 입력이 서버에 반영되기까지 0.3초가 넘는다는 건, 다른 고양이와 맞붙는 순간 억울함이 생긴다는 뜻이었다.
원인을 찾느라 요청이 어느 Cloudflare 데이터센터에서 처리되는지 확인해 보니 한국이 아니라 미국 로스앤젤레스였다. 한국 IP인데도 일반 요금제 사이트는 서울 데이터센터 대신 미국이나 일본을 거쳐 가는 경우가 있다. 이 경우 Worker 자체가 이미 태평양 건너에 있으니, 방 객체를 아시아에 두든 어디에 두든 목표였던 p95 80ms 이하는 불가능했다. 방 위치 힌트를 아시아로 줘도 오히려 미국과 아시아 사이 왕복이 한 번 더 붙었다.
방 서버만 서울로 옮겼다
선택지는 몇 가지였다. 요금제를 바꾸는 건 1인 개발 규모에 맞지 않았다. 대신 실시간 방 서버만 서울에 두기로 했다. 게임 파일(웹 빌드)은 계속 Cloudflare에서 내려받고, 경기 중 오가는 메시지만 서울 VM이 직접 받는 구조다.
- 서버: AWS Lightsail 서울 리전의 가장 작은 인스턴스(메모리 512MB, 월 5달러). 전송량 1TB가 포함돼 있고, 기록상 방 하나가 하루 약 1.7GB를 쓰므로 여유가 있다.
- 코드: Node 22용 방 서버를 새로 만들되, 게임 규칙·시뮬레이션·메시지 코덱 파일은 DO 서버와 같은 파일을 그대로 import 했다. 복사본이 없으니 규칙을 한 번 고치면 두 서버가 함께 바뀐다. 번들 크기는 74KB였다.
- DO는 폴백으로 남김: 해외 이용자는 계속 DO를 쓰고, 서울 VM이 문제가 생기면 DO로 넘어간다.
같은 날 밤 서울 VM에 올리고 같은 컴퓨터에서 다시 쟀다.
| 측정 | 미국 경유 DO | 서울 VM |
|---|---|---|
| RTT p50 | 324ms | 14ms |
| RTT p99 | 440ms | 83ms |
| 접속 후 첫 응답 | 1.7초 | 0.23초 |
p50 기준으로 약 23분의 1이다. 서버 규칙은 한 줄도 바꾸지 않았고, 서버가 놓인 자리만 바꿨다.
서울 서버에서 진행 중인 온라인 경기. 이제는 RTT가 150ms를 넘을 때만 화면에 지연 경고를 띄운다
두 서버 중 어디로 갈지는 브라우저가 고른다
서버가 둘이 되자 “누구를 어디로 보낼지”가 새 문제가 됐다. 처음에는 두 서버에 동시에 요청을 보내 먼저 답한 쪽을 골랐다. 그런데 첫 응답 한 번은 우연에 좌우된다. 0.16.4에서 방식을 바꿨다(커밋 83283e9).
- 서울 서버와 DO에 각각 세 번씩 재고 중앙값을 쓴다.
- 서울이 DO보다 30ms 이상 느리지 않으면 서울을 고른다.
- 서울 측정이 실패하면 한 번 더 시도한다.
그리고 0.11.2부터는 고른 서버가 가득 찼거나(503) 응답이 없으면, 오프라인으로 떨어지기 전에 다른 쪽 서버로 한 번 재시도한다. 해외 DO는 접속한 나라와 대륙을 보고 아시아·유럽·북미·남미 네 풀 중 하나에 방을 잡는다. 한국 풀은 서울 VM만 맡는다.
AvoidNow도 같은 구조로 갔다
이 경험은 두 번째 게임 AvoidNow에도 그대로 옮겼다. 9월 26일 첫 배포 커밋(cda1fc7)부터 웹 빌드는 Cloudflare, 한국 방은 Salmonholic과 분리된 별도 서울 VM, 해외 방은 DO였다. 클라이언트가 각 서버를 세 번씩 재고 서울을 30ms 우대하는 것도 같다.
AvoidNow도 한국은 서울 VM, 해외는 Durable Objects로 방을 나눈다
AvoidNow에서는 한 걸음 더 나가 월 5달러 VM에 몇 명이 들어가는지 부하 테스트를 했다(커밋 4eaedb8). TLS 처리까지 포함해 사람 한 명당 vCPU 약 0.4%였다. 원래 설정은 방 상한이 너무 낮아 혼자 하는 이용자를 금방 막는 반면, 연결 한도는 VM이 감당할 수 있는 양보다 높았다. 그래서 측정값에 맞춰 두 상한을 다시 잡고, 가득 차면 다른 지역으로 넘기게 했다.
정리: 숫자로 본 결정
| 항목 | 내용 |
|---|---|
| 문제 | 한국 → 미국 경유, RTT p50 324ms |
| 해결 | 실시간 방 서버만 서울 VM(월 5달러) |
| 결과 | RTT p50 14ms, p99 83ms |
| 유지 | 해외는 DO 지역 풀, 서울 장애 시 DO 폴백 |
| 재사용 | AvoidNow에 같은 구조 + 부하 테스트로 상한 설정 |
돌아보면 가장 잘한 선택은 규칙 코드를 한 벌만 둔 것이다. 서버가 두 종류여도 게임은 하나라서, 밸런스를 고칠 때마다 두 서버를 따로 맞출 필요가 없었다. 그 덕에 이후 나흘 동안 버전을 서른 번 넘게 올리는 동안에도, 릴리스마다 같은 재접속 검사 27개를 두 서버에 똑같이 돌리는 것으로 확인을 끝낼 수 있었다.
한국에서 14ms로 도는 경기는 Salmonholic 게임 페이지에서 바로 해 볼 수 있다.


