회고 · 대시보드 · Google Cloud · Google Play

서비스 계정으로 경찰과 도둑 앱의 Google Play 통계 CSV 받기

박찬빈

어드민 지표 대시보드를 만든 과정을 일곱 편으로 나눠 적었다. 이 글은 3편이다.

  1. 경찰과 도둑 앱 지표를 골라 매일 모으기로 한 이유
  2. 인스타그램 API로 경찰과 도둑 앱 하루치 지표를 구해 디스코드로 보내기
  3. 서비스 계정으로 경찰과 도둑 앱의 Google Play 통계 CSV 받기
  4. App Store Connect API로 경찰과 도둑 앱 다운로드와 노출 수 받기
  5. GA4 Data API로 경찰과 도둑 앱과 웹사이트 지표 모으기
  6. AdMob API 갱신 토큰으로 경찰과 도둑 앱 광고 지표 매일 받기
  7. Supabase로 경찰과 도둑 앱 지표 대시보드 만들기

1편에서 "Google Play는 통계 API가 없다"고 했다. 그래서 Google Play에 통계 숫자를 바로 요청하는 대신, 매일 만들어지는 표 파일(CSV)을 받기로 했다. 이 글에서는 파일을 받을 계정을 만들고, 권한을 준 뒤 실제로 읽기까지 겪은 일을 적었다. 여기서 만드는 서비스 계정은 5편(GA4)에서도 그대로 쓴다.

파일을 받아 줄 서비스 계정

프로그램이 매일 통계 파일을 받으려면 구글에 어떤 계정으로 요청하는지 알려야 한다. 사람이 콘솔에 로그인하는 것과 비슷한데, 프로그램이 매일 새벽 내 구글 계정 비밀번호를 칠 수는 없다.

그래서 구글에서는 사람 대신 프로그램이 쓰는 구글 계정을 따로 만들 수 있다. 이걸 서비스 계정이라고 한다. 만들면 이름@프로젝트ID.iam.gserviceaccount.com 모양의 이메일이 생기고, 이 이메일을 GA4나 Play Console에 사용자로 초대할 수 있다.

로그인에는 비밀번호 대신 개인 키가 담긴 키 파일(JSON)을 쓴다. 프로그램은 이 키로 서명해서 어떤 서비스 계정의 요청인지 증명하고, API를 호출할 때 쓸 액세스 토큰을 받는다.

1. 개인 키로 "나는 metrics-collector다, 이 범위를 쓰고 싶다"는 JWT에 서명한다
2. 그 JWT를 https://oauth2.googleapis.com/token 에 보낸다
   (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer)
3. 구글이 서명을 확인하고, 잠깐 쓰는 액세스 토큰을 돌려준다
4. API를 부를 때 Authorization: Bearer <액세스 토큰> 을 붙인다

이때 보내는 JWT에는 누가, 언제까지, 무엇을 하고 싶은지가 적혀 있고, 거기에 개인 키로 서명한다. 구글은 우리 공개 키로 서명을 확인하니, 개인 키를 가진 쪽만 이 문서를 만들 수 있다.

라이브러리(google-auth-library)를 쓰면 이 과정을 알아서 해 준다. 우리는 패키지 없이 Node 기본 crypto로 직접 서명했고, 코드는 30줄 남짓이었다.

중요하게 본 건 토큰으로 어디까지 할 수 있는지 정하는 범위(scope)였다. 같은 서비스 계정이라도 읽기 범위만 요청하면, 그 토큰으로는 읽기만 할 수 있다.

쓰는 곳범위
Play 통계 파일(Cloud Storage)https://www.googleapis.com/auth/devstorage.read_only
GA4https://www.googleapis.com/auth/analytics.readonly
Play 출시 버전 확인https://www.googleapis.com/auth/playdeveloperreporting

따라 하기 1. 구글 클라우드 프로젝트 만들기

먼저 어느 구글 계정으로 만들지 정했다. 내가 팀에서 빠져도 키와 권한이 팀 자산으로 남아야 해서, Play Console과 GA4에도 쓰는 팀 구글 계정으로 만들었다.

