공유행 수술부터 광고 제거 대청소까지, 길고 빡센 하루
목차
오늘 처음 터미널을 켰을 때만 해도 kowiki 봇 하나 마무리하면 되는 줄 알았다. 결과적으로는 여러 repo에 걸쳐 광고 SDK를 싹 뽑고, Vision Key 크레딧 고갈 사고를 수습하고, Journey 2026 개정 요건을 반영하고, Play ASO 스냅샷을 앱마다 아카이빙하는 하루가 됐다. 커밋 수를 세보지 않아도 많다는 건 안다.
루이 사고가 없었으면 그냥 지나쳤을 것들
kowiki 봇은 한국어 위키피디아 멤버 테이블을 파싱해서 생일이랑 국적을 DB에 백필하는 봇이다. 원래 구조가 단순했다 — 그룹 페이지에 있는 멤버 표를 읽고, 이름 매칭이 되면 써준다. 근데 오늘 실제로 돌려보다가 문제를 발견했다. 동명이인이 여러 그룹에 걸쳐 있는 경우, 봇이 어느 그룹 소속인지 구분을 못하고 잘못된 그룹의 데이터를 다른 멤버에게 써버리는 현상이었다.
커밋에 "루이 사고"라고 박은 게 그 케이스다. 실명은 기록에서 마스킹되는 특성이 있으니 그냥 루이라고 하겠다. 이름이 동일한 멤버가 서로 다른 그룹에 존재하고, 그 각각이 위키 테이블에 공유행으로 잡히는 상황. 봇은 이름만 보고 매칭을 시도하니까 어느 루이인지 알 방법이 없었다.
수정 방향을 놓고 잠깐 생각했다. LLM을 붙여서 컨텍스트로 추론하는 방식도 가능은 하다. 그룹명, 데뷔 연도 같은 주변 정보로 어떤 루이인지 유추하게 하는 것. 근데 kpopdex에서는 그게 더 위험하다. 맞을 때는 좋지만 틀리면 DB에 잘못된 정보가 들어가고, 그걸 나중에 발견하기가 어렵다. 루이 사고처럼 수동으로 수술해야 하는 상황이 반복된다.
결론은 결정론적으로 간다. 다중 그룹에 공유된 멤버 행은 아예 건드리지 않는다. 유일 매칭이 성립하고, 게이트 조건이 전부 통과하는 경우에만 쓴다. 비어있는 게 틀린 것보다 낫다는 원칙. 이걸 코드에 박고, commit 메시지에도 명확히 남겼다. 나중에 누군가 "왜 이 멤버 생일이 없냐"고 물으면 설명할 수 있어야 하니까.
codex 리뷰 반영도 꽤 됐다. 국적 스코프를 좁혔는데, 위키 테이블에서 국기 템플릿이나 국적 라벨이 명시된 셀만 보도록 게이트를 걸었다. 이름 매칭 창도 NAME_WINDOW 250으로 한정했다 — 테이블 파싱 특성상 이름 패턴이 너무 멀리 있으면 잘못 매칭될 가능성이 올라간다. 1글자 이름은 아예 제외했다. 중첩 표도 파싱 대상에서 뺐다 — 중첩 구조가 있는 위키 테이블은 파서가 제대로 못 읽는 케이스가 있어서다.
--audit 모드 추가는 나중을 위한 투자다. 실제 쓰기 없이 봇이 무엇을 쓸 예정인지 미리 볼 수 있다. 다음번 비슷한 문제 생기면 audit 먼저 돌리고 확인한 다음에 실행할 수 있다. 오늘 루이 사고 같은 걸 사전에 잡을 수 있는 도구다.
이미 오염된 데이터는 별도로 수술했다. 30행 생성, 8개 bio 제거, 잔여 20개는 의도적 동일인으로 판단해서 남겼다. 이 판단 근거를 CLAUDE.md에 기록해뒀다. 잔여 20개가 왜 남아있는지 아무 설명이 없으면 나중에 보는 사람이 오염으로 오인하고 다시 지울 수 있다.
kowiki 봇이 New & Updated 스트립을 완성시켰다
봇 작업이 끝나면서 자연스럽게 연결된 게 있었다. 봇이 멤버를 채우고 나면 해당 그룹의 updated_at을 범프하도록 했다 — 홈 스트립과 연계하기 위해서다.
New & Updated 그룹 스트립은 오늘 HomePage.astro에 붙였다. 최근 30일 안에 추가되거나 갱신된 그룹을 모아서 보여주고, NEW 배지랑 Updated 배지를 붙인다. 30일 이내 신규 그룹이면 NEW, 30일 이내에 데이터가 갱신된 그룹이면 Updated. i18n도 8개 국어로 다 달았고, db.ts에 쿼리도 새로 추가했다.
봇이 updated_at을 범프해주지 않으면 이 스트립이 의미가 없다. kowiki 봇이 생일이랑 국적을 채워줘도 그게 홈에 반영이 안 된다. 그래서 봇 작업이랑 스트립 작업이 같은 날 같이 들어간 거다.
nav 가로스크롤 버그도 이 작업 와중에 발견했다. Base.astro의 헤더 nav 브레이크포인트가 720px으로 잡혀 있었는데, 721px에서 960px 사이 구간에서 페이지 가로스크롤이 생기는 결함이 있었다. 태블릿이나 소형 노트북 해상도에서 사이트가 찌그러져 보이는 상황. 985px으로 올려서 처리했다. 작은 버그지만 방치하면 안 되는 종류다.
광고 SDK 대청소, 반복의 지옥
오늘 커밋 목록에서 가장 많이 반복되는 게 두 가지다. chore: remove ads SDK entirely랑 docs: archive applied Play listing ASO copy. 둘 다 여러 repo에 걸쳐 같은 작업을 반복하는 종류다.
광고 SDK 제거는 dormant-ads 정책 때문이고, Play 스토어 프로덕션 심사 전에 정리해야 한다. 기술적으로 어렵지 않다. 그냥 지루하다. 앱마다 들어가서 같은 파일 목록을 편집해야 한다.
AndroidManifest.xml에서 광고 관련 권한 제거, ios/Podfile에서 광고 관련 팟 제거, ios/Runner/Info.plist에서 ATT 관련 키 제거, 각 언어별 InfoPlist.strings에서 ATT 목적 문구 제거, pubspec.yaml에서 의존성 제거, build.gradle.kts에서 관련 설정 제거. 이걸 앱마다 반복했다.
그 중에서 광고 레이어 자체를 아예 뽑아낸 커밋이 하나 있었다. hedvion_ads랑 app_tracking_transparency 의존성을 제거하고, ad_config.dart, ad_service.dart, ad_state.dart, app_open_ad_manager.dart, banner_ad_slot.dart — 이 파일들이 전부 사라졌다. 실제로 광고를 노출하지 않은 지 꽤 됐을 텐데, 코드는 계속 남아있던 거다. 죽은 코드가 여러 앱에 걸쳐 살아있었다는 게 조금 찜찜하다.
ATT 권한이 Info.plist에 남아있으면 Play 리뷰어가 광고 관련 용도를 물어볼 수 있다. 실제로 광고가 없는데 추적 권한이 있으면 정책 위반 소지가 생긴다. 그래서 이 작업을 지금 하는 거다. 앱마다 Info.plist를 열어서 ATT 관련 키가 하나라도 남아있지 않은지 확인했다. 제일 실수하기 쉬운 부분이라서 한 번씩 더 봤다.
Opus 리뷰 반영이랑 versionCode 범프도 같이 들어갔다. 앱마다 번 올리고, l10n arb 파일들 수정하고, 그게 app_localizations.dart랑 generated dart 파일들 재생성으로 이어지고. 독일어, 영어, 스페인어, 프랑스어, 이탈리아어, 일본어, 한국어, 인도네시아어 — arb 파일이 언어 수만큼 있으니 건드리는 파일 수도 그만큼 늘어난다.
SwiftPM workspace state 파일은 .gitignore에 추가해서 추적에서 뺐다. Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved랑 Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved — 이 파일들이 자동 생성인데 계속 커밋에 잡히면 diff가 지저분해진다. 이제 자동으로 무시된다.
ASO 스냅샷 아카이빙은 play_listing_20260830.json 이름으로 여러 repo에 생겼다. Play 콘솔에 PATCH 적용한 시점의 스토어 문구를 그대로 json으로 박아두는 거다. slangdecoder랑 slangkr 같이 별도 앱 이름이 붙은 것도 있고, 그냥 날짜만 붙은 것도 있고. 귀찮은 작업이지만 이게 없으면 나중에 "예전 스토어 설명이 어떤 내용이었지"를 한국어로도 영어로도 알 방법이 없어진다.
Vision Key가 조용히 죽어있었다
솔직히 이게 오늘 가장 찜찜한 건이다.
pricelens에서 이미지 인식에 Vision API를 쓰는데, 크레딧이 고갈되면서 스캔이 조용히 멈춰있었다. "조용히"가 문제다. 에러 로그가 쌓이거나, 모니터링 알림이 오거나 한 게 아니었다. 그냥 요청이 들어와도 처리가 안 되는 상태였다. 언제부터였는지 정확히는 모른다.
이런 external 의존성 크레딧 고갈은 항상 이런 패턴이다. 평소에는 잘 되다가 어느 순간 조용히 멈추고, 그걸 사용자 관점에서 발견하거나 직접 들여다보기 전까지는 모른다. 내부에서 exception이 잡혀서 조용히 무시되거나, 결과가 없는 게 정상인 것처럼 보이거나.
대응은 두 방향으로 했다. 즉각 대응은 키 교체 — 이커머스 PG 플랫폼 키로 Vision 키를 바꿨다. 재발 방지는 scripts/health-monitor.py에 크레딧 프로브를 추가했다. pricelens Vision Key 크레딧 잔량을 정기적으로 확인하는 프로브다. 잔량이 일정 수준 이하로 내려가면 사전에 잡힌다. 스캔이 실제로 멈추기 전에.
크레딧 프로브를 CLAUDE.md에도 기록해뒀다. health-monitor가 어디까지 커버하는지 명시해두는 거다. 이게 없으면 나중에 프로브가 있다는 걸 모르고 다시 설정하거나, 중복이 생기거나 한다. 더 중요한 건, 다음번에 새로운 외부 API를 연동할 때 크레딧 프로브를 같이 박는 버릇을 들여야 한다는 걸 남겨두는 거다.
Journey 2026 개정 요건, 그리고 집계 버그
Journey 2026 개정 요건 반영은 ga4-daily.py와 CLAUDE.md를 같이 건드렸다. 프리미엄 세션 기준이 1,000으로 바뀌었고 - US, UK, CA, AU 권역 기준이다 - Grow의 30일 대기 클럭 게이트가 폐지됐다. 클럭 게이트가 없어지니까 ga4-daily 스크립트에서 30일 경과 여부를 체크하던 로직이 달라져야 했다.
작업하다가 집계 버그를 발견했다. 30일 세션을 국가별 행으로 조회하면, 같은 세션이 여러 국가 행에 중복 집계되어서 합산하면 실제보다 크게 나온다. 무차원 totals 조회로 바꿔서 픽스했다. 30일이 경과한 경우 ✅ 표시가 붙도록 해서 콘솔에서 봤을 때 현황 파악이 쉬워졌다.
과대집계된 수치로 Journey 기준을 판단하고 있었다면, 세션 수가 실제보다 많게 보였을 거다. 어떤 영향을 미쳤는지는 직접 계산해봐야 알겠지만, 이 버그가 오래됐을 가능성도 있다.
정책 문서, 자동 생성 테스트, cortis 링크
의료용 앱 기획 금지 게이트랑 Play 건강선언 허용 범위를 docs/mobile-app-launch.md에 정리했다. 2026-08-29 전수 실측 기반이라고 명시되어 있다. 이런 정책 문서는 실측 날짜를 박아두는 게 중요하다. 시간이 지나면 Play 정책이 바뀌어 있을 수 있고, 그때 이 문서가 stale인지 아닌지 알려면 마지막 실측이 언제였는지 알아야 한다.
cortis 공식 링크 시드를 kpopdex/bot/seed_links.py에 추가했다. 자료 요청 대응이고, 검증 출처 주석을 달았다. 링크 시드 작업은 출처가 명확해야 한다. 나중에 링크가 죽거나 내용이 바뀌었을 때 어디서 가져온 링크인지 알아야 추적이 된다.
Journey 2026 개정 요건은 docs/hedvion-CLAUDE.md도 같이 업데이트했다. 전체 봇에 로드되는 문서라서, 여기에 최신 요건이 반영되어 있어야 한다.
psy쪽 일간 자동 생성 테스트는 오늘도 돌았다. mart-shopping-type, midnight-mall-mannequin, my-room-invasion — 커버 이미지 webp 파일이랑 json이 함께 커밋됐다. 수작업 없이 자동으로 생성되는 것들이라 특별히 할 말은 없다. 잘 돌고 있다는 게 확인됐다.
오늘 하루 정리
커밋 수가 이렇게 많은 날은 보통 둘 중 하나다. 진짜 많은 일이 있었거나, 반복 작업이 잔뜩 있었거나. 오늘은 둘 다였다.
kpopdex kowiki 봇은 기술적으로 흥미로운 부분이 있었다. 결정론적 매칭이라는 제약 안에서 얼마나 정확하게 백필할 수 있는가. LLM을 쓰면 더 많은 케이스를 커버할 수 있지만, 거짓 매칭이 들어가면 그게 루이 사고처럼 수동 수술로 이어진다. 덜 채워지더라도 결정론적으로 가는 게 맞다는 판단을 오늘 다시 확인했다.
광고 SDK 제거는 반대로 기술적으로 어렵지 않고 그냥 지루했다. 같은 파일 목록을 앱마다 편집하고, 빠뜨린 게 없나 확인하는 작업. 이런 작업의 위험은 지루함 때문에 집중력이 떨어지는 것이다. Info.plist에서 ATT 키 하나가 남아있어도 리뷰에서 걸릴 수 있으니까, 각 앱마다 한 번씩 더 확인했다.
Vision Key 크레딧 고갈 사고가 사실 제일 불편한 건이다. 내가 들여다보기 전까지 얼마나 오래 죽어있었는지 모른다는 것. 이제는 health-monitor가 사전에 잡아줄 테니, 같은 방식으로 죽을 일은 없다. 하지만 비슷한 구조의 다른 의존성에서 같은 일이 안 일어난다는 보장은 없다.
댓글 0
첫 댓글 달아줘.