AdMob API 갱신 토큰으로 경찰과 도둑 앱 광고 지표 매일 받기
어드민 지표 대시보드를 만든 과정을 일곱 편으로 나눠 적었다. 이 글은 6편이다.
- 경찰과 도둑 앱 지표를 골라 매일 모으기로 한 이유
- 인스타그램 API로 경찰과 도둑 앱 하루치 지표를 구해 디스코드로 보내기
- 서비스 계정으로 경찰과 도둑 앱의 Google Play 통계 CSV 받기
- App Store Connect API로 경찰과 도둑 앱 다운로드와 노출 수 받기
- GA4 Data API로 경찰과 도둑 앱과 웹사이트 지표 모으기
- AdMob API 갱신 토큰으로 경찰과 도둑 앱 광고 지표 매일 받기
- Supabase로 경찰과 도둑 앱 지표 대시보드 만들기
3편에서 만든 서비스 계정을 AdMob에도 넣으면 끝날 줄 알았다. 그런데 AdMob은 서비스 계정을 받지 않는다. 광고 숫자를 매일 가져오려면 이번에는 팀 구글 계정으로 직접 허락하는 OAuth 과정을 거쳐야 했다.
서비스 계정을 안 받는 AdMob
GA4와 Play는 사람 대신 프로그램이 쓰는 서비스 계정의 이메일을 "사용자"로 초대하면 됐다. 그래서 'AdMob도 똑같이 하면 되겠지.' 싶었다. 그런데 AdMob API는 사람 계정이 OAuth로 허락한 토큰만 받는다.
그래서 팀 구글 계정으로 한 번 로그인해, 이 프로그램이 내 AdMob 보고서를 읽어도 된다고 허락해야 했다. 허락하고 나면 구글이 갱신 토큰(refresh token)을 준다. 새 액세스 토큰을 계속 받아 올 때 쓰는 토큰이다.
매일 숫자를 모으는 수집기(7편)는 이 갱신 토큰으로 수명이 짧은 액세스 토큰을 매일 새로 받는다. API를 호출할 때는 이 액세스 토큰을 쓴다.
[처음 한 번, 사람이]
브라우저에서 팀 계정으로 로그인 → "보고서 읽기 허락" → 인증 코드 → 갱신 토큰
[매일, 수집기가]
갱신 토큰 → https://oauth2.googleapis.com/token → 액세스 토큰(1시간) → AdMob API
갱신 토큰이 있으면 그 사람 계정으로 허락받은 범위 안에서 계속 데이터를 가져올 수 있다. 그래서 서비스 계정 키만큼 조심해서 다뤘다.
따라 하기 0. AdMob API 켜기
3편에서 만든 구글 클라우드 프로젝트에서 AdMob API를 켠다. API 및 서비스 → 라이브러리 → "AdMob API" → 사용.

따라 하기 1. 동의 화면을 외부, 프로덕션으로 설정하기
OAuth 클라이언트를 만들기 전에, 로그인한 사람이 보고서 읽기를 허락할 때 뜨는 "동의 화면"을 먼저 설정한다. 지금 구글 클라우드에서는 메뉴 이름이 Google 인증 플랫폼이다.
- 메뉴 → "Google 인증 플랫폼"
- 처음이면 "시작하기"를 눌러 앱 이름, 지원 이메일을 넣는다
- 왼쪽 "대상"에서 사용자 유형과 게시 상태를 본다

먼저 사용자 유형을 정했다. "내부"는 Google Workspace 조직 안 사람만 쓸 수 있는데, 우리 팀 계정은 일반 Gmail이라 "외부"를 골랐다.
다음은 게시 상태였다. "테스트" 상태에서 받은 갱신 토큰은 7일 뒤 만료된다. 그대로 두면 일주일마다 사람이 다시 로그인해서 토큰을 바꿔야 한다.
매일 자동으로 돌아가는 수집기를 만들려는데, 매주 사람이 다시 로그인해야 하면 조금 번거로울 것 같았다. "프로덕션으로 게시"를 누르면 이 7일 제한이 없어지기 때문에, 우리는 프로덕션으로 게시해 두고 갱신 토큰을 받았다.
따라 하기 2. OAuth 클라이언트를 데스크톱 앱으로 만들기
-
왼쪽 "클라이언트" → "클라이언트 만들기"
-
애플리케이션 유형: 데스크톱 앱
-
이름: 알아보기 쉽게. 우리는
metrics-collector-admob
애플리케이션 유형은 데스크톱 앱 -
"만들기"를 누르면 클라이언트 ID와 비밀이 나온다. JSON으로 내려받아 둔다