학교나 회사의 Google Workspace 계정으로 만든다면, 소속 조직이 2024년 5월 3일 이후 만들어졌는지 먼저 확인해야 한다. 이날 이후 만들어진 조직은 서비스 계정 키 만들기가 기본으로 막혀 있다. 일반 Gmail 계정으로 만든 프로젝트에는 이 제한이 없고, 우리 팀 계정도 Gmail이라 문제없었다.

  1. https://console.cloud.google.com 에 팀 계정으로 로그인한다

  2. 맨 위 왼쪽, 구글 클라우드 로고 옆 프로젝트 선택 상자를 누른다

  3. 뜬 창 오른쪽 위 "새 프로젝트"

  4. 프로젝트 이름을 넣는다. 우리는 dongsim-analytics

    새 프로젝트. 이름 아래 프로젝트 ID는 나중에 못 바꾼다
    새 프로젝트. 이름 아래 프로젝트 ID는 나중에 못 바꾼다

    이름 아래에 프로젝트 ID가 자동으로 만들어진다. 이건 나중에 못 바꾼다.

    서비스 계정 이메일 뒷부분이 이 ID가 되니, "수정"을 눌러 알아보기 쉬운 이름으로 바꿔 두는 게 좋다.

  5. "만들기"를 누르고, 다 만들어지면 위 상자에서 그 프로젝트를 고른다

앱이 쓰는 Firebase 프로젝트가 이미 있었지만, 거기에 끼우지 않고 수집용 프로젝트를 따로 만들었다.

따라 하기 2. 쓸 API 켜기

새 프로젝트는 대부분의 API가 꺼져 있어서, 권한이 있어도 꺼진 API를 호출하면 403이 난다. 이때는 오류 문구에 "has not been used in project ... or it is disabled"가 나온다. 그래서 먼저 우리가 쓸 API부터 켰다.

  1. 왼쪽 위 줄 세 개 → "API 및 서비스" → "라이브러리"
  2. 검색해서 들어가 "사용"을 누른다

이번 수집에 쓸 API는 아래와 같았다.

API쓰는 곳
Google Analytics Data API5편 GA4
AdMob API6편 AdMob
Google Play Developer Reporting API이 글 끝, 출시 버전 확인
켜고 나면 "API 사용 설정됨"이 보인다
켜고 나면 "API 사용 설정됨"이 보인다

Play 통계 파일이 있는 Cloud Storage는 새 프로젝트에 이미 켜져 있어서 따로 켜지 않았다.

따라 하기 3. 서비스 계정 만들기

  1. 메뉴 → "IAM 및 관리자" → "서비스 계정"

  2. 위쪽 "서비스 계정 만들기"

  3. 이름에 metrics-collector를 넣는다. 아래에 이메일 주소가 미리 보인다

    서비스 계정 이름을 넣으면 이메일 주소가 미리 보인다
    서비스 계정 이름을 넣으면 이메일 주소가 미리 보인다
  4. "만들고 계속하기"

  5. 2번 "권한" 단계에서 아무 역할도 고르지 않고 넘어간다

  6. "완료"

만든 서비스 계정. 이메일을 복사해 두면 Play와 GA4에 초대할 때 쓴다
만든 서비스 계정. 이메일을 복사해 두면 Play와 GA4에 초대할 때 쓴다

비워 둔 역할

여기서 "편집자"나 "소유자" 역할을 주는 튜토리얼이 꽤 많다. 그런데 우리한테도 이 역할이 필요한지는 따져 봐야 했다.

구글 클라우드에서는 IAM으로 권한을 관리한다. 여기서 고르는 역할은 이 프로젝트 안의 리소스(VM, 버킷, DB)를 다룰 때 쓰는 권한이다. 그런데 우리가 읽을 GA4 속성과 Play 통계 버킷은 이 프로젝트 안에 없다. 권한도 GA4는 GA4 관리 화면에서, Play는 Play Console에서 준다.

그래서 이 프로젝트에서 서비스 계정이 할 일은 키로 토큰을 받는 것밖에 없었다. 굳이 역할을 주면 키가 샜을 때 피해만 커지기 때문에 프로젝트 역할은 비워 뒀다.

프로젝트 IAM. 서비스 계정은 여기 없다. 프로젝트 역할이 0개라는 뜻이다
프로젝트 IAM. 서비스 계정은 여기 없다. 프로젝트 역할이 0개라는 뜻이다

