자정 스프린트: Play+ASC 양쪽 스토어 제출 자동화와 80장 스크린샷
목차
새벽 0시가 막 넘은 시점, 화면에 떠 있던 터미널이 세 개였다. 하나는 Play Developer API 응답을 스트리밍하고 있었고, 하나는 flutter test --tags screenshot 로그가 쌓이고 있었고, 나머지 하나에선 App Store Connect 대시보드를 새로고침했다. 그날 새벽의 목표는 vidpress 앱을 Play와 ASC 양쪽에 동시에 심사 제출하는 것이었다.
"앱을 만드는 일"과 "앱을 스토어에 올리는 일"이 전혀 다른 공정이라는 걸 처음 경험하는 사람은 없겠지만, 둘을 동시에, 8개 로케일, 자동화 스크립트로 밀어붙이면 예상 못한 차단 요소가 단계마다 하나씩 튀어나온다. 그것들을 하나씩 뚫는 게 이날 새벽 여섯 시간의 실제 내용이었다.
Play Store: API 자동화로 AAB 업로드부터 IAP 활성화까지
Play 쪽은 처음부터 API 자동화로 가기로 했다. 콘솔 UI를 손으로 클릭해서 8개 언어 리스팅을 채우면 재현이 불가능한 작업이 되고, 다음 앱 출시 때 처음부터 다시 해야 한다. 그래서 store/ 디렉토리에 네 개의 Python 스크립트로 나눴다.
play_token.py- 서비스 계정 JSON으로 OAuth 토큰 발급play_upload.py- AAB 파일을 Internal 트랙에 업로드하고 edit commitplay_listing.py-store/listing.md에서 8로케일 타이틀/설명 파싱 후 일괄 PUTplay_iap.py- 인앱상품(IAP) $4.99를 ACTIVE 상태로 생성
이 중에서 play_listing.py가 가장 손이 많이 갔다. Play Developer API v3의 listings 엔드포인트는 locale 코드가 BCP-47 형식(ko-KR, en-US)이어야 하는데, 잘못된 코드를 넣으면 404가 아니라 200 OK를 내려주면서 리스팅이 생성되지 않는다. 응답이 성공인데 콘솔에 반영이 안 되니까 처음엔 캐시 문제인 줄 알았다. 한참 후에야 locale 값이 ko로 넘어가고 있던 걸 로그에서 발견했다. 파싱 정규식에서 ko-KR 앞 두 글자만 잘리고 있었다. 단순한 버그지만 에러 응답 자체가 없으니 찾는 데 시간이 걸렸다.
play_iap.py는 상대적으로 단순했다. inappproducts.insert를 호출하고 defaultPrice만 세팅하면 나머지 국가별 환산은 Play가 처리한다. 다만 status: "ACTIVE"를 명시하지 않으면 INACTIVE로 생성된다. 처음에 그걸 빠뜨렸고, 콘솔에서 상품이 보이는데 구매가 안 된다는 걸 확인하고 나서 스크립트를 고쳤다.
body = {
"packageName": PACKAGE_NAME,
"sku": "premium_lifetime",
"status": "ACTIVE", # 없으면 INACTIVE로 생성됨, 명시 필수
"purchaseType": "managedUser",
"defaultPrice": {"priceMicros": "4990000", "currency": "USD"},
}
앱 콘텐츠 선언 10종(타겟 연령층, 광고 정책, 앱 접근권한, 데이터 보안 섹션 등)은 API 자동화가 현실적으로 어렵다. Play 콘솔 UI에서만 입력받는 항목들이고, 일부는 API에 아예 노출이 안 된다. 이건 수동으로 채웠다. 콘텐츠 선언이 하나라도 미완료면 비공개 테스트 제출도 막힌다. 어떤 항목이 필수인지 체크리스트로 문서화해두지 않으면 다음에 또 뒤늦게 막힌다.
비공개 테스트 검토 제출까지 통과하고 앱 ID 4972258898827418117가 콘솔에 찍혀 있는 걸 보니 그제야 Play 쪽은 실감이 났다.
스크린샷 80장 자동화: Flutter 하네스 구현
8로케일 × 2기기(iPhone + iPad) × 5장 = 80장이다. 수동으로 찍으면 기기 세팅, 로케일 전환, 앱 내 상태 세팅을 160번 반복하게 된다. 그래서 lib/screenshots_harness.dart에 스크린샷 하네스를 구현했다.
integration_test 패키지 기반으로 각 스크린(홈, 편집, 내보내기, 구독, 설정)의 진입 시퀀스를 미리 기록해두고, locale과 deviceType을 파라미터로 받아 자동 캡처한다. CI에서 --dart-define=LOCALE=ko-KR --dart-define=DEVICE=iphone 형태로 구분해서 돌리면 각 조합 결과물이 지정 경로에 쌓인다.
| 기기 | 요구 해상도 | 로케일 수 | 장수 | 소계 |
|---|---|---|---|---|
| iPhone 6.5" | 1284×2778 | 8 | 5 | 40 |
| iPad Pro 12.9" | 2048×2732 | 8 | 5 | 40 |
하네스의 핵심은 "스크린 상태를 재현 가능하게 만드는 것"이다. 편집 화면처럼 미디어 파일이 있어야 보이는 화면은 assets/test_media/ 아래 샘플 파일을 번들에 포함시켜서, 테스트 시작 전 앱 내부 경로에 복사해두는 방식으로 해결했다. 이렇게 하지 않으면 스크린샷에 빈 상태가 찍힌다.
시뮬레이터 해상도도 함정이었다. ASC는 기기 종류마다 정확한 픽셀 수를 요구하고, 조금이라도 다르면 업로드 시점에서 거절한다. 처음에 iPhone 14 Pro Max로 찍었는데 해상도가 살짝 달라서 전부 다시 돌렸다. 기기 선택 기준을 하네스 실행 README에 고정해뒀다.
ASC에서 심사 제출 버튼이 회색이었던 이유 세 가지
Play 쪽이 어느 정도 정리된 새벽 2시쯤 ASC로 넘어갔더니 "Submit for Review" 버튼이 비활성화 상태였다. 차단 요소가 세 개였고, 각각 성격이 달랐다.
첫 번째는 App Privacy 선언 미완료다. ATT(앱 추적 투명성) 프레임워크를 쓰는 앱은 수집 데이터 유형, 목적, 추적 여부를 Privacy Nutrition Label 형식으로 ASC에서 별도 선언해야 한다. 이걸 입력하지 않으면 심사 제출 버튼 자체가 뜨지 않는다. vidpress는 광고 SDK가 들어 있어서 Advertising Data와 Device ID를 선언해야 했고, ATT 동의 흐름도 Info.plist에 NSUserTrackingUsageDescription을 이미 넣어뒀던 상태라 선언 내용과 일치시켰다.
두 번째는 가격 및 판매지역 미설정이다. 가격 티어와 판매 국가를 선택하지 않으면 제출이 막힌다. 무료 앱이라도 "무료 티어"를 명시적으로 골라야 한다. 당연히 됐을 거라 생각하고 지나쳤던 항목이었다.
세 번째가 가장 당황스러웠다. iOS IAP를 심사에 포함할 때 IAP 항목마다 리뷰 스크린샷 1장을 첨부해야 MISSING_METADATA 상태가 풀린다. 구매 화면이나 상품 노출 화면 스크린샷을 IAP 항목의 "Review Information" 섹션에 직접 업로드해야 한다. Play 쪽에는 이런 요구사항이 없어서 완전히 놓쳤다. 스크린샷 1장 업로드 후 상태가 READY_TO_SUBMIT으로 바뀌는 걸 보고 원인을 확신했다.
변경 전: MISSING_METADATA
변경 후: READY_TO_SUBMIT
원인: IAP 항목 > Review Information > 스크린샷 미첨부
ASC 리스팅 자체는 8개 로케일 각각에 앱 이름, 부제목, 설명, 키워드를 채웠다. store/listing.md를 기반으로 수동 복붙했는데, 언어 간 문자 수 제한이 달라서 특히 일본어/아랍어 쪽은 키워드 자르는 데 시간이 좀 걸렸다. 카테고리는 Photo & Video(기본) + Productivity(보조), 연령등급 4+로 확정했다.
수익화 스택 표준화: 광고 버그 수정과 이식
이날 새벽에 한 작업 중 가장 조용하지만 중요한 부분이 수익화·광고·동의 스택 이식이었다. 여러 프로젝트에서 동일한 패턴을 쓰다 보니 "best-of-breed" 구현을 하나 만들어두고 신규 앱에 이식하는 방식으로 정착했는데, 이번에 vidpress에 넣으면서 기존 구현에 있던 광고 SDK 초기화 타이밍 버그를 잡았다.
광고 초기화를 앱 시작 시점이 아니라 ATT 동의 콜백 이후로 미뤄야 하는데, 동의 팝업을 띄우기 전에 SDK가 초기화되면 동의 없이 추적이 시작되는 문제가 있었다. AndroidManifest.xml과 Info.plist 양쪽에서 초기화 지연 플래그를 세팅하고, 동의 결과를 받아 처리하는 흐름을 정리했다. 이식하면서 수정한 버그라 "vidpress 광고버그 수정"으로 커밋 메시지에 명시했다.
런타임 결정을 문서로: mobile-app-launch.md SSOT
새벽 4시가 넘어 모든 차단 요소가 해소됐을 때, 그 자리에서 바로 docs/mobile-app-launch.md를 썼다. 기억이 가장 선명할 때 적는 게 맞다. vidpress 전 과정을 처음부터 끝까지 직접 밟으면서 검증한 절차라 신뢰도가 높다.
특히 함정 항목들을 별도로 정리했다:
- 서명키 백업 필수 규칙: 키스토어 파일과 비밀번호를 OneDrive에 백업하지 않으면 앱 업데이트 자체가 불가능해진다. APK 서명 불일치로 Play 거절. 필수 규칙으로 명시했다.
- iOS IAP 리뷰 스크린샷:
MISSING_METADATA로 막히는 원인 1위. IAP 항목마다 1장 필수. - App Privacy / ATT 선언: 심사 제출 차단 요소 중 가장 눈에 안 띄는 항목. 광고 SDK 여부에 따라 선언 범위가 다르다.
- Play 콘텐츠 선언 10종: 항목 하나라도 미완료면 비공개 테스트 제출도 막힌다.
docs/hedvion-CLAUDE.md에도 이 문서로의 참조를 추가했다. 다음 앱 출시 작업을 할 때 봇이 이 문서를 단일진실로 참조하도록.
자정부터 여섯 시간 작업의 산출물을 정리하면: Play 비공개 테스트 제출, ASC 심사 제출 차단 요소 3종 해소, 스크린샷 하네스로 80장 자동화, Play API 자동화 스크립트 4종, 수익화 스택 이식 및 버그 수정, 다음 출시에 쓸 SSOT 문서 한 벌이다. 앱을 스토어에 올리는 공정이 코드를 짜는 것만큼, 어쩌면 그 이상으로 체크리스트 관리와 자동화에 손이 가는 영역이라는 걸 이날 새벽에 다시 확인했다. 그리고 그 삽질의 대부분은 문서로 남겨뒀으니, 다음 번엔 적어도 같은 이유로 새벽을 지새우지는 않을 것이다.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.