오늘은 새 기능 작업은 없었고, 출시 이후 실사용자 쪽에서 터진 두 가지 인프라 문제(푸시 안 옴, 광고 노출 0%)를 잡고, "업데이트 안내가 실제로 뜨는지" 확인해달라는 요청을 코드 레벨에서 점검했다. 둘 다 코드는 멀쩡한데 스토어/콘솔 설정 쪽이 원인이라 삽질 방향을 잘못 잡았다가 되돌아온 과정이 길었다.
시뮬레이터/개발 기기로는 잘 오던 푸시가, 앱스토어에 실제로 설치한 기기에서는 전부 실패한다는 리포트가 들어왔다.
send-push Edge Function 로그를 보려고 에러 상세를 그대로 노출하는 디버그 버전을 임시로 배포했다.
401 THIRD_PARTY_AUTH_ERROR / BadEnvironmentKeyInToken
BadEnvironmentKeyInToken이라는 메시지 때문에 처음엔 "iOS 앱 자체가 개발용 aps-environment로 서명된 게 아닌가"로 방향을 잡았다. 앱 번들의 엔타이틀먼트를 열어 확인했더니 실제로 flutter build ipa가 만든 로컬 IPA는 개발용 entitlement로 서명돼 있었다 — 그런데 App Store에 실제로 배포된(사용자 기기에 깔린) 빌드는 Apple이 업로드 시점에 프로덕션 entitlement로 자동 재서명한다는 걸 확인하면서 이 가설은 기각했다. 로컬 빌드 산출물만 보고 있었던 거다.
Apple Developer 콘솔에서 Firebase 프로덕션 슬롯에 올라가 있던 키(9V367KTB4Q, 2026.09.03 발급)의 상세를 다시 열어보니 Enabled Services가 [Sandbox]로만 찍혀 있었다. 이름이나 발급 경로 어디에도 "프로덕션용"이라는 표시가 없었는데 실제로는 처음부터 Sandbox 전용으로 발급된 키였던 것 — Environment는 발급 후에는 수정도 안 된다.
| 키 | Environment | 결과 |
|---|---|---|
9V367KTB4Q (기존, 09.03 발급) | Sandbox 전용 | 실사용자 기기(production) 401 |
ZN9Z73XWKH (신규, 09.13 발급) | Sandbox & Production | 정상 |
새 키를 "Sandbox & Production"으로 발급받아 Firebase 콘솔 프로덕션 APNs 슬롯을 교체하고, 디버그 버전을 되돌린 뒤 다시 배포해서 확인했다.

앞으로 APNs 키를 새로 만들 땐 Environment를 반드시 "Sandbox & Production"으로 선택할 것 — 기본값이나 무심코 고르면 Sandbox 전용으로 굳어버려서 재발급 말고는 답이 없다.
Android(비공개 테스트)는 광고가 정상 노출되는데, iOS(정식 출시)는 요청 12건에 노출이 0건이었다. 리워드 광고 코드 자체(로드 실패 시 토스트, 표시 중 스피너, nonPersonalizedAds: true)를 다시 봤지만 로직엔 문제가 없었다.
Info.plist에 AdMob 앱 ID는 등록돼 있었는데 SKAdNetworkItems가 아예 없었다. iOS 14부터는 이게 없으면 대부분의 광고 네트워크가 응찰 자체를 안 해서 매치율이 0%로 떨어진다 — 요청은 AdMob 서버까지 도달하니 코드/네트워크 문제로는 안 보이고, 정작 원인은 입찰 참여 자격 자체가 없었던 것.
<key>SKAdNetworkItems</key>
<array>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cstr6suwn9.skadnetwork</string>
</dict>
<!-- ... 구글 AdMob 퀵스타트 가이드 권장 목록, 총 50개 ... -->
</array>
구글 AdMob 퀵스타트 가이드(developers.google.com/admob/ios/quick-start)에 있는 식별자 50개를 그대로 추가했다. ATT(App Tracking Transparency) 권한은 따로 안 붙였는데, 이미 AdRequest(nonPersonalizedAds: true)로 비개인화 광고만 요청하는 구조라(admob_ad_service.dart) 트래킹 동의 자체가 필요 없는 케이스라서다.

Info.plist를 고쳤으니 새 빌드를 올려야 하는데, 직전 승인된 마케팅 버전(1.0.0+15)으로 빌드 번호만 올려 제출하려 하니 App Store Connect가 "Invalid Pre-Release Train ... is closed for new build submissions"로 거부했다. 이미 승인된 마케팅 버전엔 새 빌드를 못 얹는다 — patch 자리를 올려 1.0.1+16으로 다시 시도했는데 이것도 막혀서, 결국 1.0.2+17까지 올리고서야 제출 가능한 상태가 됐다.
IPA는 로컬에서 빌드 완료했고(objective_c.framework가 아카이브에 안 딸려 들어간 것도 확인), Xcode/Transporter로 직접 업로드하는 것만 남았다 — 로컬 업로드 도구는 파일 크기 제한 때문에 못 쓴다.
앱 시작 시 최신 버전이 아니면 알려주는 기능(upgrader 패키지, UpgradeAlert)이 잘 작동하는지 확인해달라는 요청이 있어서 코드를 다시 훑었다. 결론부터 말하면 코드는 문제없는데 실기기에서 다이얼로그가 뜨는 걸 눈으로 본 적은 아직 한 번도 없다.
lib/main.dart:372 부근에서 MaterialApp.router의 builder가 UpgradeAlert(child: child!)로 앱 전체를 감싸는 구조라 항상 마운트된다.package:version의 semver > 연산자를 쓰기 때문에 "1.0.10"이 "1.0.9"보다 낮게 취급되는 문자열 비교 버그는 없다.project.pbxproj의 버전이 하드코딩됐던 적이 있는데(그러면 iOS에서 항상 구버전으로 읽힘) 9/11 커밋에서 이미 고쳐진 것도 재확인했다.upgrader 패키지 자체의 알려진 함정도 GitHub 이슈로 확인해뒀다: 사용자가 다이얼로그 버튼을 안 누르고 앱을 그냥 꺼버리면 그 버전은 "이미 보여준 걸로" 기록돼서 다시 안 뜬다(#379, #444). 스토어 응답이 비어있으면 FormatException으로 조용히 실패하는 것도 알려진 이슈(#94).코드 변경은 없었고, 다음에 "업데이트 안내 왜 안 떠요" 문의가 오면 바로 참고할 수 있게 위 내용을 메모리에 남겨뒀다.
Info.plist에 SKAdNetworkItems(구글 권장 50개) 추가 — iOS 광고 매치율 0% 문제 해결1.0.0+15 → 1.0.2+17로 상향(App Store Connect의 승인된 버전 재제출 거부 때문에 두 번 올림), IPA 로컬 빌드 완료upgrader/UpgradeAlert 구현 상태 코드 레벨 점검 — 문제 없음을 확인, 실기기 검증 이력·Android 등록 여부 등 미확인 사항을 메모리에 정리출시 전 필수
그 외