이 글은 누구를 위한 것인가
- iOS·Android·웹을 동시에 운영하며 사용자 진행 상태를 동기화해야 하는 모바일 개발자
- 게임·생산성 앱에서 기기 변경 후 진행 손실 문의가 잦은 운영팀
- "마지막에 저장한 게 뭐였는지" 충돌 처리 정책을 정해야 하는 PM·기획
들어가며
세이브 데이터 동기화는 모바일에서 가장 자주 깨지지만 가장 늦게 손대는 영역이다. 단일 기기에서는 잘 작동하는 것처럼 보이다가, 사용자가 두 번째 기기에 로그인한 순간부터 문제가 시작된다. 한쪽에서 진행한 결과가 다른 쪽에서 덮어써지거나, 두 진행본이 충돌해 이전 상태로 롤백되는 일이 흔하다.
테이블플레이의 모바일·크로스플랫폼 세이브와 클라우드 동기화 — 유저가 챙길 체크리스트 글이 사용자 관점에서 이 문제를 정리한다. 이 글은 같은 주제를 모바일 개발자가 어떻게 설계할지 관점에서 정리한다. 같은 매치3 모바일 브라우저 체크 글과 함께 읽으면 모바일 브라우저 환경의 세이브 한계까지 같이 본다.

1. 어디까지 동기화할 것인가
세이브 데이터는 보통 세 계층으로 나뉜다.
| 계층 | 예시 | 동기화 우선순위 |
|---|---|---|
| 진행 상태 | 레벨, 클리어 여부, 캐릭터 능력치 | 높음 |
| 사용자 설정 | 사운드 볼륨, 알림 설정, 언어 | 중간 |
| 캐시 | 이미지, 사운드 파일 | 낮음 (또는 안 함) |
캐시까지 동기화하면 용량이 폭증하고, 진행 상태만 동기화하면 사용자는 새 기기에서 처음부터 설정해야 한다. 일반 권장은 진행 상태와 핵심 설정만 클라우드에 올리고, 캐시는 기기 로컬에 둔다. 클라우드 비용·동기화 시간 모두에서 안정적이다.
2. iOS — iCloud Key-Value vs CloudKit
iOS의 클라우드 세이브 옵션은 두 가지가 흔하다.
iCloud Key-Value Storage
- 최대 1MB, 키당 1MB까지
- 키 1024개 제한
- 자동 충돌 해결: 마지막 쓰기 우선
- 코드 한 줄 수준으로 단순
let store = NSUbiquitousKeyValueStore.default
store.set(progress.toJSONData(), forKey: "userProgress")
store.synchronize()
가벼운 진행 상태(레벨, 클리어 비트맵, 통화 잔액)에 적합하다. 게임 세이브 슬롯 여러 개나 큰 인벤토리에는 작다.
CloudKit
- 사용자당 1GB 무료 (Apple ID 기준)
- 충돌 해결을 개발자가 정의
- 레코드·필드·쿼리 지원
- 학습 곡선 있음
let record = CKRecord(recordType: "SaveSlot")
record["slotIndex"] = 1
record["payload"] = saveData
record["updatedAt"] = Date()
container.privateCloudDatabase.save(record) { saved, error in
// 충돌 시 error.code == .serverRecordChanged
}
CloudKit의 강점은 충돌 해결을 직접 결정할 수 있다는 점이다. 기기 A에서 레벨 10을 클리어하고 기기 B에서 레벨 12를 클리어한 경우, 큰 진행도 우선 같은 도메인 규칙을 코드로 적용한다.
3. Android — Play Games Services Saved Games
Play Games Services의 Saved Games API는 다음 특성을 갖는다.
- 슬롯당 최대 3MB, 슬롯 개수 무제한 (실용 권장 1~5개)
- 메타데이터(슬롯 이름, 플레이 시간, 진행도)와 본문 분리
- 충돌 시 두 스냅샷을 모두 받아 개발자가 머지 결정
val client = PlayGames.getSnapshotsClient(activity)
client.open("save_slot_1", true).addOnSuccessListener { result ->
val snapshot = if (result.conflict != null) {
resolveConflict(
local = result.conflict!!.snapshot,
remote = result.conflict!!.conflictingSnapshot
)
} else {
result.data
}
snapshot?.snapshotContents?.writeBytes(payload)
val meta = SnapshotMetadataChange.Builder()
.setDescription("진행도 ${currentLevel}")
.setPlayedTimeMillis(playedMillis)
.build()
client.commitAndClose(snapshot!!, meta)
}
충돌 처리 함수 resolveConflict가 핵심이다. 게임 도메인이라면 진행도 큰 쪽, 생산성 앱이라면 최근 수정 시각 우선이 흔한 정책이다.
4. 충돌 처리 정책 결정 트리
다음 트리가 도메인별로 자주 쓰이는 결정 흐름이다.
충돌 발생
│
├─ 진행도(monotonically increasing) 데이터인가?
│ └─ Yes → 큰 값 우선 (max)
│
├─ 최근성이 의미 있는가? (메모, 설정)
│ └─ Yes → 최근 수정 시각 우선
│
├─ 누적 데이터인가? (수집 아이템, 통계)
│ └─ Yes → 합집합 (set union)
│
└─ 사용자 결정이 필요한가?
└─ Yes → UI 표시: "어느 진행본을 사용하시겠어요?"
가장 위험한 결정은 마지막 쓰기 무조건 우선이다. 사용자가 새 기기에서 잠깐 실행한 빈 진행본이 클라우드에 올라가, 원래 기기의 한 달치 진행을 덮어쓰는 사례가 발생한다. 첫 동기화 시점에는 작은 진행도가 큰 진행도를 덮지 못하게 하는 가드를 둔다.
5. 모바일 브라우저(웹 게임)에서의 세이브
테이블플레이 매치3 모바일 브라우저 체크 글과 포커 웹 게임 저장 실패 글이 다루는 영역이다. 모바일 브라우저는 데스크톱과 달리 세이브가 자주 사라진다. 원인은 다음이다.
| 원인 | 영향 | 대응 |
|---|---|---|
| iOS Safari Intelligent Tracking Prevention | 7일 미접속 시 LocalStorage 삭제 | IndexedDB도 동일 영향 |
| 시크릿/프라이빗 모드 | 세션 종료 시 모든 저장소 삭제 | 사용자 안내 |
| 저장소 용량 초과 | 자동 정리 | 정기 압축·정리 |
| 사용자 수동 삭제 | "데이터 지우기"로 한 번에 삭제 | 백업 권장 안내 |
브라우저 세이브에 의존하면 안 된다는 결론이다. 핵심 진행 상태는 서버 또는 플레이어 식별 가능한 클라우드 세이브에 백업한다. 익명 사용자라면 익명 ID + 1차 인증으로 익명 진행을 보존하는 흐름을 둔다.
async function saveProgress(progress) {
try {
await indexedDBPut('progress', progress);
} catch (e) {
// 용량·권한 실패
}
if (navigator.onLine) {
await fetch('/api/progress', {
method: 'POST',
body: JSON.stringify({
anonymousId: localStorage.getItem('aid') ?? createAnonymousId(),
progress,
clientUpdatedAt: Date.now(),
}),
});
} else {
queueOfflineSync(progress);
}
}
오프라인 큐를 두고 온라인 복귀 시 동기화한다. 모바일에서는 네트워크 단절이 데스크톱보다 훨씬 자주 일어난다.

