일기 slecs

pub-3485 해지 통보 하루, 광고 SDK를 전 앱에서 뽑아내다

목차

어제 저녁 슬랙에 떠 있던 알림이 머릿속에 남아 있었다. AdMob pub-3485 계정 해지. 정확히 언제 발효되는지 모른 채 자고 일어났더니 오늘 아침부터 일이 터졌다. 이미 일부 앱에서 광고가 안 뜬다는 신호가 들어오고 있었다. 선택지는 없었다. 오늘 하루 전부를 CAS.ai 마이그레이션에 쏟아야 했다.

마이그레이션 범위를 파악하는 데만 한참 걸렸다

앱이 몇 개인지 머릿속으로 세봤다. hedvion_ads, meter_kit, 그리고 소비 앱들 — Daily TOPIK, moneydiary, moneyleaf, 퀴즈 앱, 식물 앱, 스코프, 수독, 코드게임, 코골이 앱, Pilmori, 거기에 파트너 포털까지. google_mobile_ads를 직접 의존하는 앱, meter_kit을 통해 간접 의존하는 앱, hedvion_ads를 쓰는 앱, 아직 직접 AdMob을 쓰는 앱. 전부 다 달랐다. 한 줄로 교체할 수 있는 게 아니었다.

일단 공통 레이어부터 손을 댔다. hedvion_ads를 0.3.0으로 올리면서 casId 결정권을 앱 코드에서 완전히 빼냈다. 기존에는 각 앱이 casId를 하드코딩하거나 환경변수로 들고 다녔는데, 이걸 admin_db.app_remote_config를 단일진실로 삼아 서버에서 내려받는 구조로 바꿨다. hedvion.com/appcfg 엔드포인트가 이미 있었고, app-ads 라우트에서 casId 필드를 공개 API 응답에 포함시켰다. 앱은 그걸 읽어서 CAS를 초기화하면 된다. 앱에 ID를 박지 않아도 되고, 나중에 casId를 바꿔야 할 때 앱을 배포하지 않아도 된다.

근데 app-ads 라우트에서 바로 500이 났다. MySQL JSON 컬럼은 드라이버가 이미 파싱해서 넘겨주는데, 코드에서 JSON.parse()를 한 번 더 돌린 것이었다. 문자열이 아닌 객체에 JSON.parse를 치면 [object Object]가 들어가거나 그냥 터진다. 확인하고 수정하는 데 5분도 안 걸렸는데, 배포 전에 못 잡은 게 찜찜하다.

앱마다 구조가 달라서 복붙이 안 됐다

공통 레이어를 정비한 뒤 각 앱으로 내려갔다. 순서를 정해놓고 한 앱씩 처리했다. build.gradle.kts에서 AdMob 의존성 제거, Podfile에서 google_mobile_ads 빼고 CAS 추가, Info.plist 업데이트, main.dart에서 초기화 순서 재조정, consent_service.dart 교체. 이 흐름은 동일했는데, 세부는 앱마다 달랐다.

특히 리워드 광고 쪽에서 앱마다 버그를 하나씩 찾았다. 어떤 앱은 콜백 타이밍이 틀렸다. onRewarded가 오기 전에 premium 상태를 갱신하는 바람에 사용자가 광고를 다 봤는데 혜택을 못 받는 경우가 생기는 구조였다. 또 다른 앱은 rewardedAd가 no-fill 됐을 때 null 체크 없이 show를 때리다가 조기완료 처리됐다. 이런 게 AdMob 시절에는 어떻게 돌아가고 있었는지 모르겠다. CAS로 바꾸면서 코드를 다시 보니 보이는 거지, 원래 있던 버그들이다.

app_open_ad_manager에서는 12시간 게이트를 하나 추가했다. 앱 열 때마다 오픈 광고 때리면 UX 기준으로 최악이라 쿨타임을 두는 게 맞는데, 일부 앱에서 이게 빠져 있었다. 프리미엄 사용자한테 광고 SDK 자체를 안 띄우는 처리도 몇 앱에서 빠져 있었다. main.dart 진입점에서 premium 여부 확인 후 ATT 요청과 CAS init을 완전히 스킵하는 분기가 필요한데, 이걸 빼먹은 앱들이 있었다. 오늘 다 잡았다.

GADApplicationIdentifier 지뢰 - 세 번 밟았다

이게 오늘 가장 많이 날 괴롭혔다.

CAS Optimal을 선택하면 CAS 내부에서 Google Mobile Ads를 의존한다. 앱에서 google_mobile_ads를 직접 import하지 않아도 CAS가 GMA를 끌어들인다. 그러면 앱이 런치할 때 GMA가 GADApplicationIdentifier 키를 Info.plist에서 읽으려 한다. 없으면 런치 크래시.

