Game Center·Play Games 리더보드: 날짜 경계·제출 신뢰성
23시 50분에 오늘의 퍼즐을 시작해 다음 날 0시 5분에 풀었다고 해 보자. 이 점수는 어느 날 기록일까? 앱의 오늘 퍼즐에는 시작한 날의 기록이지만, 리더보드 SDK에는 제출한 날의 점수로 들어갈 수 있다. 사용자가 여는 “오늘” 순위 화면은 둘 중 어느 쪽도 아닌 플랫폼 고유의 시간 구간을 보여 줄 수도 있다.
일일 리더보드에서 날짜를 하나로 취급하면 자정 부근에서만 나타나는 버그가 생긴다. 더 까다로운 점은 제출 실패다. 게임 완료와 로컬 통계는 정상인데 원격 점수만 사라질 수 있고, 이를 막겠다고 완료 화면에서 로그인을 띄우면 핵심 플레이 흐름이 인증 상태에 종속된다.
이 글에서는 같은 일일 퍼즐을 Game Center와 Google Play Games에 제출하는 흐름을 기준으로 날짜 경계, 인증, 보류 제출, 로컬 실패 격리를 정리한다. API 동작은 2026년 8월 6일 Apple·Google 공식 문서 기준이다.
daily는 하나의 날짜가 아니다
먼저 “오늘”을 세 개의 값으로 나눈다.
| 값 | 뜻 | 소유자 |
|---|---|---|
puzzle_day |
어떤 일일 퍼즐을 풀었는지 나타내는 도메인 날짜 | 앱 |
submitted_at |
점수를 SDK에 보낸 시각 | 앱과 기기 |
leaderboard_window |
순위 화면이 daily 또는 today로 묶는 시간 구간 | 플랫폼 SDK |
puzzle_day는 퍼즐을 만든 순간 결정되고 플레이 도중 바뀌지 않아야 한다. submitted_at은 재시도 여부에 따라 달라질 수 있다. leaderboard_window는 앱의 로컬 자정과 일치한다고 가정할 수 없다.
따라서 점수 제출 payload에 날짜를 넣는 것과 플랫폼의 일일 순위 화면을 그 날짜로 필터링하는 것은 별개의 기능이다. 앱은 앞의 두 값을 통제할 수 있지만, 표준 리더보드 UI의 시간 구간은 플랫폼 계약을 따른다.
퍼즐 날짜는 완료 시각이 아니라 보드 정체성에서 가져온다
자정을 넘긴 게임에서 완료 시각으로 날짜를 다시 계산하면, 같은 퍼즐이 플레이 속도에 따라 서로 다른 날짜에 기록된다. 점수에 붙일 날짜는 완료 순간의 시계가 아니라 게임 세션이 들고 있던 puzzle_day여야 한다.
퍼즐 생성: 2026-08-05 23:50 → puzzle_day = 2026-08-05
게임 완료: 2026-08-06 00:05 → submitted_at = 2026-08-06 00:05
제출 메타데이터 → 2026-08-05
Android에서는 이 날짜를 yyyy-MM-dd 형태의 scoreTag로 보낼 수 있다. 중요한 것은 tag가 점수의 출처를 설명할 뿐이라는 점이다. 날짜별 중복 제거 키도 아니고, 표준 Play Games 순위 화면의 날짜 필터도 아니다.
Game Center의 점수 제출 API에는 앱별 데이터를 담는 context가 있지만, 날짜를 전달하려면 앱이 직접 인코딩 규칙을 정해야 한다. context: 0으로 제출하는 구현이라면 Game Center 점수만 보고 원래의 puzzle_day를 복원할 수 없다. 두 플랫폼이 같은 날짜 의미를 가져야 한다면 이 차이를 숨기지 말고 제품 계약에 적어야 한다.
scoreTag와 context는 날짜 파티션이 아니다
Google의 submitScore는 선택적인 scoreTag를 받는다. 문서상 tag는 점수에 붙는 URI-safe 메타데이터이며 최대 64자다. 앱이 2026-08-05를 넣어도 TIME_SPAN_DAILY 화면이 그 tag를 기준으로 행을 나누지는 않는다.
Game Center의 context도 앱이 해석하는 부가 데이터다. 표준 UI의 timeScope를 바꾸지 않는다. 날짜별 순위를 정확히 분리해야 한다면 가능한 선택지는 다음처럼 요구 수준에 따라 달라진다.
- 플랫폼이 제공하는 daily 또는 today 화면이면 충분하다면 메타데이터만 남기고 표준 UI를 쓴다.
- Game Center에서 명시적인 회차가 필요하다면 recurring leaderboard를 검토한다.
- 두 플랫폼에서 동일한 달력 날짜와 조회 규칙이 반드시 필요하다면 별도 저장소와 커스텀 UI가 필요하다.
마지막 선택지는 운영·부정행위 방지·개인정보 처리까지 함께 생긴다. “오늘의 경쟁”이면 충분한 제품에 미리 도입할 이유는 없다.
두 플랫폼의 오늘은 같은 구간이 아니다
Google Play Games는 모든 리더보드에 daily·weekly·all-time 변형을 만들며, daily는 연중 UTC-7 자정에 초기화한다. Game Center의 .today는 달력상의 오늘이 아니라 최근 24시간 범위다.
| 화면 | 시간 의미 | 앱의 puzzle_day와 일치 보장 |
|---|---|---|
Play Games TIME_SPAN_DAILY |
UTC-7 기준 일일 구간 | 없음 |
Game Center .today |
현재 시점 이전 24시간 | 없음 |
| Game Center recurring occurrence | App Store Connect에서 정한 반복 회차 | 설정에 따름 |
서울 자정에 새 퍼즐을 여는 앱이라면 이 차이가 바로 드러난다. 0시 직후 새 퍼즐 점수가 앱의 전날 퍼즐 점수와 같은 Play Games daily 구간에 보이거나, Game Center today 화면에 어제 점수와 오늘 점수가 함께 보일 수 있다. 이것은 scoreTag나 context로 고칠 수 있는 표시 버그가 아니라 서로 다른 시간 계약이다.
점수 선택 정책도 따로 본다. Android 앱의 latest-wins 보류 슬롯은 “어떤 미전송 시도를 남길지” 정하는 로컬 정책이다. Play Games는 서버의 현재 기록보다 나은 점수를 반영하고, Game Center는 리더보드 설정에 따라 best score 또는 most recent score를 사용할 수 있다. 로컬에서 최신 시도를 골랐다고 서버가 최신 값을 순위 기록으로 채택한다는 뜻은 아니다.
완료 화면에서 인증을 시작하지 않는다
게임 완료는 인증 UI가 나타나기 가장 나쁜 시점이다. 제출 조건은 부작용 없는 판정으로 닫는 편이 안전하다.
submit = won && daily && authenticated
무료 플레이, 패배, 비인증 상태에서는 제출하지 않는다. 인증 상태가 불확실하다면 조용히 확인할 수는 있지만 완료 처리 중 로그인 화면을 띄우지는 않는다. 대화형 로그인은 사용자가 리더보드 버튼을 누른 경로에만 둔다.
이렇게 하면 Game Center나 Play Games가 꺼져 있거나 로그아웃된 상태에서도 퍼즐 완료, 로컬 통계, 다음 화면 이동이 그대로 작동한다. 로그인 취소도 게임 완료 실패로 바뀌지 않는다.
latest-wins 한 칸은 오프라인 큐가 아니다
Android UI host가 아직 연결되지 않은 짧은 구간을 위해 메모리에 제출 한 건만 보류할 수 있다. 새 점수가 들어오면 이전 점수를 교체하고, Activity가 연결되면 한 번 꺼내 인증을 확인한 뒤 제출한다. 이 정책의 범위는 작고 명확하다.
Activity 없음
score A 보류
score B 도착 → A를 B로 교체
Activity 연결
B를 꺼내 인증 확인 → 제출 시도
그러나 이것은 재시도 큐가 아니다.
- 프로세스가 종료되면 보류 값이 사라진다.
- 값을 꺼낸 뒤 조용한 인증 확인이 false이거나 요청 자체가 실패해도 다시 넣지 않는다.
- 네트워크 제출 결과를 받지 않는 fire-and-forget 호출은 앱에서 성공 여부를 확정할 수 없다.
- iOS에 같은 보류 슬롯이 없다면 플랫폼별 전달 가능성도 다르다.
Google의 submitScore 문서도 호출 결과를 앱에 알리지 않는 fire-and-forget 방식이라고 설명한다. submitScoreImmediate는 Task를 돌려주지만, 그것만으로 프로세스 재시작을 견디는 durable retry가 생기지는 않는다.
일일 점수가 부가 기능이고 일부 누락을 허용한다면 이 한 칸이 합리적인 상한선이다. 전달을 보장해야 할 때만 디스크 기반 outbox, 재시작 복구, backoff, 성공 확인을 추가한다.
로컬 완료와 원격 제출을 다른 실패 도메인으로 둔다
원격 리더보드는 게임 완료의 원장이 아니다. 로컬 완료 경로는 최소한 다음 책임을 가진다.
- 같은 terminal transition을 한 번만 기록한다.
- 재개용 저장 슬롯을 지운다.
- 통계와 일일 완료 기록을 갱신한다.
- 원격 제출은 별도 best-effort 경로로 시도한다.
iOS처럼 로컬 저장을 요청한 뒤 비동기 Game Center 제출을 시작하거나, Android처럼 로컬 기록 작업과 원격 제출을 독립적으로 시작할 수 있다. 어느 쪽이든 원격 실패가 완료 상태를 되돌리면 안 된다.
여기서 “local-first”를 “원격 호출 전에 디스크 flush까지 끝났다”는 뜻으로 과장해서는 안 된다. Android처럼 로컬 쓰기를 비동기 작업으로 예약하고 곧바로 원격 호출을 시작했다면 두 작업의 완료 순서는 보장되지 않는다. 보장되는 계약은 원격 오류가 로컬 완료를 rollback하지 않는다는 것이다. 디스크 확정 순서까지 필요하다면 저장 완료를 기다리는 별도 설계가 필요하다.
전달 보장을 먼저 선택한다
구현을 키우기 전에 제품이 요구하는 손실 허용치를 고른다.
| 정책 | 보장 | 비용 | 적합한 경우 |
|---|---|---|---|
| best-effort 직접 제출 | 현재 세션에서 한 번 시도 | 가장 낮음 | 순위가 부가 기능이고 누락 허용 |
| 메모리 latest-wins 한 칸 | UI host 부재 중 최신 한 건 보존 | 낮음 | 짧은 lifecycle race만 완화 |
| durable outbox | 재시작 뒤 재시도 가능 | 높음 | 제출 누락이 보상·대회 결과에 영향 |
durable outbox가 필요하다면 payload를 먼저 영속화하고, 안정적인 제출 식별자와 puzzle_day, 점수, 대상 리더보드를 함께 저장한다. 성공 응답 뒤에만 제거하고 재시작 시 복구한다. 서버가 더 좋은 기존 점수를 유지하는 정책과 앱의 재전송 완료 판정도 분리해야 한다.
그 요구가 없다면 한 칸짜리 보류를 범용 큐로 확장하지 않는 편이 낫다. 손실 가능성을 테스트와 문서에 남기는 것으로 충분하다.
경계 사례로 계약을 검증한다
SDK 실서버에 의존하기 전에 순수 판정과 보류 정책을 작은 표로 고정한다.
| 입력 | 기대 결과 |
|---|---|
| 무료 플레이 승리 | 원격 제출 0건 |
| 일일 퍼즐 패배 | 원격 제출 0건 |
| 일일 퍼즐 승리, 비인증 | 로그인 UI 없이 제출 0건 |
| 23:50 시작, 00:05 완료 | 완료 시각이 아닌 세션의 puzzle_day 사용 |
| Activity 없이 A, B 순서로 완료 | 보류 슬롯에는 B 한 건만 남음 |
| 보류 뒤 프로세스 종료 | 재시작 제출 없음이 현재 계약 |
| 보류를 꺼낸 뒤 인증 실패 | 자동 재보류 없음이 현재 계약 |
| 원격 제출 실패 | 로컬 완료와 통계는 유지 |
| 더 낮은 최신 점수 제출 | 로컬 latest 선택과 서버의 best-score 선택을 별도 검증 |
| terminal event 중복 전달 | 완료 기록과 제출 시도 각각 1회 |
| 표준 daily/today 화면 열기 | scoreTag·context가 아니라 SDK 시간 구간 표시 |
실제 SDK 통합 테스트에서는 인증 취소, 네트워크 단절, leaderboard intent 또는 access point 표시 실패를 별도로 확인한다. 단위 테스트가 보장하는 것은 앱의 분기와 payload이며, 플랫폼 서버가 점수를 반영했다는 사실은 아니다.
정리
일일 리더보드의 날짜는 puzzle_day, submitted_at, leaderboard_window로 나눠야 한다. 퍼즐 날짜는 세션의 정체성에서 가져오고, scoreTag와 context는 메타데이터일 뿐 표준 순위 화면의 날짜 파티션으로 보지 않는다.
완료 경로에서는 인증 UI를 띄우지 않고 로컬 기록과 원격 제출을 서로 다른 실패 도메인으로 둔다. 메모리 latest-wins 한 칸은 lifecycle race를 줄이는 best-effort 장치이지 재시도 큐가 아니다. 누락이 제품적으로 허용되지 않을 때만 durable outbox를 추가한다. 이 세 경계를 먼저 정하면 자정을 넘긴 점수와 실패한 제출이 게임 완료 자체를 흔들지 않는다.
출처
- Android Developers — Leaderboards
- Google for Developers — LeaderboardsClient
- Android Developers — Platform authentication
- Apple Developer — Submit scores and ranks
- Apple Developer — GKLeaderboard.TimeScope.today
- Apple Developer — Authenticating a player
- Apple Developer — Creating recurring leaderboards
- App Store Connect Help — Game Center leaderboards
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.