따라 하기 4. 키 파일 받기

  1. 서비스 계정 목록에서 metrics-collector를 누른다

  2. 위쪽 "키" 탭

  3. "키 추가" → "새 키 만들기"

    키 탭. 구글이 키를 내려받지 말라고 경고하는 게 보인다
    키 탭. 구글이 키를 내려받지 말라고 경고하는 게 보인다
  4. JSON을 고르고 "만들기"

JSON 키 만들기. 누르는 순간 파일이 내려받아지고, 다시는 못 받는다
JSON 키 만들기. 누르는 순간 파일이 내려받아지고, 다시는 못 받는다

파일이 바로 내려받아지는데, 이게 유일한 사본이다. 잃어버리면 그 키를 지우고 새로 만들어야 해서, 바로 비공개 폴더나 비밀번호 관리자로 옮겨 둔다.

그런데 키 화면 위에 노란 경고가 크게 떠 있었다. 서비스 계정 키는 새면 위험하니 내려받지 말고 "워크로드 아이덴티티 제휴"를 쓰라는 내용이었다.

워크로드 아이덴티티 제휴를 쓰면 GitHub Actions나 AWS 같은 외부 실행 환경이 구글에 문서를 하나 보여 주고 토큰을 받는다. 이 문서는 지금 실행이 어느 저장소의 어떤 워크플로에서 왔는지 증명한다. 오래 쓸 수 있는 키 파일을 보관하지 않아도 되니, 경고는 맞는 말이었다.

다만 수집은 GitHub Actions가 아니라 우리 웹 서버인 Vercel의 서버 함수 안에서 실행된다. 그래서 우리는 키 방식을 쓰되, 키는 Vercel 환경변수에만 넣고 저장소, 채팅, 노션에는 넣지 않기로 했다. 프로젝트 역할도 0개로 둬서 키가 새더라도 GA4 보기와 Play 통계 읽기밖에 못 하게 했다.

오류 메시지에도 키 파일 내용이 섞이지 않게 했다. JSON을 읽다가 오류가 나면 메시지에 입력 일부(개인 키 조각)가 들어갈 수 있어서, 우리 코드는 "읽지 못했다"는 메시지만 내보낸다.

구글은 공개 저장소에서 서비스 계정 키를 발견하면 자동으로 사용 중지한다. 키 화면의 파란 안내에도 이 내용이 있었지만, 공개 저장소에 올라가기 전에 막아야 한다고 봤다.

따라 하기 5. Play Console에 서비스 계정 초대하기

계정과 키가 준비됐으니, 이제 Play 쪽에서 이 서비스 계정이 우리 통계를 읽어도 된다고 허락할 차례였다.

  1. https://play.google.com/console 에 팀 계정으로 로그인한다

  2. 앱을 고르지 않은 첫 화면(모든 앱) 에서 왼쪽 "사용자 및 권한"

    사용자 및 권한. 오른쪽 위 신규 사용자 초대
    사용자 및 권한. 오른쪽 위 신규 사용자 초대
  3. "신규 사용자 초대"

  4. 이메일 주소 칸에 서비스 계정 이메일을 붙여 넣는다

    이메일 칸에 서비스 계정 이메일을 넣는다
    이메일 칸에 서비스 계정 이메일을 넣는다
  5. 아래 "권한"에서 계정 권한으로 "앱 정보 보기 및 보고서 일괄 다운로드(읽기 전용)" 하나만 체크한다

    계정 권한에서 앱 정보 보기 및 보고서 일괄 다운로드 하나만. 앱 품질 정보 보기는 같이 체크된다
    계정 권한에서 앱 정보 보기 및 보고서 일괄 다운로드 하나만. 앱 품질 정보 보기는 같이 체크된다
  6. "사용자 초대"를 누른다

여기서는 계정 권한과 앱 권한을 구분해야 한다. 통계 파일은 개발자 계정 전체가 쓰는 버킷에 있다. 버킷은 Cloud Storage에서 파일을 담는 통이다. 그래서 앱 하나에만 권한을 주는 "앱 권한"으로는 읽을 수 없고, 계정 권한으로 줘야 한다.