6. 크로스플랫폼 ID 정렬
iOS·Android·웹 사용자를 같은 사람으로 묶기 위해서는 공용 ID가 필요하다. 흔한 옵션은 다음이다.
- Apple Sign In + Google Sign In + 이메일/소셜: 익명 사용자 → 가입 유도 → 동기화
- 계정 시스템 자체 운영: 자체 인증 + OAuth 연동
- 익명 ID 우선 + 나중 매칭: 익명 진행을 가입 시 흡수
세 방법 모두 익명 진행의 흡수 단계가 핵심이다. 사용자가 가입하기 전 만든 진행이 가입 후 사라지면 이탈로 직결된다. 익명 ID에 묶인 데이터를 가입 ID에 대신 키만 바꿔 옮기는 단순한 흐름이 가장 안전하다.
7. 운영 체크리스트
다음 12개 항목이 클라우드 세이브 도입 후 6개월 이내에 한 번씩 점검할 가치가 있다.
- 첫 동기화 시 작은 진행본이 큰 진행본을 덮지 않도록 가드 있음
- 충돌 시 사용자 선택 UI가 세이브 전에 노출됨
- 클라우드 저장 실패 시 로컬 저장은 유지됨
- 오프라인 큐가 유한하게 관리됨 (무한 누적 방지)
- 슬롯 메타데이터에 "기기 명·플레이 시간·진행도" 포함
- iCloud/Play Games 비활성 사용자에 대한 대안 백업 안내
- 시크릿 모드·ITP 영향 사용자 안내
- 비회원 → 회원 전환 시 진행 흡수 테스트
- 세이브 데이터 용량 한계 경고 알림
- 세이브 데이터 버전과 호환성 매트릭스
- 마이그레이션 스크립트 (구 포맷 → 신 포맷)
- 세이브 복구 도구 (CS에서 사용)
마지막 항목이 의외로 자주 빠진다. 세이브가 깨졌을 때 사용자에게 어떻게 도와줄지를 운영 도구로 갖고 있지 않으면, CS는 무력하고 사용자는 떠난다.
마무리
클라우드 세이브는 기능이 아니라 운영이다. 한 번 켜두는 것이 아니라 충돌·실패·마이그레이션을 계속 다뤄야 한다. 테이블플레이의 모바일·크로스플랫폼 세이브와 클라우드 동기화, 포커 웹 게임 저장 실패, 매치3 모바일 브라우저 체크 세 글은 사용자가 어떤 상황에서 세이브를 잃는지를 잘 정리해 둔 사례다. 같은 상황을 개발자가 사전에 막을 수 있는 항목으로 옮기면, 위 체크리스트의 절반이 자동으로 채워진다.
세이브 동기화에 시간을 한 분기 정도 투자하면, 기기 변경 후 사용자 잔존율이 한 단계 올라간다. 모바일 운영에서 가장 가성비 좋은 한 분기다.