카테고리 없음

Cloudflare Workers + D1로 조작할 수 없는 랭킹 웹게임 만들기

IIRN 2026. 11. 4. 02:37

웹게임에 랭킹을 붙이면 반드시 조작 시도가 들어옵니다. 브라우저 개발자 도구로 점수를 바꾸거나, 정답을 미리 읽거나, 스크립트로 수천 판을 돌리는 식입니다. GOOD LUCK을 만들면서 이런 시도를 구조적으로 막기 위해 적용한 방법을 정리합니다. 서버는 Cloudflare Workers, DB는 D1(SQLite)이고 프론트엔드는 빌드 없는 바닐라 자바스크립트입니다.

1. 문 배치는 서버에만 존재한다

가장 중요한 원칙입니다. 문 10개 중 어느 것이 PASS인지는 서버에서 만들고 서버에만 저장합니다. 브라우저가 받는 정보는 "PASS 6개, FAIL 3개, ITEM 1개가 남아 있다" 같은 개수와, 투시로 이미 드러난 문의 정체뿐입니다.

// 클라이언트에 보내는 상태: 정체 모르는 문은 종류별 개수만
export function publicView(run) {
  const remaining = { pass: 0, fail: 0, item: 0 };
  run.doors.forEach((k, i) => {
    if (run.known[i] === null) remaining[KEY[k]] += 1;
  });
  return { stage: run.stage, remaining, known: run.known.slice(), ... };
}

문을 고르면 서버가 판정하고, 그때 처음으로 그 스테이지의 전체 배치를 돌려줍니다. 화면 연출용입니다. 다음 스테이지 배치는 아직 아무 데도 없으므로 미리 알아낼 방법이 없습니다. 점수 역시 서버가 계산해서 DB에 쓰기 때문에, 클라이언트가 보내는 값은 "몇 번 문을 골랐다"뿐입니다.

2. 동시 요청은 버전 번호로 막는다

같은 판에 요청을 동시에 여러 개 보내면, 서버가 같은 상태를 두 번 읽고 두 번 처리할 수 있습니다. 예를 들어 FAIL 판정이 나기 전에 다른 요청이 통과 처리를 해버리는 경우입니다. 이를 막기 위해 판마다 ver 값을 두고, 저장할 때 읽었던 버전과 같을 때만 쓰도록 했습니다.

UPDATE runs SET state = ?, status = ?, ver = ver + 1, updated_at = ?
WHERE id = ? AND ver = ?

변경된 행이 0개면 다른 요청이 먼저 처리한 것이므로 거절합니다. 별도 락 없이도 한 판의 상태가 꼬이지 않습니다.

3. 일일 도전은 시드로 재현하되, 시드는 숨긴다

일일 도전은 전 세계 사람이 같은 배치를 받아야 공정합니다. 그래서 난수를 시드에서 만듭니다. 그런데 시드가 날짜만으로 정해지면 누군가 미리 계산해서 답을 공유할 수 있습니다. 그래서 서버 비밀값을 섞었습니다.

seed = sha256(DAILY_SALT + ":" + 날짜 + ":" + 시도번호) 앞 8자리

또 하루 3번 시도할 때마다 시도 번호가 달라서 배치도 바뀝니다. 첫 번째 시도에서 본 배치를 외워서 두 번째에 써먹는 것도 불가능합니다. 난수 생성기(mulberry32)의 상태는 판 데이터에 같이 저장해서, 요청 사이에도 같은 순서가 이어집니다.

4. 봇은 세 겹으로 막는다

수단 하는 일
Cloudflare Turnstile 게임을 시작할 때마다 보이지 않는 사람 확인. 토큰이 없으면 403
Rate Limiting 바인딩 IP당 게임 요청 분당 120회, 가입·신고 분당 5회
입력 간격 제한 같은 판에서 250ms 안에 연속으로 오는 요청은 거절

Turnstile은 사용자에게 퍼즐을 보여주지 않아서 게임 흐름을 해치지 않습니다. Rate Limiting은 Workers 설정 파일에 바인딩으로 선언하면 코드에서 env.RL_PLAY.limit({ key: ip }) 한 줄로 쓸 수 있습니다. 250ms 제한은 사람이 누르는 속도에는 영향이 없고, 스크립트가 초당 수십 번 찍는 것만 걸러냅니다.

5. 계정 없이 본인 확인하기

회원가입을 넣으면 사람들이 안 합니다. 대신 처음 기록을 남길 때 무작위 비밀 키를 발급해 브라우저에 저장합니다. 서버에는 키의 SHA-256 해시만 저장합니다. 이 키가 있으면 메모를 고칠 수 있고, 다른 기기에서 키를 입력하면 같은 기록을 이어 쓸 수 있습니다.

비용

Workers와 D1 모두 무료 티어 안에서 충분히 돌아갑니다. 정적 파일도 같은 Worker가 서빙하므로 서버는 하나뿐입니다. 빌드 단계가 없어서 수정 후 wrangler deploy 한 번이면 전 세계에 반영됩니다.

👉 완성된 게임: goodluck.ideality.kr