따라 하기 6. Cloud Storage 주소 복사하기

  1. 왼쪽 "보고서 다운로드" → "통계"
  2. 오른쪽 위 앱이 맞는지 확인한다. 계정에 앱이 여럿이면 다른 앱이 골라져 있을 수 있다
  3. "Cloud Storage URI 복사"
통계 보고서 다운로드. 오른쪽 위 Cloud Storage URI 복사
통계 보고서 다운로드. 오른쪽 위 Cloud Storage URI 복사

복사한 주소는 gs://pubsite_prod_ 로 시작했다. 이 주소가 가리키는 Cloud Storage 버킷에 Play가 우리 통계 CSV를 매일 다시 써 둔다. 화면에서 월을 펼쳤을 때 보이는 파일들도 이 버킷 안의 파일과 같다.

10월을 펼친 모습. 개요, 국가별 같은 파일이 버킷에 그대로 있다
10월을 펼친 모습. 개요, 국가별 같은 파일이 버킷에 그대로 있다

그런데 어떤 문서에는 주소가 pubsite_prod_rev_로 시작한다고 적혀 있었다. 우리 버킷은 rev_ 없이 pubsite_prod_로 시작했다. 그래서 주소는 짐작해서 넣기보다 이 버튼으로 복사하는 게 좋다.

권한을 줬는데도 403

10월 5일 저녁에 초대를 마치고 바로 읽어 봤다. 그런데 파일 대신 오류가 돌아왔다.

HTTP 403 ... (or it may not exist)

처음에는 권한이 아직 반영되지 않은 것 같아서 기다렸다. 그런데 몇 시간 뒤에도 403이었고, 다음 날 저녁에도 403이었다. '기다리면 풀리는 건가, 내가 뭘 잘못 넣은 건가?' 싶었다.

문제는 끝에 있는 "(or it may not exist)"였다. 권한이 없을 때와 버킷 이름이 틀렸을 때 똑같은 403이 나오니, 이 메시지만으로는 기다려야 하는지 고쳐야 하는지 알 수 없었다.

401, 403, 404 가르기

먼저 서버가 응답과 함께 돌려주는 HTTP 상태 코드부터 짚어 봤다.

코드뜻
401 Unauthorized너 누군지 모르겠다. 로그인(인증)부터 해라
403 Forbidden누군지는 알겠는데 이건 못 보여 준다
404 Not Found그런 거 없다

버킷 이름은 전 세계에서 하나뿐이고, Cloud Storage는 권한 없는 사람에게 그 버킷이 있는지 없는지를 일부러 숨긴다. 남의 버킷 이름을 마구 넣어 보면서 존재 여부를 알아내는 걸 막기 위해서다. 그래서 로그인했지만 그 버킷 권한이 없는 서비스 계정에게는 버킷이 있든 없든 403을 준다.

그런데 로그인 없이 요청했을 때는 응답이 달랐다. 복사한 이름과 다른 이름들을 아래 명령으로 하나씩 확인했다.

Shell
curl -s -o /dev/null -w "%{http_code}" "https://storage.googleapis.com/storage/v1/b/<버킷이름>/o"
물어본 버킷로그인 없이 물었을 때
복사한 우리 버킷401 (있다. 로그인해라)
지어낸 이름404 (없다)
rev_가 붙은 이름404 (없다)

로그인한 서비스 계정에는 못 보여 준다고만 하던 버킷이, 로그인 없이 물으니 오히려 "있다. 로그인해라"(401)라고 답했다. 버킷은 실제로 있었고 주소도 맞았다. 같은 키로 GA4(5편)는 10월 5일 바로 읽혔으니 키도 문제가 아니었다. 그래서 Play 권한이 저장소까지 반영되는 데 시간이 걸리는 것 같다고 판단하고, 설정을 고치지 않은 채 기다리기로 했다.

정확히 언제 풀렸는지는 적어 두지 못했다. 10월 6일 저녁까지 403이었고, 10월 7일에는 파일을 받아 GA4와 대조하고 있었다. 초대하고 하루 넘게 걸렸다.

이 경험 때문에 1편 순서표에서 Play를 위쪽에 두었다. 권한 요청을 먼저 해 두면 기다리는 동안 다른 걸 만들 수 있기 때문이다.

파일을 받은 뒤 맞출 것

다른 앱 파일 거르기

