출시 준비 중인 간헐적 단식 앱에 애드몹 광고 네 종류(배너·전면·App Open·보상형)를 붙였다. 광고는 붙이는 것보다 “언제 띄우고 언제 참을지”를 설계하는 게 훨씬 어렵다. 성가시면 사용자는 앱을 지운다. 실제로 어떻게 짰는지, 그리고 실기기에서만 드러난 함정을 공유한다.
광고 4종을 어디에 뒀나
- 배너: 홈 하단, 리포트 하단(상시 노출).
- 전면(Interstitial): 단식 완주 후 “식사 시작”을 누르는 한 지점에만. 성취 직후라 이탈 저항이 낮은 순간을 골랐다.
- App Open: 앱을 백그라운드에서 다시 열 때(웜복귀).
- 보상형(Rewarded): 사용자가 자발적으로 보면 3시간 광고를 없애준다.
수익화 논리는 명확했다. 애드몹 수익 ≈ DAU × 세션당 노출 × eCPM. 일반 단식앱은 하루 2번·몇 초라 광고 자리가 없다. 그래서 “배고픈 순간 코치” 같은 앱을 하루 여러 번 열 이유를 만들어 노출 지면을 늘렸다 — 제품 설계와 수익모델이 같은 방향을 본다.
핵심은 ‘빈도 캡’ — 광고가 겹치면 최악이다
App Open과 전면을 각각 두면, 완주하고 앱을 닫았다 다시 여는 순간 전면 + App Open이 연달아 뜨는 참사가 난다. 그래서 두 전면 계열을 하나의 4시간 캡으로 묶었다. “마지막으로 전면류를 띄운 시각”을 저장하고, 4시간이 안 지났으면 App Open도 전면도 억제한다.
게이팅 상수는 도메인 한 곳(ad_gate.dart)에 모았다 — kFullscreenAdCap = 4h, kRewardedAdFreeDuration = 3h. 모든 광고는 shouldShowAds 게이트를 통과해야 뜨고, 광고제거 기간이면 배너까지 전부 억제된다. 상수를 한 곳에 모으니 단위테스트로 게이팅 로직을 통째로 검증할 수 있었다.
실기기에서만 터진 함정: drift 스트림이 백그라운드 쓰기를 놓친다
여기서 하루를 잡아먹었다. 처음엔 “마지막 광고 시각”을 drift의 watch 스트림으로 구독해 캡을 판정했다. 시뮬레이터에선 잘 됐다. 그런데 실기기 릴리스에서 App Open이 복귀할 때마다 떴다. 4시간 캡이 안 먹은 것이다.
원인은 이랬다. 앱이 백그라운드에 있는 동안 일어난 DB 쓰기를, watch 스트림이 런타임에 반영하지 못하는 경우가 있다. App Open은 정확히 “백그라운드→복귀” 순간에 판정하는데, 그때 스트림 값이 낡아 있으니 캡이 무력화됐다.
해결: 캡·광고제거처럼 정확성이 중요한 게이팅은 스트림 구독이 아니라 그 순간 DB를 직접 읽어 판정한다(
getLastFullscreenAdAt()직접 호출). 화면 UI처럼 능동 구독이 필요한 곳은 watch를 그대로 쓴다.
교훈
- 광고는 배치보다 빈도 설계가 수익과 이탈을 가른다. 전면류는 하나의 캡으로 묶어라.
- 게이팅 상수/로직은 도메인 한 곳에 모아 테스트 가능하게.
- 생명주기에 걸린 판정(App Open)은 스트림을 믿지 말고 그 순간 DB를 직접 읽어라.
- 이런 버그는 debug/시뮬에서 안 나오고 릴리스 실기기에서만 나온다. 테스트 기기는 콘솔에 GAID를 등록해 “테스트 광고” 라벨을 확인하고 클릭해야 계정이 안전하다.