마이그레이션하면서 "이제 AdMob 안 쓰니까 GAD 키도 필요 없겠지"라는 생각에 지운 앱들이 있었다. 그 앱들이 빌드하면 런치 순간 죽었다. 한 앱에서 발견하고 고쳤는데 다른 앱에서 또 같은 패턴이 나왔다. 세 번째쯤 됐을 때는 짜증을 넘어 반자동적으로 손이 갔다.

Pilmori 쪽에서는 project.yml 레벨에서 info 딕셔너리에 GADApplicationIdentifier를 명시하는 방식으로 고쳤다. 왜냐면 xcodegen이 project.yml을 기반으로 Info.plist를 재생성할 때, plist를 직접 수정해두면 그게 증발하기 때문이다. xcodegen 쓰면서 plist 직접 수정하는 건 함정이다. 1.5 심사 반려도 여기서 나왔고, 빌드11 재제출도 이 건이었다.

오늘 이 패턴이 반복되는 걸 보고 tool/check_native_config.sh를 만들었다. CAS나 GMA가 링크되어 있을 때 GAD 키 존재 여부를 검사하는 게이트. 릴리즈 전에 돌리는 체크 스크립트다. 같은 실수를 네 번 이상 반복한 뒤에야 자동화할 생각을 했다는 게 반성이다. 진작 했어야 했는데.

iOS 타겟 13.0이 15.0이 된 경위

file_picker가 문제였다. stable 10.x 버전은 win32 6.x와 조합하면 컴파일이 안 된다. 그래서 12.0.0-beta.7로 올렸는데, 이 버전이 iOS 14.0 이상을 요구한다는 게 뒤늦게 드러났다. CAS Optimal도 13.0 이상 지원인데 실제로는 15.0으로 올려야 하는 경우가 있었다.

그러다가 "그냥 15.0으로 통일하자"고 결론 내리고 여러 앱의 Podfileproject.pbxproj를 다 올렸다. Podfile에서 platform :ios, '15.0' 변경하고, pbxproj에서 IPHONEOS_DEPLOYMENT_TARGET도 전부 맞췄다. 이걸 각 앱마다 했으니 같은 작업을 여러 번 했다.

file_picker는 결국 다시 다운그레이드했다. 12.0.0-beta.7은 beta라 stable 릴리즈 기준으로 pubspec.lock이 깨지는 경우가 있어서, 10.x를 win32 버전 오버라이드랑 같이 써서 컴파일을 통과시키는 쪽으로 갔다. 한 앱에서 다운그레이드, 다른 앱에서 beta 고정, 또 다른 앱에서 다시 stable — 앱마다 결론이 약간씩 다르게 됐다. 나중에 file_picker stable이 이 문제를 해결하면 전 앱 일괄 올리는 작업이 하나 더 생기는 거다.

인증서 revoke 사건

오늘 docs/mobile-app-launch.md에 에이전트가 Apple 인증서를 revoke하거나 새로 생성하는 행위를 금지한다는 룰을 박아넣었다. 뭔가 있었다는 뜻이다. 오늘 2026-07-29에 실제로 revoke 사건이 있었다. 에이전트가 자동으로 인증서 관련 작업을 하다가 기존 인증서를 revoke해버렸다. 인증서 revoke는 그걸 사용 중인 모든 프로파일을 무효화한다. 배포 중인 앱에 영향이 간다. 이건 실수로 하면 안 되는 종류의 작업이다.

오늘 이 사건을 수습하면서 동시에 룰 문서를 수정했다는 사실 자체가 좀 무거웠다. 자동화가 편리한 만큼 이런 부류의 실수를 예방하는 가드가 충분히 있어야 한다는 걸 또 한 번 체감했다.

HedvionAdsKit를 hedvion_ads로 흡수

별도 저장소로 운영하던 HedvionAdsKit(native iOS SPM 패키지)을 hedvion_ads/native/ios로 당겼다. 이유는 단순하다. 두 저장소를 따로 유지하면서 동기화가 계속 틀어지는 게 귀찮았다. 앱에서 SPM 참조를 할 때 URL이 바뀌는 불편함도 있었고.

흡수하면서 AdsRemoteConfig 스키마를 크로스플랫폼 계약으로 분리했다. Dart 레이어와 Swift 레이어가 같은 구조의 응답을 다루는데, 이걸 명시적인 계약 문서(CONFIG_CONTRACT.md)로 기록해뒀다. 한쪽이 바뀌면 다른 쪽도 바꿔야 한다는 걸 문서 없이 코드만 보다가 놓치는 경우를 줄이기 위해서다.

Pilmori 앱은 이 통합을 반영해서 project.yml의 SPM 참조 URL을 업데이트했다. 흡수 완료 기록도 CLAUDE.md에 남겼다. 1.6.0+12 버전 범프도 같이 했다.

SKAdNetworkItems — 258개짜리 목록 소유권 문제

