Growth 관련 정보를 한곳에서 보려고 내부 지표 화면을 만들기 시작했다. 일반적인 서버처럼 DB 정보를 쿼리하는 작업이 아니라 Google Analytics, Mixpanel, Google Search Console 같은 외부 source에서 정보를 가져와 합치는 일이어서 생각보다 많이 헤맸다. 그 과정을 기록해둔다.
기획은 노션에 있고, 실제 고객 상태는 DB에 있고, 유입은 GA에 있고, 검색 성과는 Search Console에 있고, 제품 이벤트는 Mixpanel에 있었다. 뭔가 하나를 판단하려면 탭을 여러 개 열어야 했다. "이번 주에 뭘 봐야 하지?"라는 질문에 바로 답하기 어려웠다.
그래서 Growth OS는 계획과 액션이 아니라 결과와 지표를 보여주는 쪽으로 역할을 잡았다.
그런데 GA와 Search Console을 붙이면서 알게 됐다. 외부 분석 데이터를 화면에 붙이는 일은 API 호출 하나를 성공시키는 문제가 아니었다. 인증이 끊길 수 있고, 권한이 빠질 수 있고, 갱신 작업이 조용히 실패할 수 있다. 화면은 그 실패를 그대로 맞으면 안 된다.
이번 작업은 Growth 화면이 외부 분석 API 실패에 덜 흔들리게 만드는 일이었다.
처음에 헷갈린 지점
처음에는 선택지가 cadence 문제처럼 보였다.
- 정해진 시간마다 미리 가져올까.
- 사용자가 볼 때마다 바로 가져올까.
- 저장된 값을 먼저 보여주고, 뒤에서 새 값으로 바꿀까.
하지만 다시 보니 핵심은 cadence가 아니었다. 정해진 시간에 가져오든 사용자가 볼 때 가져오든, 둘 다 같은 불안정한 Google fetch 위에 올라가 있었다. 인증이나 권한이 한 번 막히면 어느 쪽도 안정적이지 않았다.
정해진 시간마다 밀어 넣는 방식도 마음에 걸렸다. 아무도 보지 않는 동안 실패하면 티가 잘 안 난다. 숫자가 오래된 건지, 갱신이 죽은 건지, 단지 방문자가 없었던 건지 구분하기 어렵다.
반대로 매번 실시간으로 가져오면 화면이 외부 API 속도와 quota에 묶인다. Growth cockpit은 숫자를 빠르게 훑어보는 도구다. 들어갈 때마다 분석 API를 기다리게 만들고 싶지는 않았다.
그래서 구조를 바꿨다. 화면은 항상 저장된 값을 먼저 본다. 오래됐거나 비어 있으면 서버가 뒤에서 갱신한다. 실패하면 직전 값을 그대로 둔다.
바꾼 구조
프리셋 기간의 GA/Search Console 데이터는 서버에서 stale-while-revalidate 방식으로 다룬다.
사용자가 Growth OS를 열면 서버는 공유 캐시 테이블에서 최근 값을 읽어 바로 반환한다. 이 응답은 Google API를 기다리지 않는다.
그 값이 오래됐거나 빠진 구간이 있으면 응답을 보낸 뒤 백그라운드에서 한 번만 갱신한다. 성공하면 다음 방문자는 새 값을 보고, 실패하면 기존 값을 계속 본다.
이 구조에서 화면의 약속은 단순하다.
- 먼저 보여준다.
- 오래됐으면 뒤에서 고친다.
- 실패해도 마지막으로 성공한 값을 망치지 않는다.
- 여러 사람이 동시에 들어와도 같은 구간을 중복 갱신하지 않는다.
맨 위 sequence diagram은 이 흐름을 압축한 것이다.
왜 브라우저가 아니라 서버 캐시인가
처음 프론트에서 생각했던 방식도 있었다. 저장된 값을 먼저 보여주고, 브라우저가 조용히 live 값을 가져와 갈아끼우는 방식이다. UX만 보면 괜찮다.
하지만 Growth OS의 숫자는 개인화된 값이 아니다. 같은 팀원이 같은 숫자를 봐야 한다. 그러면 이 데이터의 현재 상태도 브라우저마다 따로 있으면 안 된다.
브라우저가 각자 live fetch를 하면 사용자가 늘어날수록 같은 요청이 반복된다. 실패 처리도 각 브라우저에 흩어진다. 무엇보다 "이 숫자는 언제 갱신됐는가"가 서버의 공통 사실이 아니라 각 탭의 상태가 된다.
그래서 캐시는 서버와 DB에 두는 편이 맞았다. 프리셋 지표는 공유 캐시를 읽고, 임의로 고른 기간만 그때그때 직접 조회한다. 둘의 성격이 다르기 때문이다.
프리셋은 팀이 반복해서 보는 운영 지표다. 임의 기간 조회는 특정 질문에 대한 일회성 확인에 가깝다. 같은 API처럼 보여도 운영 기준은 다르게 가져가는 편이 낫다.
권한 문제는 인증 문제가 아니었다
이번에 가장 헷갈렸던 부분은 Google 권한이었다.
서비스 계정 키가 있고 서버가 그 키로 token을 만들 수 있으면 "이제 됐다"고 생각하기 쉽다. 그런데 그건 누구인지 증명한 것뿐이다. GA4 property나 Search Console property를 읽을 권한이 있다는 뜻은 아니다.
이걸 한 덩어리로 보면 디버깅이 길어진다. 실제로는 세 개의 문을 따로 통과해야 했다.
첫 번째 문은 identity다. 서버가 credential을 읽고 Google token을 만들 수 있어야 한다.
두 번째 문은 제품 권한이다. 같은 identity가 GA4와 Search Console에서 읽기 권한을 가져야 한다. 여기서 빠지면 token은 정상이어도 데이터 접근은 막힌다.
세 번째 문은 런타임 전달이다. 배포된 컨테이너가 그 credential 파일을 실제로 읽을 수 있어야 한다. 서버에서 수동으로 한 번 맞춰도 배포가 compose 파일을 다시 동기화하면 사라질 수 있다. 그래서 secret mount도 배포 가능한 인프라 정의 안에 들어가야 했다.
이 순서를 분리하고 나서야 문제가 빨리 좁혀졌다. token이 되는지, 제품 권한이 있는지, 컨테이너 안에서 파일을 읽는지. 이 세 가지는 서로 다른 체크다.
구현하면서 정한 기준
이 작업에서 중요한 기준은 "최신 값을 얼마나 빨리 가져오느냐"가 아니었다. 화면이 어떤 실패 모드를 갖느냐가 더 중요했다.
Growth OS는 운영 cockpit이다. 사용자가 들어왔을 때 빈 화면이나 크래시를 먼저 보여주면 안 된다. 외부 API가 잠깐 실패해도, 적어도 마지막으로 확인된 숫자는 보여줘야 한다.
그래서 구현 기준을 이렇게 잡았다.
- 프리셋 지표 화면은 Google API를 기다리지 않는다.
- 갱신 실패는 기존 값을 덮어쓰지 않는다.
- 같은 구간 갱신은 동시에 여러 번 돌리지 않는다.
- 인증, 제품 권한, 컨테이너 설정은 각각 따로 검증한다.
이 기준을 잡고 나니 cron은 주 경로가 아니라 보조 도구가 됐다. 수동 복구나 백필에는 여전히 쓸 수 있지만, 매일 돌아야만 화면이 살아 있는 구조는 피했다.
배포 후 확인한 것
배포 후에는 코드 테스트만 보고 끝내지 않았다. 배포된 런타임에서 확인했다.
credential 파일을 읽을 수 있는지 봤고, 프리셋 지표 응답에 캐시 상태가 포함되는지 봤고, GA와 Search Console 값이 모두 들어오는지 봤다.
최종적으로 화면은 저장된 값을 즉시 반환했고, 캐시는 fresh 상태였고, 프리셋 구간 전체에서 GA와 Search Console 값이 채워졌다.
여기서 중요한 건 "숫자가 나왔다"보다 "다음에 실패해도 어떤 일이 일어나는가"였다. 이제 Google fetch가 잠깐 실패해도 화면은 직전 snapshot을 보여준다. 실패는 운영자가 볼 로그와 캐시 상태로 남고, 사용자는 빈 화면을 보지 않는다.
공개 글에 일부러 안 쓴 것
이 글에서는 실제 설정값을 쓰지 않았다.
- 서비스 계정 이메일
- GA property id
- DNS verification token
- credential 파일명과 서버 절대 경로
- repository 또는 서버 secret 값
- 내부 API 경로 이름
문제를 이해하는 데 필요한 건 값 자체가 아니라 경계다. 어떤 권한이 필요한지, 어떤 순서로 확인해야 하는지, 어떤 실패를 막아야 하는지가 중요하다. 구체 식별자는 공개 글에 남길수록 위험만 늘어난다.
다음에는 이렇게 판단한다
외부 분석 API를 제품 화면에 붙일 때는 "API 호출이 되나?"보다 먼저 "실패하면 화면이 어떤 상태를 유지하나?"를 본다.
cron은 단순하지만 조용히 죽을 수 있다. 매번 live 조회는 신선하지만 화면을 외부 API latency에 묶는다. 여러 사람이 같이 보는 운영 지표라면, DB-backed stale-while-revalidate가 더 좋은 기본값이었다.
그리고 Google 권한은 한 문장으로 묶지 않는다.
- token minting은 identity다.
- GA4/Search Console access는 제품 권한이다.
- container file readability는 런타임 전달이다.
이 셋을 분리해서 보면 문제를 훨씬 빨리 좁힐 수 있다.
Growth 지표 화면은 분석 API client가 아니라 운영 cockpit이어야 한다. cockpit은 외부 계기가 흔들려도, 적어도 마지막으로 믿을 수 있었던 상태를 계속 보여줘야 한다.
