Cloudflare를 거친 게임 소켓 핑이 279ms였습니다: 소켓만 DNS 전용 호스트로 빼서 8ms로 줄인 기록
웹 메카 슈팅 RAYFRAME을 배포한 날, Cloudflare 프록시를 거친 경기 핑이 279ms로 나왔습니다. 한국에서 보낸 요청이 미국 데이터센터를 거쳐 춘천 서버로 돌아오는 경로를 확인하고, 게임 소켓만 DNS 전용 호스트로 빼서 8ms로 줄인 과정과 그 대가를 적었습니다.
목차
RAYFRAME은 제가 만들고 있는 웹용 3대3 메카 슈팅입니다. 2026년 10월 10일에 rayframe.flowoodz.com으로 배포했습니다. 서버는 30Hz 권위 서버이고, 클라이언트는 WebSocket 하나로 서버와 경기 상태를 주고받습니다.
배포하고 나서 브라우저로 경기를 돌려 핑을 재 보니 279ms였습니다. 같은 서버에 소켓만 직접 붙였더니 8ms가 나왔습니다. 서버는 그대로였고, 바뀐 것은 소켓이 지나가는 길 하나뿐이었습니다.
숫자부터 봅니다#
flowoodz.com의 서브도메인은 전부 Cloudflare 프록시를 켜 두는 것이 지금까지의 관례였습니다. RAYFRAME도 처음엔 그대로 올렸고, 배포 당일 두 경로를 나란히 쟀습니다.
| 항목 | 프록시 경유 (rayframe) |
직접 연결 (ws-rayframe) |
|---|---|---|
| 브라우저 경기 핑 | 279ms | 8ms |
| TCP 연결 | 140ms | 7~9ms |
/healthz 첫 바이트 |
1.0~2.6초 | 56~63ms |
| 헤드리스 둘 1대1, 15초 동안 받은 스냅샷 | 286/294 | 300/300 |
마지막 줄은 배포 주소에 헤드리스 클라이언트 두 개를 붙여 1대1 방을 15초 돌리는 smoke 스크립트의 결과입니다. 프록시 경유만 300/300에 못 미쳤고, 오류는 둘 다 0건이었습니다.
원인: 한국에서 보낸 요청이 미국을 돌아옵니다#
Cloudflare를 거친 응답에는 cf-ray 헤더가 붙고, 끝의 세 글자가 요청을 받은 데이터센터의 공항 코드입니다. 배포 당일에는 flowoodz.com의 모든 서브도메인이 -LAX로 끝났습니다. 로스앤젤레스입니다.
서버는 춘천 리전의 VPS에 있습니다. 그러니 소켓 프레임 하나가 한국 브라우저에서 미국 엣지로 갔다가, 다시 태평양을 건너 춘천 서버에 닿습니다. 서버의 응답도 같은 길을 거꾸로 밟습니다. 279ms는 이 왕복 거리에서 나온 숫자로 보고 있습니다.
Cloudflare가 무료 요금제에서 한국 요청을 왜 미국으로 보내는지는 이 글에서 다루지 않습니다. 제가 직접 확인한 것은 cf-ray 값과 위의 시간뿐입니다.
사이트는 두고 소켓만 뺐습니다#
프록시를 통째로 끄는 방법도 있었습니다. 다만 페이지와 자산은 Cloudflare를 거치는 편이 낫다고 판단했습니다. 해시가 붙은 /assets/*는 엣지 캐시에서 HIT로 나갑니다. HTML 첫 바이트가 1초 가까이 걸려도 그건 처음 한 번이고, 조작감과는 상관이 없습니다.
지연이 직접 손맛으로 느껴지는 건 경기 중 오가는 소켓뿐입니다. 그래서 소켓 전용 호스트 ws-rayframe.flowoodz.com을 하나 더 만들었습니다.
- Cloudflare DNS에 같은 서버를 가리키는 A 레코드를 프록시 끔(DNS 전용)으로 추가했습니다.
- Coolify에서는 같은 앱에 이 주소를 둘째 도메인으로 붙였습니다. 인증서는 Traefik이 Let's Encrypt로 받아 왔고(
CN=ws-rayframe.flowoodz.com), Traefik v3.6은 WebSocket 업그레이드를 별도 설정 없이 통과시킵니다.
소켓 주소는 한 곳에서만 고릅니다#
클라이언트에서 소켓 주소를 정하는 코드는 apps/client/src/net/online.ts의 이 두 줄입니다.
const { VITE_WS_URL, VITE_WS_PAGE } = import.meta.env;
this.url = VITE_WS_URL && location.hostname === VITE_WS_PAGE ? VITE_WS_URL : `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;값은 Dockerfile의 빌드 인자 기본값으로 넣습니다.
ARG VITE_WS_PAGE=rayframe.flowoodz.com
ARG VITE_WS_URL=wss://ws-rayframe.flowoodz.com/ws조건을 페이지 호스트에 건 이유가 있습니다. 개발 서버, 로컬 서버, 시험용 컨테이너, 나중에 올릴 다른 포털 빌드는 모두 자기 출처의 /ws를 써야 합니다.
직접 연결 주소를 무조건 쓰게 했다면, 같은 Dockerfile로 만든 시험용 컨테이너의 클라이언트도 운영 서버에 붙었을 겁니다.
따로 열 것이 없었던 이유#
호스트가 둘로 갈라지면 보통 출처 검사나 쿠키 범위에서 막힙니다. 이번에는 세 가지 덕분에 손댈 곳이 없었습니다.
- 서버에 Origin 검사가 없습니다.
- 페이지에 CSP가 없어서 다른 호스트로 소켓을 여는 것을 막는 규칙이 없습니다.
- 플레이어 신원은 쿠키가 아니라 접속 직후 보내는 hello 메시지의 토큰입니다. 그래서 호스트가 달라도 같은 사람으로 인식합니다.
세 번째 덕분에 Origin 검사가 없어도 크게 걱정하지 않습니다. 브라우저가 쿠키를 알아서 실어 보내는 구조가 아니라서, 다른 사이트가 방문자 브라우저로 소켓을 열어도 그 사람의 신원으로 들어올 수는 없습니다.
대가: 서버 IP가 드러납니다#
프록시를 켠 레코드는 DNS 조회에 Cloudflare의 주소만 보여 줍니다. DNS 전용 레코드는 서버의 실제 IP를 그대로 돌려줍니다. ws-rayframe을 조회하면 누구나 VPS 주소를 알 수 있고, Cloudflare를 건너뛰고 서버에 바로 요청을 보낼 수 있습니다.
지금 규모에서는 경기 핑 271ms를 줄이는 이득이 더 크다고 보고 이 대가를 받아들였습니다. 공격이 실제로 들어오면 어떻게 막을지는 아직 미결입니다.
나흘 뒤 다시 재 봤습니다#
2026년 10월 14일에 제 맥에서 같은 두 주소를 다시 쟀습니다. 이날 cf-ray는 -LAX가 아니라 -PDX(포틀랜드)였습니다.
같은 날 앞선 측정에서 허브 flowoodz.com은 -DFW(댈러스)였고, 이 글을 고치는 동안 다시 보니 -SJC(새너제이)였습니다. 도시는 바뀌어도 미국이라는 점은 같았습니다.
| 항목 (5회) | 프록시 경유 | 직접 연결 |
|---|---|---|
| WebSocket 연결 성립 | 1,017~1,781ms | 59~137ms |
| TCP 연결 | 140~161ms | 12~16ms |
/healthz 첫 바이트 |
0.56~1.04초 | 0.06~0.08초 |
WebSocket 연결 시간은 Node의 WebSocket으로 open 이벤트까지 잰 값이고, 나머지는 curl -w의 time_connect와 time_starttransfer입니다. 프록시 경유로는 연결 한 번에 1초 넘게 걸리니, 재접속할 때마다 그만큼 더 기다려야 합니다.
허브에도 같은 길이 남아 있습니다#
이 사이트 flowoodz.com도 같은 프록시 뒤에 있습니다. 같은 날 홈 /health의 첫 바이트는 0.57~1.04초였고 cf-ray는 역시 -PDX였습니다.
허브는 소켓이 아니라 HTML이라서 RAYFRAME과 같은 방법을 쓸 이유는 없습니다. HTML이 엣지에 캐시되면 미국 엣지에서 춘천 서버까지 다녀오는 구간이 빠지는데, 지금 홈 HTML은 cf-cache-status: DYNAMIC으로 나갑니다. 엣지 캐시 규칙을 걸지는 아직 정하지 않았습니다.
남은 확인도 하나 있습니다. ws-rayframe 연결은 데스크톱 Chrome에서만 확인했습니다. 폰 Safari와 안드로이드 Chrome 실기기에서 붙는지 보는 일이 남았습니다.