권한이 풀려 버킷을 열어 보니 우리 앱 파일만 있는 게 아니었다. 개발자 계정 전체가 함께 쓰는 버킷이고, 우리 계정에는 앱이 여럿이라 다른 앱 파일도 같이 있었다. 파일은 329개, 리포트 종류는 23개였다. 파일 이름에 앱마다 붙는 고유한 이름인 패키지 이름이 들어 있어서 그걸로 걸렀다.

stats/installs/installs_<패키지>_202610_overview.csv
stats/installs/installs_<패키지>_202610_country.csv
stats/store_performance/store_performance_<패키지>_202610_country.csv

파일 목록도 Cloud Storage JSON API에 패키지 이름이 들어간 파일 이름 앞부분을 접두어로 넣어서 받았다.

GET https://storage.googleapis.com/storage/v1/b/<버킷>/o
    ?prefix=stats/installs/installs_<패키지>_
Authorization: Bearer <devstorage.read_only 토큰>

UTF-16 파일

받은 CSV를 그냥 UTF-8로 읽었더니 글자 사이사이에 빈 글자가 낀 것처럼 깨졌다. 파일은 UTF-16 리틀 엔디언으로 저장돼 있었고, 맨 앞에는 이 형식을 알려 주는 BOM(FF FE 두 바이트)이 있었다. 사업계획서를 쓸 때 AdMob 파일에서 겪었던 문제를 이번에는 우리 코드에서 또 겪었다. 파일 앞에 형식이 적혀 있었는데도 그걸 보지 않고 읽고 있었다.

그래서 첫 두 바이트를 보고, 거기에 맞는 디코더로 글자를 읽게 했다.

TypeScript
const utf16le = buf[0] === 0xff && buf[1] === 0xfe;
const text = utf16le ? new TextDecoder("utf-16le").decode(buf) : new TextDecoder("utf-8").decode(buf);

행사 전날로 찍힌 설치

글자를 제대로 읽고 나니 이번에는 날짜가 맞지 않았다. 7월 4일 서울게임타운 행사(한국 낮)에 생긴 설치 52건 중 43건이 Play 파일에는 7월 3일로 찍혀 있었다. 파일만 보면 행사 설치가 대부분 전날에 생겼다. 9월 19일 행사도 9월 18일 19건, 9월 19일 11건으로 갈렸다.

한국 낮은 미국 서부로는 전날 밤이다. 파일 날짜를 하루 뒤로 옮겨 보니, GA4 안드로이드 첫 실행(first_open, 설치하고 앱을 처음 연 기록)과의 상관이 0.34에서 0.90으로 올랐다. 상관은 두 숫자가 날짜별로 얼마나 함께 움직이는지 보는 값이다.

그래서 어드민 화면과 디스코드 주간 리포트에서는 Play 날짜를 하루 뒤로 옮겨 쓴다. 다만 데이터를 쌓는 테이블에는 파일 날짜를 그대로 두고, 읽을 때만 옮긴다. 나중에 규칙이 바뀌어도 원본이 남도록 했다.

3~7일 늦게 채워지는 파일

설치 파일은 3~7일 늦게 채워진다. 그대로 "지난 4주 vs 그 전 4주"를 비교하면 지금 기간 끝의 며칠이 비어서, 늘 줄어든 것처럼 보인다.

그래서 Play 숫자는 들어온 날까지 잘라서 보고, 비교 기간도 같은 날 수로 센다. 화면에도 "Google Play는 설치 10월 13일까지 들어왔어요"처럼 적어 뒀다.

콘솔과 파일의 다른 지표

날짜를 맞춰도 콘솔 화면과 파일 숫자가 같아지는 건 아니었다. 2026년 6월부터 Play Console 화면의 스토어 지표는 "획득"에서 "클릭", "클릭률"로 바뀌었는데, 버킷 CSV는 예전 지표(획득)만 계속 준다.

Cloud Storage CSV는 원래 지표만 계속 제공한다. 출처: Play Console 도움말, 2026-10-03 캡처
Cloud Storage CSV는 원래 지표만 계속 제공한다. 출처: Play Console 도움말, 2026-10-03 캡처

