App Store 매출 수집 파이프라인을 짜고 대시보드에 연결한 아침
목차
오전 6시에 슬랙 알림 끄고 제일 먼저 한 건 Apple App Store 매출 수집기였다. 그동안 매출은 App Store Connect 웹 UI 들어가서 눈으로 보고 있었는데, 그게 쌓이기 시작하니까 더 이상 수동으로 하기가 싫어졌다. 어드민 대시보드에 KPI 카드 하나 박아두면 아침마다 들어가도 되고, 나중에 트렌드도 볼 수 있겠다 싶어서 수집기부터 짰다.
수집기 구조: 파이썬 스크립트 + cron
_lib/appstore-sales-check.py 파일을 새로 만들었다. App Store Connect API를 쏴서 일일 매출 집계를 받아온 뒤 admin_db.cms_appstore_sales 테이블에 INSERT하는 구조다. 단순하게 짰는데, 테이블 스키마 잡는 게 생각보다 고민이 좀 됐다. 통화 단위를 어떻게 처리할지, 날짜를 로컬 기준으로 잡을지 UTC로 잡을지, 앱별로 로우를 따로 쌓을지 아니면 집계해서 하나로 쌓을지. 결국 앱별로 분리해서 쌓는 방향으로 갔다. 나중에 앱 늘어나도 GROUP BY로 합산하면 되니까.
cron 설정은 _lib/cron-appstore-sales-check 파일로 분리했다. 매일 새벽에 돌게 해뒀는데, App Store Connect의 데이터 갱신 시점이 UTС 기준 새벽이라 그것보다 충분히 늦게 잡았다. 처음엔 너무 일찍 돌다가 전날 데이터가 안 들어와 있어서 삽질한 경험이 있어서.
# 수집기 흐름 요약
1. App Store Connect API 호출 (sales/trends/reports)
2. 전날 날짜 기준 데이터 파싱
3. admin_db.cms_appstore_sales UPSERT
(중복 실행 시 덮어쓰되 idempotent 유지)
4. 이상값(0원 또는 null) 감지 시 로그만 남기고 스킵
idempotent하게 짠 이유는 cron이 간혹 실패 후 재실행될 때 중복 INSERT 들어오는 게 싫어서다. UPSERT로 처리하면 재실행해도 깔끔하다.
대시보드 KPI 카드: 데이터 연결과 TypeScript 빌드 에러
수집기가 붙으니까 이제 대시보드 쪽을 건드렸다. src/lib/dashboard-stats.ts에 getAppstoreStats() 함수를 추가하고, admin/(protected)/dashboard/page.tsx에 App Store 매출 KPI 카드를 하나 더 꽂았다. 구조 자체는 기존 카드들이랑 똑같은 패턴이라 빠르게 됐다.
근데 빌드 돌려보니까 터졌다. 에러 메시지는 이렇게 생겼다:
Type 'IterableIterator<[string, number]>' is not an array type
or a string type.
Map.entries()를 스프레드 연산자로 쓰고 있었는데, tsconfig의 lib 설정 때문에 타입 추론이 안 맞았다. 이건 Array.from()으로 감싸주면 해결된다:
// 이전 (빌드 에러)
const entries = [...statsMap.entries()];
// 수정 후
const entries = Array.from(statsMap.entries());
별거 아닌데 처음에 에러 메시지만 보고 lib 설정 건드려야 하나 잠깐 고민했다. downlevelIteration 켜는 방법도 있긴 한데, 그쪽 건드리면 다른 데 영향 갈 수 있으니 그냥 Array.from이 맞다.
publish-progress: 의도적 제외 주석 달기
scripts/publish-progress.py도 살짝 손봤다. gamehotdeals(93점)랑 cpsrush(88점)가 progress 집계에 잡히면서 "왜 이 두 사이트는 발행 진척도가 안 오르냐"는 의문이 생기는데, 둘 다 deals 사이트 또는 정적 구조라 일일발행 개념 자체가 N/A인 사이트들이다. 그래서 제외 처리 자체는 맞는데, 코드에 아무 설명이 없으면 나중에 내가 봐도 "이거 빠진 거 아닌가?" 싶어서 의도적 제외라는 주석을 명시적으로 달았다.
# gamehotdeals, cpsrush: deals/정적 구조 = 일일발행 N/A → 의도적 제외
EXCLUDE_SITES = {"gamehotdeals", "cpsrush"}
이런 주석 하나가 나중에 몇 분을 아낀다. 특히 스크립트를 몇 달 만에 다시 보는 경우에.
문서 SSOT 정비: onboarding과 GSC 상태 실측 정정
오전 후반엔 문서 정비를 했다. 두 군데.
첫 번째는 docs/NEW-SITE-ONBOARDING.md 섹션 H. 40서버 외부 apex 도메인을 신규 사이트에 등록할 때 어디 어디를 건드려야 하는지 전수 정리했다. 기존엔 구두로 알고 있거나 여기저기 흩어져 있어서 새 사이트 붙일 때마다 뭔가 빠뜨리는 경우가 있었다. 이번에 패리티 검증 방법도 같이 정리해뒀는데, 핵심은 grep으로 빠르게 등록 여부를 확인하는 법이다:
# 전수 등록점 패리티 확인
grep -rn "newsite.example" /opt/nginx/conf/ /opt/ops/docs/ /opt/traefik/ 2>/dev/null
파일 몇 군데에 해당 도메인이 없으면 등록 빠진 거니까 한 번에 파악할 수 있다. 이걸 문서에 박아두면 다음 사람도 시행착오 없이 따라할 수 있다.
두 번째는 docs/sites-detail.md의 gamehotdeals GSC 등록 상태 정정이다. 문서에는 "미등록"이라고 적혀 있었는데, 실제로 sc-domain: 방식으로 siteOwner 인증이 이미 되어 있었다. 적힌 것 맹신 말고 실측이 단일진실이라는 원칙대로 직접 확인하고 stale 상태를 정정했다.
| 필드 | 기존(stale) | 정정 후(실측) |
|---|---|---|
| GSC 등록 상태 | 미등록 | sc-domain siteOwner 인증 완료 |
| 확인 방법 | 문서만 참조 | Search Console API 직접 조회 |
이런 stale 데이터가 무서운 이유는, 나중에 누군가 이 문서 보고 "아 여기 GSC 등록 안 됐네, 해야겠다" 하면서 중복 작업 하거나 잘못된 방향으로 디버깅할 수 있어서다. 문서 정확도가 실제로 팀 비용이 된다.
오전 6시간의 흐름을 돌아보면
결국 이 오전 작업은 한 줄기로 연결된다. App Store 매출을 자동으로 수집하고 싶다는 필요에서 출발해서, 수집기를 짜고(appstore-sales-check.py + cron), 그 데이터를 시각화할 대시보드 카드를 붙이고(getAppstoreStats + KPI 카드), 그 과정에서 나온 빌드 에러를 잡고(Map.entries 스프레드 이슈), 연관 문서를 정리했다(CLAUDE.md, onboarding 섹션 H).
publish-progress 주석이나 GSC 실측 정정은 별개의 작업처럼 보이지만, 사실 "지금 이 순간에도 어딘가 stale하거나 암묵지로만 남아있는 게 있다"는 걸 의식하면서 손에 잡히는 것들 정리한 거다. 인프라가 복잡해질수록 코드보다 문서의 신뢰도가 실제 작업 속도에 영향을 준다.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.