제품 지표를 보기 시작하면 가장 먼저 부딪히는 문제가 있다. 숫자는 찍히는데, 그 숫자가 고객 행동인지 테스트 행동인지 헷갈린다.
Place2Page의 분석 환경을 정리하면서 GA4, Mixpanel, Search Console도 같이 정리했다. 이때 제품 분석에 넣을 트래픽과 제외할 트래픽을 먼저 나눠야 했다. 특히 초기 제품에서는 트래픽 모수가 작아서, 테스트나 운영 확인 몇 번만으로도 전환율과 funnel 숫자가 쉽게 흔들린다.
처음에는 Mixpanel 쪽만 생각했다. 관리 화면에서 Mixpanel event를 안 보내면 된다고 봤다. 그런데 다시 보니 문제는 특정 SDK 하나가 아니었다. GA4도 있고, Mixpanel도 있고, 로그인 상태에 따라 user identity도 붙는다. 한쪽만 막으면 나중에 숫자를 볼 때 다시 의심하게 된다.
그래서 이번 작업의 기준은 단순하게 잡았다.
제품 지표에서 제외해야 하는 조건은 한 곳에서 판단한다.
왜 wrapper에서 막아야 했나
분석 도구를 붙일 때는 보통 이벤트 이름과 property schema를 먼저 본다. 어떤 CTA를 눌렀는지, 어떤 단계에서 이탈했는지, 어떤 URL을 제출했는지 같은 것들이다.
하지만 테스트성 트래픽을 빼는 문제는 schema보다 앞에 있다. 애초에 보내지 말아야 하는 이벤트면, 이벤트 이름을 잘 정리해도 소용이 없다.
이번에 제외해야 했던 것은 두 가지였다.
- 제품 분석 대상이 아닌 관리성 화면
- 제품 사용자가 아닌 운영·테스트 주체
경로 기준은 비교적 쉽다. 현재 화면이 제품 사용 흐름이 아니라 관리성 흐름이면 GA4와 Mixpanel 모두 보내지 않으면 된다.
사용자 기준은 조금 더 조심해야 했다. 브라우저가 처음 로드될 때는 아직 로그인 상태가 정해지지 않았을 수 있다. 즉, "이 사용자를 제품 지표에 포함해도 되는가?"라는 질문은 auth state가 resolve된 뒤에야 확정된다.
그래서 route suppression과 user suppression을 같은 helper에 두되, user suppression은 auth state가 바뀔 때 갱신하게 했다.
GA4는 이벤트만 막으면 부족했다
처음에는 기존 trackEvent wrapper에서 return early 하면 충분하다고 생각하기 쉽다.
if (shouldSuppressAnalytics()) {
return
}하지만 GA4는 wrapper로 보내는 이벤트만 문제가 아니다. gtag script가 이미 페이지에 들어가면 자동으로 page/session 수집이 일어날 수 있다.
그래서 GA4는 두 겹으로 막았다.
첫 번째는 script 자체를 렌더링하지 않는 것. 관리성 화면에서는 GoogleAnalytics client component가 gtag script를 넣지 않는다.
두 번째는 이미 브라우저에 GA가 준비된 뒤 auth state가 바뀌는 경우다. 제품 분석에서 제외할 사용자로 확인되면 window["ga-disable-${measurementId}"]를 켠다. 이러면 wrapper event뿐 아니라 해당 measurement id로 나가는 GA 수집도 끊을 수 있다.
이 구조가 마음에 드는 이유는 책임이 분명해서다.
- route suppression은 script render 단계에서 막는다.
- event suppression은 wrapper 단계에서 막는다.
- identity suppression은 auth state가 resolve된 뒤 disable flag로 막는다.
Mixpanel은 더 앞에서 막았다
Mixpanel은 event를 안 보내는 것만으로 끝내지 않았다. 제품 분석 대상이 아닌 화면에서는 SDK init 자체를 하지 않고, session replay도 시작하지 않는다.
이유는 간단하다. 운영 확인용 흐름은 제품 분석 대상이 아니다. 그러면 track()만 막을 게 아니라 init, identify, reset, replay start/stop까지 같은 기준으로 no-op 되는 편이 낫다.
이렇게 해두면 나중에 다른 화면에서 Mixpanel 함수를 호출하더라도, suppressed route나 suppressed user에서는 같은 정책이 적용된다.
route/auth/action
-> shared suppression helper
-> GA4 wrapper or Mixpanel wrapper
-> provider call or no-op핵심은 GA4와 Mixpanel이 서로 다른 조건문을 갖지 않게 만드는 것이다. 분석 도구는 보통 시간이 지나면서 조금씩 붙는다. 이때 "GA는 막았는데 Mixpanel은 열려 있다"거나, 반대로 "Mixpanel은 막았는데 GA realtime에 보인다" 같은 상태가 되면 지표를 믿기 어렵다.
auth state와 연결한 부분
사용자 기준 suppression은 auth state에 연결했다.
로그인 상태가 바뀌면 현재 사용자를 shared suppression helper에 전달한다. suppressed user로 판단되면 GA user id를 세팅하지 않고, Mixpanel identify도 호출하지 않는다.
여기서 일부러 보안 기능처럼 다루지는 않았다. 이건 접근 제어가 아니라 분석 수집 정책이다. 실제 권한 판단은 여전히 backend와 token verification 쪽 책임이다.
다만 분석 지표 관점에서는 효과가 크다. 테스트나 운영 확인 과정에서 생긴 행동이 제품 funnel에 섞이지 않는다.
검증한 것
이번 변경은 테스트를 먼저 넓혔다. 단순히 "브라우저에서 안 보이는 것 같다"로 끝내면 나중에 다시 깨지기 쉽다.
확인한 것은 다음과 같다.
- 관리성 화면에서는 GA4 script가 렌더링되지 않는다.
- 관리성 화면에서는 GA4 page visit과 event wrapper가 no-op 된다.
- 제외 대상 사용자로 auth state가 resolve되면 GA disable flag가 켜진다.
- Mixpanel init, identify, track, reset, session replay가 suppressed state에서 no-op 된다.
- 사용자 식별 기준은 대소문자 차이로 흔들리지 않는다.
- 일반 public route와 anonymous traffic은 계속 수집된다.
로컬에서는 web test, lint, typecheck를 모두 돌렸다.
cd apps/web && npm test
cd apps/web && npm run lint
cd apps/web && npm run typecheck남은 주의점
사용자 기준 suppression은 auth state가 resolve된 뒤에 확정된다. 그래서 제외 대상 사용자가 public page를 열었는데 아직 auth state를 모르는 아주 초기 순간에는 anonymous event가 먼저 찍힐 수 있다.
지금 단계에서는 괜찮다고 봤다. 당장 제거하려는 것은 관리성 화면의 트래픽과 로그인된 테스트성 행동이다. 만약 나중에 제외 대상 사용자의 public page 접근까지 더 강하게 막아야 한다면, analytics provider를 로드하기 전에 서버가 내려준 분석 제외 flag를 확인하는 구조가 필요하다.
이번 작업에서 얻은 기준은 이렇다.
분석 코드는 이벤트를 잘 보내는 코드이기도 하지만, 보내지 말아야 할 것을 정확히 안 보내는 코드이기도 하다. 특히 초기 제품에서는 이 두 번째 기준이 지표 신뢰도를 더 크게 좌우한다.
