첫 앱 출시기 3 — 광고를 보여줘도 되는 순간
계산기에 배너·전면·보상형 광고를 넣으며 동의, 구매, 저장 완료 상태에 따라 요청을 나눴습니다. 에뮬레이터에서 광고가 비어 있던 원인은 DNS였습니다.
들어가며
물타기 계산기에 광고를 넣으면서 먼저 자리를 정했습니다. 배너는 바텀바 위, 전면 광고는 계산을 저장한 뒤 세 번에 한 번, 보상형 광고는 무료 종목 3개를 넘겨 만들 때 보여주기로 했습니다.
자리를 정하고 나니 광고를 요청할 조건이 남았습니다. 동의가 끝났는지, 광고 제거 상품을 샀는지, 계산은 저장됐는지 확인해야 했습니다. 2026년 8월 테스트 트랙을 준비하며 광고를 붙이는 데 하루를 썼습니다.
ID 두 개부터 구분하기
AdMob 콘솔에서는 앱과 광고 단위를 따로 만듭니다.
- 앱 ID (
ca-app-pub-…~…): 앱마다 하나이며 매니페스트에 넣습니다. - 광고 단위 ID (
ca-app-pub-…/…): 배너·전면·보상형처럼 광고 자리마다 만들고 코드에서 사용합니다.
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-…~…" />
앱 ID가 없거나 잘못되면 SDK가 예외를 발생시켜 앱이 시작하자마자 종료됐습니다. 처음에는 광고 설정과 앱 실행 실패를 연결하지 못했습니다. ID 안의 물결(~)과 빗금(/)을 구분하는 데서 시작했습니다.
광고 형식도 맞아야 했습니다. 콘솔의 ‘보상형’과 ‘보상형 전면(베타)’을 혼동해 코드와 다르게 만들었을 때는 SDK가 로드를 거절했습니다. 에러 메시지만 보고는 원인을 바로 찾지 못했습니다.
테스트 광고와 실제 광고를 매번 손으로 바꾸지 않도록 빌드에서 선택하게 했습니다.
object AdUnits {
private const val TEST_BANNER = "ca-app-pub-3940256099942544/9214589741"
val BANNER = if (BuildConfig.DEBUG) TEST_BANNER
else "ca-app-pub-…/…" // 실제 단위
}
디버그에는 구글 공식 테스트 단위를, 릴리스에는 실제 단위를 사용합니다. 매니페스트의 앱 ID는 양쪽 모두 실제 ID입니다. 당시 구글 안내에 따라 이 조합을 썼습니다. 테스트 ID로 출시하면 수익이 나지 않고, 개발 중 실제 광고를 반복해서 누르는 것도 무효 트래픽 문제를 만들 수 있어서 빌드 설정에 맡겼습니다.
동의와 구매 상태를 먼저
당시 EEA·영국·스위스에 광고를 제공할 때는 인증된 동의 플랫폼(CMP)이 필요했습니다. 구글의 UMP(User Messaging Platform) SDK를 붙여 필요한 동의 절차가 끝나기 전에는 광고를 요청하지 않도록 했습니다.
모든 사용자에게 폼을 띄우지는 않습니다. UMP가 동의 필요 여부를 판단하고, 한국 사용자에게 동의가 불필요하다고 응답하면 폼 없이 다음 단계로 넘어갑니다.
object ConsentGate {
var canRequestAds by mutableStateOf(false)
private set
…
}
배너, 전면, 보상형 모두 요청 전에 이 값을 봅니다. 동의를 다시 선택할 수 있어야 하는 지역에는 설정의 ‘광고 개인정보 선택’을 보여주고, 필요 없는 경우에는 숨겼습니다.
광고 제거용으로는 Play Billing의 비소모성 상품 하나를 등록했습니다. 앱 시작 시 같은 구글 계정의 구매 상태를 조회하므로, 재설치 뒤에도 별도의 복원 버튼 없이 광고를 제거할 수 있습니다. 소유 상태를 조회하면서 미확인 구매도 처리했습니다. 구매를 확인(acknowledge)하지 않으면 3일 뒤 자동 환불되는 조건이 있었기 때문입니다.
구매를 확인한 뒤에는 광고뿐 아니라 광고 제거 구매 항목도 숨겼습니다. 이미 산 상품을 다시 살 수 있는 것처럼 보일 필요가 없었습니다.
계산하는 중에는 기다리지 않도록
배너는 로드 전에 높이를 비워뒀습니다. 광고가 도착하면서 화면이 밀리면 사용자가 누르려던 버튼 자리에 광고가 올 수 있습니다. 키패드가 올라와 숫자를 입력하는 동안에는 배너를 숨겼습니다.
전면 광고는 저장을 끝냈고, 광고도 미리 로드된 경우에만 보여줬습니다.
fun onSaved(activity: Activity) {
saves += 1
if (saves % everyN != 0) { preload(activity); return }
val ad = loaded ?: run { preload(activity); return } // 준비 안 됐으면 그냥 보냅니다
…
}
광고가 준비되지 않았다면 다음 로드를 요청하고 저장 흐름은 끝냅니다. 광고를 받느라 저장 완료를 늦추지 않으려는 순서입니다.
배너 객체의 수명도 바꿨습니다. 처음에는 컴포저블 안에서 AdView를 만들었는데, 키패드를 여닫을 때마다 객체가 파괴되고 새 광고를 요청했습니다. 그 과정에서 배너가 비는 현상을 봤습니다. 잦은 요청이 광고 채움에 영향을 줄 가능성도 고려해, 배너를 한 번 만든 뒤 앱이 실행되는 동안 재사용하도록 했습니다.
광고 자리만 보였던 반나절
에뮬레이터에서는 배너 공간이 생겨도 광고가 계속 비어 있었습니다. 로그에 남은 내용은 다음과 같았습니다.
Unable to resolve host … No address associated with hostname
Ad failed to load : 0
기본 설정으로 실행한 에뮬레이터가 googleads.g.doubleclick.net을 해석하지 못하고 있었습니다. 원인을 찾는 데 반나절이 걸렸습니다.
emulator -avd <이름> -dns-server 8.8.8.8,1.1.1.1
DNS를 지정해 다시 실행하자 테스트 광고가 표시됐습니다. 광고 자리와 ID는 그대로 둔 채로였습니다.
마치며
광고 요청에는 동의와 구매 상태를 반영했고, 전면 광고는 저장이 끝난 뒤 준비된 경우에만 보여주게 했습니다. 광고를 기다리느라 계산이나 저장 흐름이 멈추지 않도록 순서를 정했습니다.