회고 · 대시보드 · GA4

GA4 Data API로 경찰과 도둑 앱과 웹사이트 지표 모으기

박찬빈

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

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

3편에서 만든 서비스 계정을 GA4에도 그대로 쓴다. 사람 대신 프로그램이 쓰는 구글 계정이다. 설정은 금방 끝났는데, 막상 받은 숫자를 믿을 수 있게 만드는 데는 생각보다 오래 걸렸다. 그래서 이 글은 숫자를 받아 온 뒤의 이야기가 조금 길다.

웹에 GA가 없다는 오해

시작부터 잘못 짚었다. 웹 코드에서 gtag를 검색했는데 아무것도 안 나와서 '웹에는 GA가 없구나. 이것부터 넣어야겠네.' 하고 생각했다.

그런데 다시 보니 웹은 구글 태그 매니저(GTM)로 GA4에 데이터를 보내고 있었다. app/layout.tsx에 GTM 스니펫이 있고, 커스텀 이벤트는 lib/analytics.ts에서 dataLayer에 넣고 있었다. GTM을 거치면 GA4 설정은 GTM 화면 안에 있어서 코드에 gtag가 안 보인다.

gtag로 검색하면 아무것도 안 나오지만, GA4는 GTM을 거쳐 붙어 있었다
gtag로 검색하면 아무것도 안 나오지만, GA4는 GTM을 거쳐 붙어 있었다

처음에는 gtag만 찾으면 될 줄 알았다. googletagmanager, dataLayer까지 찾아보기도 전에 GA부터 넣으려고 했다.

따라 하기 0. 속성 개수 확인

GA4는 속성(property)마다 숫자가 따로 쌓인다. 그래서 숫자를 받기 전에 어느 속성에 쌓이고 있는지부터 확인했다. 웹과 앱이 따로 있으면 속성마다 설정해야 해서 할 일이 두 배가 된다.

  1. GA4 왼쪽 아래 톱니바퀴 "관리"
  2. 속성 설정 → 데이터 수집 및 수정 → "데이터 스트림"
  3. 목록을 본다

우리는 둘로 나뉘어 있었다.

웹 속성. 스트림은 경찰과 도둑 웹 하나
웹 속성. 스트림은 경찰과 도둑 웹 하나
앱 속성(Firebase 프로젝트와 연결). iOS, Android, 그리고 어드민 웹이 들어 있다
앱 속성(Firebase 프로젝트와 연결). iOS, Android, 그리고 어드민 웹이 들어 있다
속성들어 있는 것숫자가 있는 날
웹공식 사이트 (GTM)4월부터
앱iOS, Android 앱 (Firebase), 어드민 웹6월부터

앱이 Firebase를 쓰면 앱 숫자는 Firebase 프로젝트에 연결된 GA4 속성에 쌓인다. Firebase 콘솔 → 프로젝트 설정 → 통합 → Google Analytics에서 어느 속성인지 확인하면 된다.

이렇게 어느 속성에서 받아야 하는지는 찾았다. 다만 웹과 앱의 시작일이 다르다는 게 나중에 문제가 됐다.

따라 하기 1. 서비스 계정을 뷰어로 넣기

숫자를 받으려면 서비스 계정에 보고서를 볼 권한을 줘야 한다. 속성마다 해야 해서 우리는 두 번 했다.

  1. 관리 → 속성 → "속성 액세스 관리"

  2. 오른쪽 위 파란 +

  3. "사용자 추가"

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

    이메일 주소 칸에 서비스 계정 이메일
    이메일 주소 칸에 서비스 계정 이메일
  5. "신규 사용자에게 이메일로 알림"은 끈다. 서비스 계정은 메일을 못 받는다

  6. 역할은 뷰어

    역할은 뷰어. 보고서 데이터와 설정을 볼 수만 있다
    역할은 뷰어. 보고서 데이터와 설정을 볼 수만 있다
  7. 오른쪽 위 "추가"

속성 액세스 관리 목록 맨 아래에 서비스 계정이 뷰어로 들어간 모습
속성 액세스 관리 목록 맨 아래에 서비스 계정이 뷰어로 들어간 모습

혹시 이 버튼(+)이 안 보이면 관리자 권한이 없는 계정으로 들어온 상태다.

따라 하기 2. 속성 ID 찾기

  1. 관리 → 속성 → "속성 세부정보"
  2. 오른쪽 위 속성 ID(숫자)를 복사한다