그래서 콘솔에서 보는 숫자와 대시보드 숫자가 달라도 오류는 아니다. 대시보드에는 어떤 숫자를 보여 주는지 알 수 있도록 "획득 기준"이라고 적어 뒀다.

테스트 기기가 거의 없는 Play 최초 설치

5편에서 자세히 쓰지만, GA4 안드로이드 첫 실행에는 구글 플레이가 새 버전마다 돌리는 자동 테스트 기기가 섞여 있다. 9월 GA4 안드로이드 첫 실행이 실사용 129회, 테스트 기기 108회였는데, Play 최초 설치는 87명이었다.

9월 숫자를 비교해 보니 Play 최초 설치에는 이런 테스트 기기가 섞여 있지 않은 것 같았다. 다만 테스트 기기를 빼더라도 첫 실행이 설치보다 많을 이유는 있다. GA4 첫 실행은 다시 깔아도, 앱 데이터를 지워도 또 세지만, Play 최초 설치는 사람마다 처음 한 번만 센다.

아직 못 받는 스토어 노출

설치 숫자는 받았지만 스토어 노출은 아직 남아 있었다. Play Console 통계 화면에는 2026년 7월부터 노출수(총, 사용자, 기기)가 나라별로 있고, 구글도 "Play Console, Play Developer Reporting API에 냈다"고 발표했다. 그래서 10월 8일, 우리 서비스 계정으로 받을 수 있는 방법을 전부 확인했다.

경로결과
통계 버킷설치, 비정상 종료, 평점, 스토어 등록정보, 리뷰뿐. 노출 없음
Play Developer Reporting API품질 지표와 출시 정보뿐. 노출 자원은 404
Play Developer API (androidpublisher)출시, 리뷰, 결제 관리뿐
BigQuery 데이터 전송버킷과 같은 리포트를 옮길 뿐. 노출 표 없음
GA4의 Google Play 연결인앱 결제, 구독 이벤트만
콘솔 통계 화면 "보고서 내보내기"여기서만 나온다

여섯 경로를 다 확인했는데, 노출수는 사람이 콘솔에서 누르는 "보고서 내보내기" 버튼 하나에만 있었다. 그래서 지금은 사람이 내보내야 한다는 결론을 내렸다. 로그인된 콘솔 화면에서 자동으로 가져오려면 팀 계정으로 계속 대신 접속해야 하고, 화면이 바뀌면 수집도 멈출 수 있어서 이 방법은 쓰지 않았다.

사람이 매달 파일을 올리게 하면 차트가 중간에 끊길 수 있었다. 그렇게 끊긴 차트는 숫자가 없는 것보다 더 헷갈린다고 봐서, 파일을 올리는 칸도 만들지 않았다.

Reporting API로 받는 출시 버전

노출은 남겨 뒀지만, 차트에 이날 안드로이드 업데이트가 나갔다는 표시는 넣고 싶었다. 처음에는 설치 파일에서 새 버전이 처음 퍼진 날을 찾으려 했다. 그런데 기존 사용자 업데이트 칸이 늘 0이라 새로 깐 사람만 보였다. 이대로면 출시일 대신 설치가 몰린 날(행사 날)이 잡힌다.

대신 앞에서 켠 Play Developer Reporting API의 fetchReleaseFilterOptions로 지금 프로덕션(실제 사용자에게 나가는 출시 트랙)에 나가 있는 버전을 매일 확인한다. 바뀐 걸 처음 본 날의 전날을 출시일로 넣는다. "앱 정보 보기" 권한으로 쓸 수 있고, API만 켜 두면 된다. 이렇게 찾은 출시일은 1편의 일정 달력과 차트 마름모에 자동으로 들어간다.


이제 Google Play 설치 숫자는 매일 파일로 받아 쌓이지만, 스토어 노출은 아직 사람이 콘솔에서 내보내야 한다. App Store는 또 달랐다. 리포트를 요청하면 숫자 대신 404가 돌아왔는데, 판매가 없는 날과 아직 리포트가 안 나온 날이 같은 404였다. 다음 편에는 그 404를 구분해 가며 다운로드와 노출 수를 받은 과정을 적었다.

다음 편: 4편 App Store Connect API로 경찰과 도둑 앱 다운로드와 노출 수 받기

keep reading

comments

자유롭게 생각을 남겨주세요.