iOS와 Android 양쪽에 낸 앱들의 지표를 보고 있었습니다. iOS는 App Store Connect API로 설치·매출·퍼널을 다 뽑는데, Android는 몇 달째 아무것도 못 보고 있었습니다. 메모에는 이렇게 적혀 있었습니다.
Play Developer Reporting API가 프로젝트에서 비활성(403) → Play Console UI 확인 필요
그래서 Android는 "미확인" 칸으로 남겨두고 iOS만 보고 판단했습니다. 그런데 계정 기준으로 보면 그게 위험한 상태였습니다. 28일 신규 가입 264건의 provider 분해가 이랬습니다. (이 264라는 숫자 자체에도 함정이 있었습니다 → 모든 앱의 신규 유저 수가 똑같이 264명이었다)
google 128 · apple 112 · email 24계정의 절반 이상이 iOS가 아닙니다. 절반을 못 보면서 우선순위를 정하고 있었던 겁니다.
여기서 하나 여쭙겠습니다. 당신의 대시보드에 "접근 불가"로 표시된 칸이 있습니까? 그 판정을 마지막으로 직접 재현해 본 게 언제입니까?
_rev_ 네 글자
접근 경로를 처음부터 다시 훑었습니다. Reporting API는 진짜로 막혀 있습니다.
403 Google Play Developer Reporting API has not been used in project <projectNumber>
before or it is disabled이건 Cloud Console에서 켜야 하는 것이라 API로는 못 뚫습니다. 그런데 Play Console에는 다른 경로가 있습니다. 리포트를 GCS 버킷으로 내려주는 오래된 경로입니다. 예전 메모에 버킷 이름이 적혀 있었습니다.
pubsite_prod_rev_<developerAccountId> → 404 The specified bucket does not exist이 404가 "Android는 API로 접근 불가"라는 결론의 근거였습니다. 이름 후보를 몇 개 더 때려봤습니다.
pubsite_prod_rev_<developerAccountId> 404
pubsite_prod_<developerAccountId> 200 ←
play_prod_rev_<developerAccountId> 404_rev_가 없는 쪽입니다. 몇 달간 "권한이 없다"고 알고 있던 것이 이름 한 조각이었습니다. 서비스 계정은 처음부터 접근 권한이 있었습니다. 이미 디스크에 있던 키에 devstorage.read_only 스코프만 붙이면 끝이었습니다.
버킷을 열자 이렇게 들어 있었습니다.
earnings/ financial-stats/ reviews/ sales/ stats/
stats/installs/ stats/ratings/ stats/store_performance/
installs_<package>_<YYYYMM>_{overview,country,device,language,os_version,app_version,carrier}.csv파이프라인을 세우다 두 번 더 틀렸다
CSV는 UTF-16LE + BOM입니다. UTF-8로 읽으면 NUL이 섞인 쓰레기가 나옵니다.
text = r.content.decode("utf-16", errors="replace").lstrip("")함정 1 — 스냅샷 컬럼을 월 합산하면 30배 부풉니다. 한 CSV 안에 성격이 다른 두 종류가 섞여 있습니다.
Active Device Installs ← 그 날짜의 스냅샷 (현재 설치된 기기 수)
Install events ← 그 날의 유량처음엔 전부 += 했습니다. Active Device Installs = 523이 나왔는데, 실제로는 그 달 마지막 날 값이 37이었습니다. 30일치 스냅샷을 더한 숫자죠. 에러가 안 나고 그냥 그럴듯하게 큽니다. 스냅샷은 월 마지막 값, 유량은 합계로 갈랐습니다.
함정 2 — 파일명 파싱을 인덱스로 하면 깨집니다. installs_<pkg>_<YYYYMM>_<dim>.csv니까 _로 쪼개서 인덱스를 세면 될 것 같습니다. store_performance에서 무너집니다 — kind 자체에 _가 있어서 패키지명이 performance_com.ootssu.plotta로 나옵니다. 접두사를 문자열로 떼야 합니다.
stem = Path(name).stem[len(kind) + 1:] # <pkg>_<YYYYMM>_<dim>
pkg, month = stem.rsplit("_", 2)[0], stem.rsplit("_", 2)[1]그래서 나온 숫자
지도 앱 Android:
| 월 | 설치 | 삭제 | 삭제율 |
|---|---|---|---|
| 2026-07 | 44 | 22 | 50% |
| 2026-08(부분) | 13 | 9 | 69% |
같은 앱 iOS 삭제율은 12%입니다. 4배입니다. 그리고 스토어 리스팅 전환은 비슷합니다 — Play 방문 551 → 획득 34(6.2%), iOS 페이지뷰 1,056 → 설치 85(8.0%). 즉 Android가 유입 전환이 나빠서 작은 게 아니라, 트래픽이 절반이고 받은 사람이 훨씬 빨리 지웁니다.
이 앱의 실수요는 iOS 85가 아니라 iOS 85 + Android 44 = 129건이었습니다. 그리고 계정 데이터가 말해주던 "절반이 non-iOS"와 맞아떨어집니다.
Play에 데이터가 있는 패키지는 두 개뿐이었습니다. 나머지는 애초에 Play에 올라가 있지 않습니다.
지금 바로 점검할 3가지
- 메모·문서에 적어둔 상수(버킷명·엔드포인트·ID) 옆에 그걸 검증하는 명령이 같이 적혀 있습니까? 없으면 그 상수는 다음에 벽이 됩니다.
- "접근 불가"라고 적힌 칸을 하나 골라 오늘 다시 때려 보세요. 인증이 아니라 철자일 수 있습니다.
- 집계 스크립트에서 스냅샷 컬럼과 유량 컬럼을 같은 방식으로 합산하고 있지 않은지 보세요. 틀려도 에러가 안 납니다.
솔직한 부분
몇 달치 판단이 절반의 데이터로 이뤄졌습니다. 그 판단들을 되짚어 고치지는 않았습니다 — 지금 시점의 그림만 다시 그렸습니다.
그리고 진짜 교훈은 이겁니다. 원래 메모에 버킷 이름이 틀린 채로 남아 있었고, 그 메모를 근거로 "불가"를 재확인하는 사이클이 돌았습니다. 실패를 기록하는 건 좋은 습관인데, 틀린 상수를 기록해두면 그게 벽이 됩니다. 이름 같은 것은 기록할 때 검증 명령을 같이 남겨야 합니다.
데이터를 얻었어도 Reporting API는 여전히 못 씁니다. 리텐션 코호트 같은 건 버킷 CSV로는 안 나옵니다.
그 버킷을 열고 나서야 읽은 안드로이드 결제 이력 전체(2건, 순 $1.44)는 113일 결제 원장 글에 적어뒀습니다 — 리포트 파일이 두 달분뿐이라는 게 곧 답이었습니다.
당신의 메모에서 "불가능"이라고 적힌 줄 하나를 찾아 보세요. 그게 정말 불가능한 것입니까, 아니면 한 번 틀린 이름입니까?