속성 세부정보. 오른쪽 위 숫자가 속성 ID다. 보고 시간대도 여기서 본다
속성 세부정보. 오른쪽 위 숫자가 속성 ID다. 보고 시간대도 여기서 본다

G-로 시작하는 측정 ID와는 다르다. 측정 ID는 웹 페이지가 데이터를 보낼 때 쓰는 값이고, API로 숫자를 받을 때는 숫자로 된 속성 ID를 쓴다.

같은 화면의 보고 시간대도 확인해 뒀다. GA4 API가 주는 날짜는 이 시간대 기준이고, 우리 웹 속성은 한국 시간이었다. 속성 ID와 보고 시간대는 비밀로 관리할 값이 아니어서 코드 설정 파일에 두고, 서비스 계정 키 하나만 비밀로 관리한다.

권한을 넣자마자 바로 숫자를 읽을 수 있었다. 10월 5일 처음 요청했을 때 최근 7일 웹 64명, Android 84명, iOS 56명이 나왔다. 3편의 Play에서는 하루 넘게 403(권한 없음)이 나왔는데, 이번에는 바로 됐다.

Data API에 묻기

POST https://analyticsdata.googleapis.com/v1beta/properties/<속성 ID>:runReport
Authorization: Bearer <analytics.readonly 범위 토큰>

{
  "dateRanges": [{ "startDate": "2026-10-10", "endDate": "2026-10-16" }],
  "dimensions": [{ "name": "date" }, { "name": "platform" }],
  "metrics": [{ "name": "activeUsers" }, { "name": "newUsers" }, { "name": "sessions" }]
}

날짜나 플랫폼처럼 무엇을 기준으로 나눌지(dimensions), 사용자나 방문처럼 무엇을 셀지(metrics)를 보내면 표로 돌려준다. 우리는 첫 기준을 늘 date로 두고, 받은 표를 지표마다 한 줄씩 나눠 저장한다.

날짜속성쪼갠 기준키지표값
2026-10-15appplatformAndroidactiveUsers12

위 표의 숫자는 저장하는 모양을 보여 주려고 넣었다. 이렇게 저장하면 새로 보고 싶은 지표가 생겨도 테이블을 바꾸지 않고 줄만 늘리면 된다.

더하면 안 되는 하루 사용자 수

activeUsers, totalUsers로 받은 하루 사람 수는 날짜별로 더하면 안 된다. 월요일 10명, 화요일 10명이어도 이틀 동안 온 사람은 10명일 수도 20명일 수도 있다. 같은 사람이 두 날 모두 왔는지 하루 숫자만으로는 알 수 없기 때문이다.

App Store의 하루 고유 기기 수도 같다(4편). 그래서 일주일 합계를 낼 때는 세션, 이벤트 횟수처럼 더해도 되는 숫자만 더했다. 사람 수가 꼭 필요할 때는 기간을 통째로 GA4에 다시 물었다.

GA에 안 보이는 인스타 유입

받은 숫자를 보니 인스타에서 온 방문이 이상하게 적었다. 인스타가 센 프로필 링크 클릭(bio_link_clicked, 1편)은 30일 동안 44번이었는데, GA4에서 출처가 인스타(ig / social)인 방문은 5세션뿐이었다. 클릭과 세션(한 번 들어와 머문 방문)은 세는 단위가 달라 맞아떨어질 수는 없지만, 그래도 차이가 꽤 컸다.

'클릭은 44번인데 방문은 왜 5번뿐이지?' 하고 출처를 더 확인해 봤다. 프로필 링크가 링크트리를 거쳐서 GA4에는 출처가 linktr.ee로 찍혔다(28세션). 또 인스타 앱 안 브라우저는 어디서 왔는지 담은 정보(referrer)를 자주 지우기 때문에, 그런 방문은 "직접 방문"에 섞인다.

행사 QR과 링크트리 다운로드 링크가 가리키는 /download 페이지에서도 같은 일이 보였다. 이 페이지는 열리자마자 스토어로 넘기는데, 다운로드 클릭 87번 중 55번이 출처 불명이었다.

UTM 꼬리표 붙이기

브라우저가 지우는 건 어디서 왔는지 담은 헤더이고, 주소 자체에 붙은 꼬리표(?utm_source=...)는 그대로 남는다. GA4는 주소에 UTM이 있으면 그걸 출처로 쓴다. 그래서 링크트리 버튼 세 개 주소에 꼬리표를 붙였다.

