모바일·크로스플랫폼 세이브와 클라우드 동기화 — iCloud·Play Games Services 설계 체크리스트

모바일

클라우드 세이브iCloudPlay Games Services동기화크로스플랫폼

이 글은 누구를 위한 것인가

  • iOS·Android·웹을 동시에 운영하며 사용자 진행 상태를 동기화해야 하는 모바일 개발자
  • 게임·생산성 앱에서 기기 변경 후 진행 손실 문의가 잦은 운영팀
  • "마지막에 저장한 게 뭐였는지" 충돌 처리 정책을 정해야 하는 PM·기획

들어가며

세이브 데이터 동기화는 모바일에서 가장 자주 깨지지만 가장 늦게 손대는 영역이다. 단일 기기에서는 잘 작동하는 것처럼 보이다가, 사용자가 두 번째 기기에 로그인한 순간부터 문제가 시작된다. 한쪽에서 진행한 결과가 다른 쪽에서 덮어써지거나, 두 진행본이 충돌해 이전 상태로 롤백되는 일이 흔하다.

테이블플레이의 모바일·크로스플랫폼 세이브와 클라우드 동기화 — 유저가 챙길 체크리스트 글이 사용자 관점에서 이 문제를 정리한다. 이 글은 같은 주제를 모바일 개발자가 어떻게 설계할지 관점에서 정리한다. 같은 매치3 모바일 브라우저 체크 글과 함께 읽으면 모바일 브라우저 환경의 세이브 한계까지 같이 본다.

모바일·크로스플랫폼 세이브와 클라우드 동기화 — iCloud·Play Games Services 설계 체크리스트


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 Prevention7일 미접속 시 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 모바일 브라우저 체크 세 글은 사용자가 어떤 상황에서 세이브를 잃는지를 잘 정리해 둔 사례다. 같은 상황을 개발자가 사전에 막을 수 있는 항목으로 옮기면, 위 체크리스트의 절반이 자동으로 채워진다.

세이브 동기화에 시간을 한 분기 정도 투자하면, 기기 변경 후 사용자 잔존율이 한 단계 올라간다. 모바일 운영에서 가장 가성비 좋은 한 분기다.