작업 일지

GA4를 걷어내고 수집기를 직접 만든 이유

이유는 둘이었다. Firebase는 프로젝트당 앱 30개까지고, 화면 체류 시간은 몇 달째 틀린 숫자였다. 엣지 수집기 하나, 집계 하나, 클라이언트 둘. 그리고 첫 데이터의 대부분은 사람이 아니었다.

2분#analytics#ios#android#infra

첫 번째 이유: 30개

Firebase는 프로젝트당 앱 30개다. 프로젝트와 GA4 속성은 1:1. 앱을 계속 만드는 한 언젠가 쪼개야 하고, 쪼개는 순간 같은 제품의 iOS와 Android가 다른 속성으로 갈라진다. 계측이 있는 이유가 그 둘을 나란히 보는 건데, 그게 깨진다.

쿼터 증설을 요청하는 대신 계측을 직접 갖기로 했다. 이식 앱들은 Firebase에서 아예 뺐다.

두 번째 이유: 몇 달째 틀린 숫자

7월에 화면별 체류 시간이 이상하다는 걸 알았다. "LogoForm은 페이월에 78%", "Pro2Camera는 69%", "Artoon은 58%". 사용자가 페이월을 그렇게 오래 볼 리가 없다.

원인은 GA4 설정이 아니라 앱 코드였다. 화면 추적이 onAppear에서 한 줄 호출하는 게 전부였고 onDisappear가 없었다. 페이월은 모든 앱에서 .sheet로 뜬다. 시트를 닫아도 SwiftUI는 부모 뷰의 onAppear를 다시 부르지 않는다. Firebase는 마지막 screen_view를 현재 화면으로 들고 있다가 이후 engagement를 거기에 붙인다. 페이월을 닫고 에디터에서 쓴 시간이 전부 페이월로 적립된 것이다.

모달을 닫을 때 이전 화면을 복원하도록 고쳤지만, 그 전까지의 화면 체류 분석은 전부 과대계상이었다. 숫자 하나를 몇 달 믿었다.

만든 것

조각 무엇
수집 엣지 Cloudflare Worker + D1. 앱이 이벤트를 보내는 곳
집계·리포트 기존 SaaS 안의 Go 패키지
클라이언트 iOS 패키지 하나, Flutter 패키지 하나

설계에서 정한 것들.

  • app_id는 스토어 리스팅이 아니라 제품 하나에 문자열 하나. iOS와 Android가 같은 값을 보내야 합쳐진다.
  • 국가는 엣지가 연결에서 읽는다. 기기는 보내지 않는다.
  • IP, 광고 ID, 정밀 위치, 개인 식별정보는 구조적으로 못 받는다. 컬럼이 없고, 속성 키에 그런 단어가 들어오면 막는다.
  • 수집 URL이 설정되지 않으면 클라이언트는 조용히 꺼진다. 없는 주소로 큐를 쌓으면 정작 배포된 날 보낼 이벤트가 용량 초과로 밀려난다.
  • 화면 체류는 enter/exit 짝으로 잰다. 그 실수는 다시 하지 않는다.

첫 데이터의 대부분은 사람이 아니었다

수집이 돌기 시작한 첫 주, 목록을 액면대로 읽으면 "심사도 안 끝난 앱에 유저가 있다"였다. 지문을 나눠 보니 셋이었다.

정체 지문
Play 사전 출시 보고서 로봇 같은 빌드 ID에 install_id 여러 개, 국가 US, 로케일 전부 en-US, 하루에 몰림, D1 리텐션 0, 세션당 화면 진입 수십 회
Apple 심사 리뷰어 앱당 정확히 1세션·1유저, US, 체류 3~4분, 버전이 아직 미출시인 심사 제출 버전
나 KR, ko_KR, 실기기 OS 버전, 세션 한두 개

LogoForm 30일. iOS는 1세션 1유저 KR. Android는 12세션 8유저 전원 en-US, 빌드 ID 2종, 페이월 진입 182회. 둘이 합쳐져 "13세션에 페이월 165회"라는 말이 안 되는 행이 됐다.

지금은 이 셋을 나눠서 본다. 계측의 첫 일은 사람을 세는 게 아니라 사람이 아닌 걸 빼는 거였다.