https://copsandrobbers.app/download?utm_source=instagram&utm_medium=bio&utm_campaign=always&utm_content=download

행사 때는 utm_campaign만 바꾸면 사이트 방문을 행사별로 셀 수 있다. 행사와 자리마다 이런 꼬리표 링크를 나눠 만들고 QR로 뽑는 방법은 1편에 적었다.

링크트리가 덮어쓴 버튼 그림

그런데 꼬리표를 붙이자 링크트리가 이걸 새 주소로 인식해서 그 페이지의 미리보기 이미지와 제목을 다시 가져와 덮어썼다. 다운로드 버튼 제목이 "경찰과 도둑 앱 다운로드"에서 "경찰과 도둑"으로 바뀌었고, 예전 썸네일 파일은 어디에도 남아 있지 않았다.

그래서 예전 화면은 되돌릴 수 없었다. 링크 주소를 바꾸기 전에는 지금 화면부터 캡처해 두는 게 좋겠다.

빠뜨린 인스타 자체 꼬리표

인스타 방문을 셀 때는 출처 ig도 빠뜨렸다. 인스타는 프로필 링크에 스스로 utm_source=ig를 붙인다. 링크트리로 바꾸기 전에는 이 꼬리표로 들어왔다.

그래서 이 출처도 함께 세도록 바꿨다. 그러고 나니 13주 인스타 방문이 35번에서 103번으로 바뀌었다.

유입 경로 탭. 채널별 방문과 스토어로 간 방문. 인스타는 ig, instagram, linktr.ee 출처를 모두 묶는다
유입 경로 탭. 채널별 방문과 스토어로 간 방문. 인스타는 ig, instagram, linktr.ee 출처를 모두 묶는다

원본에 다시 물어본 숫자

대시보드를 다 만들고 나서 화면에 나온 숫자를 GA4 API에 별도로 다시 물어 하나씩 맞춰 봤다. 계산식도 따로 확인했는데, 여기서 틀린 게 여럿 나왔다. 전체 목록은 7편에 있고, 여기에는 GA4에서 나온 것만 적는다.

버튼이 아니었던 클릭 79번

4주 다운로드 버튼 클릭 99번 중 79번이 /download 페이지에서 찍혀 있었다. /download는 열리자마자 스토어로 넘기면서 같은 이벤트를 남기고 있었고, 실제 사이트 버튼 클릭은 20번이었다. 그래서 이름을 "스토어로 이동"으로 바꾸고, 다운로드 링크와 사이트 버튼을 나눠 보여 줬다.

사이트에 온 방문 중 얼마나 스토어로 넘어갔는지 계산할 때도 이 둘을 나눠야 했다. /download로 들어온 방문은 들어오는 순간 무조건 전환으로 잡혀서, 사이트 전환율의 분자와 분모에서 모두 뺐다. 그러자 19.6%였던 사이트 전환율이 3.8%(369 방문 중 14)가 됐다.

그런데 한 방문에서 버튼을 여러 번 누르면 클릭이 방문보다 많아져 전환율이 100%를 넘을 수도 있었다(다른 SNS 채널이 방문 11, 클릭 12). 그래서 분자를 클릭 횟수 대신 스토어로 넘어간 방문 수로 바꿨다.

바로잡고 나니 사이트 전환율이 19.6%에서 3.8%, 5분의 1 정도로 줄었다. 보기에는 아쉬워도 이렇게 고치는 게 맞다고 판단했다.

사이트 전환율은 /download 자동 이동 방문을 빼고 센다. 우리 팀이 개발하며 들어온 방문도 뺐다
사이트 전환율은 /download 자동 이동 방문을 빼고 센다. 우리 팀이 개발하며 들어온 방문도 뺐다

섞여 있던 팀 방문

방문 출처에는 localhost, vercel.com, github.com도 있었다. 개발하면서 들어온 우리 방문이 4주에 28번 잡혀 있었다. 그래서 모든 합계에서 뺐다.

GA4 관리 → 데이터 스트림 → 태그 설정 → "내부 트래픽 정의"에서 우리 IP를 막는 방법도 있다. 다만 이미 쌓인 숫자는 이걸로 지울 수 없어서 대시보드에서 출처로 걸렀다.

판이 아니라 사람이었던 311

4주 게임 시작 이벤트가 311번인데 방 만들기는 79번이었다. 방보다 판이 네 배나 많은 게 맞나 싶어서 앱 코드를 확인했다.

