첫 앱 출시기 3 — AdMob 붙이기 - 광고 세 종류와 동의, 계정 정지를 피하는 법
앱 ID를 매니페스트에 넣는 것부터 EEA 동의 폼까지. 배너·전면·보상형 세 자리를 붙이면서 실제로 막혔던 곳들과, 테스트 광고를 실수로 출시하지 않기 위해 빌드에 맡긴 장치를 이야기합니다.
지난 글에서는 HTML 목업 한 장으로 시작한 설계 과정을 이야기했어요. 목업에서 합의하고 앱으로 옮기는 사이클, 그리고 목업의 역할은 빠른 시작이었으니 그 시작이 끝났을 때 놓아줬다는 이야기까지요.
여기까지가 만드는 이야기였고, 이번 글부터는 절차를 통과하는 이야기입니다. 그 첫 번째가 광고예요.
앱을 다 만들고 나면 “이제 광고만 붙이면 되겠네” 싶은데, 그게 하루가 걸립니다. 코드가 어려워서가 아니라 콘솔을 오가며 값을 만들어 오는 일이 많아서예요.
순서 — 무엇을 먼저 만들어야 하나
AdMob은 값을 두 층으로 줍니다. 이걸 헷갈리면 계속 막혀요.
- 앱 ID (
ca-app-pub-…~…) — 앱 하나에 하나. 물결(~)이 들어갑니다 - 광고 단위 ID (
ca-app-pub-…/…) — 광고 자리마다 하나. 빗금(/)이 들어갑니다
앱 ID는 매니페스트에 박고, 단위 ID는 코드에서 씁니다.
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-…~…" />
이 값을 빠뜨리거나 틀리게 넣으면 앱이 뜨자마자 죽습니다. SDK가 ID 불일치로 예외를 던지거든요. “광고가 안 나온다”가 아니라 “앱이 안 켜진다”라서 처음엔 광고 문제인 줄도 몰랐어요.
광고를 세 자리에 넣었어요
- 배너 — 바텀바 위에 한 줄
- 전면 — 계산을 저장한 뒤에, 세 번에 한 번
- 보상형 — 무료 종목 3개를 넘겨 만들 때
각각 AdMob 콘솔에서 광고 단위를 따로 만들어야 합니다. 그리고 여기서 한 번 밟았어요. 보상형과 보상형 전면(베타)은 서로 다른 형식입니다.
콘솔에서 어느 카드로 만들었는지가 곧 코드에서 쓸 클래스를 정해요. 잘못 만들면 SDK가 로드를 거절하는데, 에러 메시지가 그걸 친절히 말해주지 않습니다.
테스트 ID로 출시하거나, 실제 ID로 개발하거나
이 실수는 방향이 반대인 두 가지인데 둘 다 치명적이에요.
- 테스트 ID인 채로 출시 → 수익이 0
- 실제 ID로 개발 기기에서 눌러댐 → 계정 정지
사람의 기억에 맡길 일이 아니라서 빌드에 맡겼습니다.
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 + 테스트 광고 단위”거든요.
동의가 먼저입니다 (UMP)
이게 제일 몰랐던 부분이에요.
EEA·영국·스위스 사용자에게 광고를 내보내려면 인증된 동의 플랫폼(CMP)이 필요하고, 동의가 서기 전에 광고 요청을 열면 정책 위반입니다. 다행히 구글이 UMP(User Messaging Platform) SDK를 무료로 줍니다.
핵심은 “폼을 모두에게 보여주기”가 아니라 “물어야 하는 사람에게만 묻기”예요. 한국 사용자에게는 폼이 아예 안 뜹니다. UMP가 “동의 불필요 지역”이라고 답하는 즉시 광고를 열 수 있어요.
그래서 광고 세 자리가 전부 문 하나를 봅니다.
object ConsentGate {
var canRequestAds by mutableStateOf(false)
private set
…
}
배너도 전면도 보상형도 요청 전에 이 값을 확인해요. 문이 여러 개면 그중 하나는 반드시 잠그는 걸 잊게 되더라고요.
그리고 동의를 되물릴 권리가 있는 지역에서는 설정에 “광고 개인정보 선택” 줄이 서야 합니다. 이것도 CMP 요구사항이에요. 반대로 물은 적 없는 지역에는 그 줄을 세우지 않습니다. 무엇을 되묻는지 알 수 없으니까요.
광고 자리를 정할 때 지킨 것
정책 위반이기 전에 나쁜 앱이 되는 것들이 있어요.
높이를 미리 잡아둡니다
광고가 도착하는 순간 화면이 밀려 내려가면, 사용자가 막 누르려던 것이 광고 자리로 미끄러져 들어옵니다. 그래서 광고가 오기 전에도 그 높이만큼 자리를 비워둬요.
숫자 치는 손 옆에 광고를 안 세웁니다
키패드가 올라오면 배너도 함께 비킵니다. 오터치가 수익이 되는 구조는 만들면 안 돼요. 그 수익은 받으면 안 되는 돈입니다.
전면은 저장 뒤에만 띄웁니다
저장은 이미 끝난 행동이라 광고가 일을 막지 않아요. 계산 도중에 끼어들면 화면의 흐름이 두 번 끊깁니다.
그리고 미리 로드돼 있을 때만 보여줍니다. 광고를 기다리게 하는 순간 “저장”이 느린 버튼이 되거든요.
fun onSaved(activity: Activity) {
saves += 1
if (saves % everyN != 0) { preload(activity); return }
val ad = loaded ?: run { preload(activity); return } // 준비 안 됐으면 그냥 보냅니다
…
}
광고는 한 번 만들어 앱이 사는 동안 씁니다
배너를 컴포저블 안에서 만들었더니 키패드를 여닫을 때마다 AdView가 파괴되고 다시 만들어졌어요.
그때마다 새 요청이 나갔습니다.
요청이 잦으면 채워주지 않고(실제로 빈 배너가 됐어요), 잦은 요청 자체가 무효 트래픽으로 읽힙니다.
광고 제거를 파는 것도 결국 광고 일이에요
“광고 빼고 싶으면 결제해주세요”를 넣었습니다. 인앱 결제(Play Billing)예요.
파는 것은 비소모성 상품 하나입니다. 한 번 사면 영원히 광고가 사라지고, 앱을 지웠다 깔아도 구글 계정에 구매가 남아요. 그래서 앱이 뜰 때마다 소유를 다시 확인해서 돌려줍니다. 복원 버튼을 따로 안 만든 이유가 이거예요.
여기서 꼭 알아야 할 게 하나 있어요.
확인(acknowledge)을 잊으면 구글이 3일 뒤 자동 환불합니다. 돈은 받았는데 사흘 뒤 사라지고, 사용자는 “샀는데 광고가 나온다”고 문의해요. 그래서 소유를 볼 때마다 미확인 구매에는 즉시 확인 도장을 찍습니다.
그리고 이미 산 사람에게는 광고 제거 줄을 아예 안 세웁니다. 산 것을 또 팔면 “내가 산 게 맞나”부터 의심하게 되고, 그 의심은 환불 문의로 돌아오더라고요.
에뮬레이터에서 광고가 안 뜬다면
마지막으로, 반나절을 날린 함정 하나 알려드릴게요.
기본으로 띄운 안드로이드 에뮬레이터는 googleads.g.doubleclick.net을 해석하지 못합니다.
Unable to resolve host … No address associated with hostname
Ad failed to load : 0
배너 자리는 잡히는데 광고만 안 채워지니 앱 버그처럼 보여요. DNS를 직접 주고 띄우면 테스트 광고가 정상으로 뜹니다.
emulator -avd <이름> -dns-server 8.8.8.8,1.1.1.1
다시 같은 일을 한다면
여기까지 하면 광고가 붙은 앱이 손에 있습니다. 이제 이걸 스토어에 올려야 하는데, 그게 또 다른 이야기예요.
마지막 편에서는 서명 키, 되돌릴 수 없는 versionCode, 필수 고지, 그리고 콘솔에 안 들어가고 명령 한 줄로 올리는 방법을 이야기할게요.