웹 대신 데스크톱 앱
우리는 내 컴퓨터에서 한 번만 허락을 받으면 됐다. 그 뒤로는 갱신 토큰으로 서버가 매일 숫자를 가져올 수 있다. 그래서 굳이 서버에 별도 페이지를 만들기보다, 내 컴퓨터에서 잠깐 작은 서버를 띄워 인증 코드를 받기로 했다.
OAuth에서는 허락이 끝난 뒤 인증 코드를 돌려받을 리디렉션 주소를 정해야 한다. 웹 애플리케이션 유형은 미리 등록한 실제 서버 주소로 받는다. 데스크톱 앱 유형은 내 컴퓨터를 가리키는 http://localhost로 받을 수 있고, 포트도 자유롭게 정할 수 있다. 그래서 우리 상황에는 데스크톱 방식이 잘 맞았다.
비워 둔 데이터 액세스 범위

범위는 보고서 읽기처럼 어디까지 허락받을지를 정한다. 이 범위는 허락을 요청할 때 주소에 넣어서 보낸다. 여기 목록은 앱 검토를 받을 때 우리 앱이 쓰는 범위를 신고하는 곳에 가깝다. 그래서 동의 화면 설정의 "데이터 액세스"에는 범위를 하나도 안 넣었다.
따라 하기 3. 내 컴퓨터에서 갱신 토큰 받기
OAuth Playground 같은 웹 도구로 토큰을 받는 방법도 많이 소개된다. 하지만 우리는 토큰이 화면이나 다른 사이트를 거치지 않고 바로 파일로 저장되게 하고 싶었다. 그래서 내 컴퓨터에서 Node 스크립트 하나로 받았다.
허락이 끝나면 브라우저가 내 컴퓨터로 돌아오게 하는 방식(로컬 루프백)을 쓰고, 여기에 PKCE를 더했다. PKCE가 무엇인지는 스크립트 순서를 적은 뒤에 설명한다. 아래는 우리가 쓴 스크립트가 하는 일을 순서대로 적은 것이다.
1. 스크립트가 내 컴퓨터에 작은 HTTP 서버를 띄운다 (http://localhost:<포트>)
2. 무작위 code_verifier를 만들고, 그 SHA-256 해시(code_challenge)를 준비한다
3. 무작위 state를 만든다
4. 브라우저로 구글 허락 주소를 연다
https://accounts.google.com/o/oauth2/v2/auth
?client_id=...
&redirect_uri=http://localhost:<포트>
&response_type=code
&scope=https://www.googleapis.com/auth/admob.report
&access_type=offline ← 갱신 토큰을 달라는 뜻
&prompt=consent ← 예전에 허락한 적이 있으면 넣는다. 다시 물어야 갱신 토큰이 새로 나온다
&code_challenge=<해시>&code_challenge_method=S256
&state=<무작위>
5. 팀 계정으로 로그인하고 허락하면, 브라우저가 localhost로 돌아오며 ?code=...&state=... 를 준다
6. 스크립트가 state가 같은지 확인하고, code와 code_verifier를 토큰 주소로 보낸다
7. refresh_token을 받아 비공개 폴더 파일에 저장한다. 화면에는 찍지 않는다
state와 PKCE
브라우저가 localhost로 돌아오면 스크립트는 함께 돌아온 state가 처음 보낸 값과 같은지 확인한다. 다른 사이트가 우리 localhost 주소로 엉뚱한 코드를 보내는 걸 막기 위해서다.
인증 코드 자체를 누가 가로채는 경우도 막아야 했다. 그래서 PKCE에서는 구글에 code_challenge라는 해시만 보내고, 원본인 code_verifier는 내 컴퓨터에만 남긴다. 코드를 토큰으로 바꿀 때 원본도 보내야 하므로, 인증 코드만 가로채서는 토큰을 받을 수 없다.
클라이언트를 만들 때 비밀도 받았지만, 데스크톱 앱의 클라이언트 비밀은 사실상 누구나 꺼낼 수 있는 값이다. 앱에 들어가 함께 배포되기 때문이다. 그래서 데스크톱이나 모바일 앱에는 PKCE를 같이 쓰라고들 하고, 우리도 PKCE를 넣었다.
보고서 읽기만 허락
AdMob API에는 계정 설정까지 읽는 admob.readonly 범위도 있지만, 우리는 보고서만 읽으면 됐다. 그래서 admob.report 하나만 받았다. 3편에서 서비스 계정에 역할을 하나도 안 준 것과 같은 이유였다.
이렇게 받은 갱신 토큰과 클라이언트 ID, 비밀은 Vercel 환경변수에 넣어 코드 밖에 따로 두었다.
| 환경변수 | 값 |
|---|---|
ADMOB_CLIENT_ID | 클라이언트 JSON의 client_id |
ADMOB_CLIENT_SECRET | 클라이언트 JSON의 client_secret |
ADMOB_REFRESH_TOKEN | 방금 받은 갱신 토큰 |
매일 networkReport로 광고 숫자 받기
이제 수집기는 매일 갱신 토큰으로 액세스 토큰을 받고, 그 토큰으로 네트워크 보고서를 요청한다. 날짜와 함께 플랫폼, 광고 형식, 나라별로 수익과 요청 수, 노출 수 등을 받는다.
POST https://admob.googleapis.com/v1/accounts/<퍼블리셔 ID>/networkReport:generate
Authorization: Bearer <액세스 토큰>
{
"reportSpec": {
"dateRange": { "startDate": { "year": 2026, "month": 10, "day": 10 },
"endDate": { "year": 2026, "month": 10, "day": 16 } },
"dimensions": ["DATE", "PLATFORM", "FORMAT", "COUNTRY"],
"metrics": ["ESTIMATED_EARNINGS", "AD_REQUESTS", "MATCHED_REQUESTS", "IMPRESSIONS", "CLICKS"]
}
}
요청 주소에는 내 AdMob 계정을 가리키는 퍼블리셔 ID(pub-로 시작)를 넣는다. 이 값은 AdMob 설정의 계정 정보에 있다.
그런데 막상 응답을 받아 보니 JSON 객체 하나 대신 여러 칸으로 나뉜 배열이 왔다. 첫 칸은 머리(header), 중간 칸들은 줄(row), 마지막 칸은 꼬리(footer)여서 row가 있는 칸만 골라 썼다.
값을 읽을 때는 단위와 날짜도 맞춰야 했다. 수익은 microsValue에 마이크로 단위로 오므로 100만으로 나눠야 달러가 된다. 날짜는 AdMob 계정 시간대를 따르는데, 우리 계정은 한국 시간이었다.
추정 수익은 나중에 조금씩 바뀌기 때문에 최근 7일은 매일 다시 받는다. 실제로 대조해 보니 차이는 있었다. 0.00003달러였다.
비율 대신 개수 저장
날짜나 나라를 합쳐 볼 때 저장된 비율끼리 평균을 내면 실제 비율과 다른 값이 나온다. 그래서 매치율, 노출률 같은 비율은 저장하지 않고 요청 수, 매치된 요청 수, 노출 수만 저장하기로 했다.
월요일 요청 100건 중 50%가 노출되고, 화요일 요청 2건 중 100%가 노출됐다고 해서 전체 노출 비율이 75%가 되지는 않는다. 102건 중 52건이니 51%다. 그래서 개수를 저장해 두고, 볼 때마다 합친 개수로 나누기로 했다.
갱신 토큰이 끊겼을 때
수집기를 매일 돌려도 갱신 토큰이 끊기는 경우가 있다. 수집이 이런 오류로 실패하면 갱신 토큰이 끊긴 것이다.
invalid_grant
| 원인 | |
|---|---|
| 팀 계정 비밀번호를 바꿨다 | 구글이 그 계정의 갱신 토큰을 끊는다 |
| 구글 계정 설정에서 앱 허락을 취소했다 | 당연히 끊긴다 |
| 동의 화면이 "테스트" 상태다 | 7일마다 끊긴다 |
그때는 따라 하기 3을 다시 하고 ADMOB_REFRESH_TOKEN만 바꾸면 된다. 반년 뒤에는 내가 잊어버릴 것 같아서, 이 방법을 저장소 문서에 "AdMob 다시 허락하기"로 적어 뒀다.
매치율과 노출률 나눠 보기
숫자를 가져온 뒤에는 매치율과 노출률을 어떻게 구분해 읽어야 할지 살펴봤다. 그러다 사업계획서에 쓴 숫자까지 다시 확인하게 됐다.
실제로 보인 비율이 아닌 fill rate
광고 관련 글을 보면 fill rate를 광고가 실제로 보인 비율처럼 쓰는 경우가 많다. 그런데 AdMob에서는 fill rate를 매치율의 다른 이름으로 쓴다.
| 지표 | 뜻 |
|---|---|
| 요청 | 앱이 "광고 주세요" 한 횟수 |
| 매치율 | 요청 중 광고가 돌아온 비율 (= fill rate) |
| 노출률 | 돌아온 광고 중 실제로 화면에 보인 비율 |
앱이 광고를 요청해서 받아 오는 것과, 받은 광고를 화면에 보여 주는 것은 따로 봐야 했다. 그래서 요청 대비 실제 노출 비율은 매치율 × 노출률로 구했다.
틀린 줄 알았던 사업계획서 숫자
9월 사업계획서에는 광고 요청 대비 실제 노출 비율이 네이티브(앱 화면 안에 섞여 보이는 광고) 62%, 전면(화면 전체를 덮는 광고) 29%라고 썼다. 그런데 정의를 보다가 이 값은 매치율 × 노출률이고, 노출률과는 다른 숫자라는 걸 알았다. '아 계획서에 틀린 숫자 냈다.' 싶었다.
정정 메모까지 쓸 뻔했는데, 9월 21~27일 원본 파일을 다시 합쳐 보니 매치율이 네이티브(60/60), 전면(14/14) 모두 100%였다. 매치율이 100%면 매치율 × 노출률은 노출률과 똑같다. 그래서 계획서에 쓴 숫자는 맞았다.

그런데 두 비율을 나눠 보니 앱에서 광고를 다 보여 주지 못하고 있었다. 광고는 요청할 때마다 돌아왔다. 전면은 받아 온 광고 열 번 중 일곱 번꼴로 화면에 띄우지 못했고(71%), 네이티브도 38%가 화면에 나오지 못했다.
그러니 고쳐야 할 곳은 앱에서 광고를 띄우는 시점과 화면이었다. 그래서 대시보드에서도 이 차이를 볼 수 있도록 매치율과 노출률을 나눠 보여 주기로 했다.
AdMob 화면과 다른 어드민 표기
이 숫자는 AdMob 보고서 → 광고 활동 보고서에서 광고 소스를 "AdMob 네트워크"로 고르면 확인할 수 있다. 그런데 이 화면과 어드민은 같은 비율을 다른 이름으로 적고 있었다.


10월 17일 화면에서는 매치율이 "일치율", 노출률이 "노출률"로 나왔다. 그런데 작업 기록을 보면 10월 초에 나는 "노출률은 AdMob 화면에 없는 말이고 공식 표기는 게재율"이라고 적어 뒀다. 그래서 어드민 화면도 "게재율"로 만들었다.

그 사이 AdMob 표기가 바뀐 건지, 내가 다른 화면을 보고 적은 건지는 지금 확인할 길이 없다. 어드민에는 "게재율은 AdMob 표기예요"라는 설명까지 적어 뒀는데, 지금 화면에 맞게 고쳐야 한다.
이제 AdMob 광고 숫자도 매일 갱신 토큰으로 가져온다. 그런데 모아 둔 숫자를 화면으로 가져오는 과정에서 1,000줄까지만 오고 나머지가 아무 오류 없이 잘리는 일이 있었다. 그 잘린 숫자를 찾아낸 일과, Supabase에 쌓은 지표를 어드민 화면 하나로 모은 과정이 마지막 편이다.
keep reading
comments
자유롭게 생각을 남겨주세요.