게임 시작 이벤트는 대기실에서 게임이 시작될 때 참가자 폰마다 한 번씩 찍혔다. 5명이 한 판 하면 5가 찍히는 구조였다. 이벤트 이름만 보고 판이라는 단위로 센 게 잘못이었다.

그래서 이 숫자는 "게임 참가(사람 기준)"로 이름을 바꿨다. 실제 판 수와 판당 인원은 백엔드 게임 기록에서 가져오도록 했다(1편에서 제안한 주간 플레이 인원이 판 수 × 판당 인원이다).

하루 합으로 못 세는 재방문

처음 연 사람 중 몇 %가 다음 날 다시 왔는지(리텐션)는 하루 숫자를 더해서 구할 수 없다. 같은 사람이 여러 날 오면 여러 번 세지기 때문이다.

그래서 처음 들어온 날이 같은 사람끼리 묶어서(코호트) GA4에 따로 물었다. runReport에 cohortSpec을 넣으면 이 날 처음 온 사람 중 n일 뒤 다시 온 사람 수를 준다. 4주 코호트 합 342가 신규 사용자, 첫 실행 사용자 수와 같은 걸 확인하고 썼다.

결과는 생각보다 낮았다. 다음 날 다시 온 비율은 7.4%, 일주일 뒤는 2.2%였다. 첫 실행 후 7일 안에 게임에 참가한 비율(활성화율)은 24%였다. 남게 만드는 게 먼저라는 1편의 결론과 같은 방향이었다.

그런데 이후 첫 실행 기록을 확인하다 보니, 이 비율도 다시 계산해야 했다.

절반 가까이가 테스트 기기였던 첫 실행

신규 사용자가 앱 첫 실행 → 로그인 → 방 입장 → 게임으로 얼마나 이어지는지 퍼널을 만들려고 했다. 그 전에 단계마다 숫자가 맞는지 먼저 봤는데, 나라 미상, 기기 정보 없음, Android 11로 찍힌 기록이 새 버전을 올린 날마다 2~7명씩 생겼다. 그중 방에 들어간 사람은 0명이었다.

확인해 보니 구글 플레이가 새 버전을 올릴 때마다 돌리는 출시 전 자동 테스트였다. 구글이 실제 기기 몇 대로 앱을 열어 보고 문제가 없는지 보고서를 만드는데, 그 기기들이 앱을 열면 GA4에는 신규 사용자로 찍힌다.

이 앱은 9월에만 새 버전을 40번 가까이 올렸다. 그만큼 기록이 쌓여서 9월 안드로이드 첫 실행 237회 중 108회(46%)가 테스트 기기였다. 안드로이드 신규 사용자 두 명 중 한 명 가까이는 구글이었다. 첫 실행 숫자뿐 아니라 다시 온 비율과 활성화율의 분모까지 부풀어 있었다.

나라 대신 기기 특징으로 거르기

테스트 기기를 거르려고 처음엔 나라를 기준으로 한국과 일본만 셌다. 이때 적용한 나라 기준은 1편의 10월 6일 퍼널에서 한국만 넣은 기준과는 별개다. 그런데 인도에서 4명 중 3명이 게임까지 간 기록도 있어서, 이렇게 하면 실제 해외 사용자까지 빠졌다. 그래서 테스트 기기만 골라낼 특징을 찾았다.

후보결과
설치 경로(구글 플레이에서 왔나)한국에서 경로 없이 잡힌 46명 중 35명이 방에 들어갔다. 진짜 사람이라 못 쓴다
기기 모델 미상만한국 진짜 사용자 9명이 같이 빠진다
나라 미상, 또는 기기 모델 미상이면서 Android 116월 12일~10월 5일 199명이 걸렸고 그중 방에 들어간 사람 0명. 이걸 골랐다

이 조건에 맞는 기록은 GA4에 숫자를 요청할 때 dimensionFilter로 뺀다. 아래는 코드를 줄여 놓은 것이다.

JSON
{ "notExpression": { "orGroup": { "expressions": [
  { "filter": { "fieldName": "countryId", "stringFilter": { "value": "(not set)" } } },
  { "andGroup": { "expressions": [
    { "filter": { "fieldName": "mobileDeviceModel", "stringFilter": { "value": "(not set)" } } },
    { "filter": { "fieldName": "operatingSystemVersion", "stringFilter": { "value": "11" } } }
  ] } }
] } } }

