작업 일지

첫 프레임 전에는 아무것도 기다리지 않는다

Play 심사가 계산기 앱 하나를 '앱이 응답하지 않음'으로 거절했다. 첨부된 스크린샷은 스플래시였다. 에뮬레이터에서는 1초 만에 뜨는 앱이었다.

2분#android#flutter#play#troubleshooting

거절 메일의 제목은 "조치 필요: 앱이 Google Play 정책을 준수하지 않음"이었고, 본문에는 "앱이 응답하지 않음"과 스크린샷 파일 하나. 열어 보니 런치 스플래시, 아이콘만 덩그러니. Flutter의 첫 프레임이 끝내 그려지지 않은 것이다.

어디서 세부를 보는가

콘솔의 정책 센터에는 "손상된 기능 정책 위반 — 거부됨"과 날짜만 있다. 어떤 화면에서 어떻게 멈췄는지는 메일 첨부에만 있다. API로 상태를 물으면 재제출 뒤라 IN_REVIEW만 돌아온다. 거절은 메일을 찾아 읽어야 한다.

원인

앱 99개가 공유하는 main()이 이렇게 생겼었다.

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initAds();        // MobileAds.initialize + 게이트
  await billing.init();   // Play Billing 연결
  runApp(const App());
}

에뮬레이터에서는 온라인이든 오프라인이든 1초 안에 뜬다. 문제는 심사 기기다. WebView가 비활성이거나, Play 계정이 없거나, GMS 광고 모듈이 없는 환경에서는 MobileAds.initialize나 Billing 연결이 플랫폼 스레드를 잡고 놓지 않거나 예외를 던진다. 둘 다 runApp 앞에 있으니 첫 프레임이 영원히 오지 않는다.

우리 쪽에서 재현은 안 됐다. 재현이 안 돼도 구조가 위험하다는 건 분명했다.

수정

공용 패키지에서 고쳤다. 앱 쪽 main()은 그대로 둬도 된다. await가 즉시 끝나기 때문이다.

  • initAds()는 즉시 반환한다. 게이트와 MobileAds.initialize는 addPostFrameCallback 뒤에서 돌고, 끝나면 adsReady를 완료한다.
  • 배너·전면·리워드·앱오픈 광고의 load()는 adsReady를 기다린 뒤 유닛 ID를 읽는다. 게이트 전에 읽으면 실배포에서도 테스트 유닛이 나간다.
  • Billing.init()도 첫 프레임 뒤에 연결한다. isAvailable에 10초 상한. 상품 조회와 복원은 백그라운드로 돌고, 타일은 productsLoaded 노티파이어를 구독해 채워진다.

규칙은 한 줄이다. 첫 프레임 전에 네트워크나 플랫폼 SDK를 기다리지 않는다. 새 앱 템플릿에도 같은 규칙이 들어갔다.

검증에서 배운 것

스플래시가 끝났는지 화면의 흰색 비율로 판정하려다 실패했다. 크림색 배경 UI가 0.91로 나와서 스플래시로 보인다. 대신 print('PROBE …')와 addPostFrameCallback으로 첫 프레임 시점을 직접 찍었다.

안전 빌드로 에뮬레이터 온라인은 첫 프레임 뒤 배너, 오프라인은 5초 안에 UI. 그날 밤 33개 앱을 다시 빌드해 재제출했다.