AdMob 시절 SKAdNetworkItems는 Google이 관리하는 목록을 갖다 쓰면 됐다. CAS로 바꾸면서 CAS recommended list로 교체했는데, 이 목록이 258개다. 문제는 이 목록을 각 앱의 Info.plist에 직접 박아넣으면 나중에 CAS가 목록을 업데이트했을 때 모든 앱을 한 번씩 다 손봐야 한다는 것이다.

native/config/SKAdNetworkItems.plist.snippet이라는 정본 파일을 만들고 check_native_config.sh가 이 스니펫의 존재 여부를 게이트로 검사하게 했다. 앱 쪽에서 복붙하는 구조는 여전히 있지만, 적어도 정본이 어디 있는지 명확해졌다. 복붙 사고 재발방지용이라고 커밋 메시지에 박았는데, 오늘 같은 날에 이런 시스템적 처리를 여러 개 만들었다는 게 내일을 위한 유일한 위안이다.

appcfg-sync - DB가 단일진실이라는 걸 서버가 아는 방법

admin_db.app_remote_config에 있는 설정값들을 hedvion.com/appcfg/{appId}.json으로 동기화하는 스크립트를 오늘 완성했다. 앱들이 서버에서 원격 설정을 내려받을 때 이 엔드포인트를 쓴다. legal-static 웨이브1 앱들의 appconfig.json도 같이 동기화하게 확장했다.

DB 비밀번호 폴백 로직이 기존 스크립트 패턴과 안 맞는 버그가 있어서 한 번 더 수정했다. 이런 건 로컬에서 돌려보면 바로 보이는데 배포 후에 발견하면 짜증스럽다.

파트너 포털 - 대리점 합산이 안 되던 버그

오전 초반에 잠깐 건드린 것들인데, 규모가 작지 않았다. 예치금 잔액과 이력이 파트너 본인 것만 나오고 하위 대리점 것은 합산이 안 됐다. SQL 쿼리에서 하위 목록을 조인하는 조건이 잘못되어 있었고, 그 결과로 하위 대리점 예치금이 0원으로 표기되는 버그도 같이 있었다.

쿼리 매퍼를 두 군데 수정하고 Java Controller도 함께 고쳤다. 화면에서 파트너 본인 행에 '본인' 배지를 달았는데 간격이 틀어져서 CSS도 한 줄 건드렸다.

스코프 앱에서는 차등수익 집계 축이 틀렸다는 리뷰 지적이 있었다. 64원이 나와야 할 게 44원이 나왔다. 쿼리에서 정산 카운트 조건이 화면 표기 기준과 일치하지 않는 문제였다. 수정 후 잔액 내역 화면의 Y축도 정산 카운트 기준으로 통일했다.

장바구니 선물하기, 카메라 축 보정, 기타

장바구니에서 선물하기를 다중 선택하면 아무 안내도 없이 막히는 UX 문제가 있었다. 토스트 메시지 하나 추가했다. 작은 거지만 사용자 입장에서는 이런 게 체감된다.

미터 앱(경사계 관련)에서는 floor slope compensation의 축이 틀렸다는 버그가 있었다. ay가 아니라 ax를 쓰고 있었다. B1/B2 축 컨벤션이 처음 구현에서 잘못 들어간 거였고, 첫 번째 픽스 이후에도 여전히 틀린 게 남아 있어서 두 번 수정했다. 테스트 케이스를 추가하고 수학 모델에 대한 설명을 CLAUDE.md에 남겼다.

daily TOPIK 앱에서 compare_screen이 stale 세션 ID를 force-unwrap 하다가 크래시 날 수 있는 경로가 있었다. 가드 조건 추가.

panic flow 앱에서는 커스텀 카드 다이얼로그가 pop 애니메이션 중에 controller가 dispose되면서 크래시가 났다. premium 실탭 QA 중에 발견했다. controller 생명주기 관리 문제였고, 이번 1.1.0 릴리즈 전에 잡혔으니 다행이다.

마무리

오늘 커밋 수를 세보니 80개를 넘어간다. 광고 SDK 마이그레이션이라는 하나의 사건이 이렇게 많은 앱과 레이어를 건드렸다. 공통 레이어를 만드는 게 맞다는 걸 다시 확인했다. hedvion_ads 패키지 하나로 casId 결정권을 서버로 이관했으니, 다음번에 광고 플랫폼을 또 바꿔야 할 일이 생긴다면 오늘보다는 낫겠지.

GADApplicationIdentifier 지뢰는 앞으로 check_native_config.sh가 막아줄 거다. 인증서 revoke 룰은 문서에 박혔다. 앱마다 들쭉날쭉하던 리워드 광고 콜백 타이밍도 정리됐다. 하루 동안 같은 부류의 실수를 여러 번 하면서 그때마다 자동화나 문서를 하나씩 만들었다.

psy 사이트 자동생성 테스트 3개는 별도 자동화가 돌아가면서 커밋에 섞여 들어온 거고, 이건 내가 뭔가 한 게 아니다. 오늘 내가 한 건 나머지 전부였고, 몸은 무겁다.

댓글 0

첫 댓글 달아줘.