미국의 기기 모델 미상(Android 16)과 Pixel 6 Pro도 로그인 0명이라 의심스러웠다. 다만 테스트 기기라고 볼 근거가 약해 아직 남겨 뒀다(2주에 19명). 같은 기준을 첫 실행, 다시 온 비율, 활성화율, 퍼널에 모두 적용하고 지난 데이터를 다시 채웠다(146초 걸렸다).

4주(9월 8일~10월 5일)빼기 전뺀 뒤
앱 첫 실행342회260회 (테스트 기기 82회)
활성화율24%39%
다음 날 다시 온 비율7.4%10%

테스트 기기를 빼고 다시 계산하니 비율이 올라갔다. 그렇다고 서비스가 좋아진 건 아니었고, 원래 숫자가 이 정도였다. 그래서 화면에는 뺀 숫자를 숨기지 않고 "테스트 기기로 보이는 n회는 뺐어요"라고 적었다.

앱 첫 실행 카드. 구글 플레이 자동 테스트 기기로 보이는 36회는 뺐다고 적었다
앱 첫 실행 카드. 구글 플레이 자동 테스트 기기로 보이는 36회는 뺐다고 적었다

대시보드에서 거른 이유

테스트 기기에서 수집을 꺼 버리는 방법도 생각했다. 하지만 첫 실행 기록(first_open)은 앱 코드보다 먼저 찍힌다. 막으려면 수집을 기본으로 꺼 둬야 하는데, 그때 실제 사용자의 첫 실행이 제대로 남는지는 문서로 확인할 수 없었다.

잘못되면 안드로이드 신규 숫자가 통째로 사라질 수도 있었다. 이 부분을 확인하지 못한 상태라, 우선 대시보드에서 거르기로 했다.

덤으로 찾은 앱 버그

게임을 끝낸 사람이 시작한 사람보다 많은 날도 있었다. 평소에는 둘 다 137명으로 같은데, 9월 19일 행사 날만 시작 41명, 끝 52명이었다.

앱 코드를 따라가 보니 "게임 시작" 기록은 대기실에서만 남고 있었다. 그런데 행사용 게임은 대기실 없이 바로 시작돼서 시작 기록이 통째로 빠졌다. 그래서 앱 저장소에 이슈를 만들었고, 대시보드에서는 시작이나 끝 중 하나라도 있으면 게임한 사람으로 센다.

남는 사람 따로 세기

남는 사람. 처음 들어온 날로 묶어 GA4에 따로 물은 값이다
남는 사람. 처음 들어온 날로 묶어 GA4에 따로 물은 값이다

이 칸의 숫자들은 모두 코호트로 따로 물은 값이고, 테스트 기기를 뺀 기준이다. 처음 연 날로부터 하루나 7일이 아직 다 지나지 않은 경우는 각 지표에서 뺀다. 아직 늘어나는 숫자를 섞으면 지난 기간과 같은 기준으로 비교하기 어려워서 그렇게 정했다.

그 밖에 알아 둘 것

앞에서 본 대로 웹은 4월, 앱은 6월부터 숫자가 있다. 그런데 둘을 합쳐서 보니, 13주를 고르면 이전 기간(4~7월) 가운데 앱 숫자가 없는 4~5월이 0으로 읽혔다. 그러면 앱 첫 실행이 많이 늘어난 것처럼 보여서, 이전 기간에 숫자가 없는 날이 있으면 증감을 숨기고 "이전 기간 숫자 없음"이라고 적었다.

사이트 방문을 기간 통째로 물으면 430, 날마다 더하면 431이었다. 자정을 넘긴 방문이 이틀에 걸려 두 번 세지기 때문인데, GA4 특성이라 문서에 적어 두고 넘어갔다.

login 이벤트는 6월 17일부터 있다. 그래서 그 전에 처음 연 사람은 퍼널에서 뺐다.

API를 얼마나 썼는지 보여 주는 할당량 기록은 관리 → 속성 → "속성 데이터 API 할당량 기록"에서 볼 수 있다.


이제 웹과 앱 숫자는 서비스 계정으로 받아 대시보드에서 본다. 그런데 같은 방식으로 광고 숫자도 받으려니 AdMob API는 서비스 계정을 받지 않았다. 다음 편은 팀 구글 계정으로 보고서 읽기를 한 번 허락받아, 매일 광고 숫자를 받을 갱신 토큰을 얻은 이야기다.

다음 편: 6편 AdMob API 갱신 토큰으로 경찰과 도둑 앱 광고 지표 매일 받기

keep reading

comments

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