Android AdMob Native Ads Compose와 함께 적용하기
AdMob에는 다양한 광고 단위가 있습니다. 배너와 전면 광고, 리워드 광고도 사용해 보았지만 이번에는 네이티브 고급 광고를 사용하여 앱의 디자인과 어울리도록 광고를 추가해 본 과정에 대해서 정리하였습니다.
Native Ads 란?
배너 광고와 네이티브 고급 광고를 제외하고 나머지 광고들은 UI에서 광고를 실행하는 게 아닌 광고 객체에서 직접 실행할 수 있는 형태입니다. 그렇기 때문에 직접적으로 UI에서 설정할 필요 없이 광고를 Load 하는 로직과 보여주는 로직, 혹은 리워드를 처리하는 콜백을 설정하는 등의 작업이 필요합니다.
하지만 배너 광고와 네이트브 광고는 직접 UI에 형태와 위치를 지정해야 합니다.
배너 광고의 경우 어디에 배너가 위치해야 할지, 배너의 크기는 어떤 크기로 할지 정해야 한다면, 네이티브 광고는 더 자유롭게 커스텀할 수 있습니다.
앱의 디자인과 분위기에 어울리게 직접 커스텀하여 조금 더 자연스럽게 광고를 사용자에게 노출할 수 있고, 이미 많은 유명한 앱에서, 스크롤 중에 자연스럽게 광고가 들어있는 부분이 네이티브 광고를 활용하여 구현한 것 같습니다.
Compose로 Native Ads 구현하기
AdMob ID 등록이나, 기본 설정의 경우는 생략하고 네이티브 광고 구현에 집중하여 작성하겠습니다.
많은 자료들이 기존 xml을 활용하여 구현된 부분이 많았습니다.
공식 문서에서도 Compose로 구현한 예시를 보여주고 있습니다.
NativeAdView 및 애셋을 컴포지션 하는 도우미가 포함된 JetpackComposeDemo/compose-util 모듈을 포함합니다.
compose-util 모듈을 사용하여 NativeAdView를 작성합니다.
ComposeDemo repository의 앱을 설치하고 실행해 보면서 Compose에서 구조를 파악했지만, onAdImpression와 onAdClicked 콜백을 받지 못하며 광고가 정상적으로 동작하지 않았습니다.
Note: The Jetpack Compose button implements a click handler which overrides the native ad click handler, causing issues. Use the NativeAdButton which does not implement a click handler. To handle native ad clicks, use the NativeAd AdListener onAdClicked callback.
@Composable
fun NativeAdButton(text: String, modifier: Modifier = Modifier) {
Box(
modifier =
modifier
.background(Color.Black)
.clip(ButtonDefaults.shape)
.padding(ButtonDefaults.ContentPadding)
) {
Text(color = Color.Red, text = text)
}
}
Native Ads의 View에서는 클릭 이벤트를 개발자가 직접 구현하여 연결하는 것이 아니기 때문에, 이 부분에서 Compose와의 호환이 아직 부족한 것 같습니다.
결국 xml Layout과 Compose를 함께 사용하는 방법으로 구현하기로 하였습니다.
광고를 Load 하는 로직은 Compose Demo를 참고하되 View를 연결하는 방식은 xml과 viewBinding을 사용하여 처리하도록 하였습니다.
xml Layout 만들기
xml 레이아웃에서는 NativeAdView를 사용하여 어떤 모습으로 UI에 표시할지 설정합니다.
https://github.com/googleads/googleads-mobile-android-native-templates
GitHub - googleads/googleads-mobile-android-native-templates
Contribute to googleads/googleads-mobile-android-native-templates development by creating an account on GitHub.
github.com
이 레포지토리에서는 네이티브 광고의 템플릿도 제공하고 있습니다.
네이티브 광고에서 전달받을 수 있는 필드와 표시 요구 사항에 대한 내용은 아래의 표에서 확인할 수 있습니다.
필드 | 설명 | 광고 응답에 포함됨 | 표시 요구 사항 |
광고 배지 | 광고가 광고임을 명확하게 표시하는 광고 배지입니다. | 항상 | 필수 |
제목 | 기본 광고 제목 텍스트입니다.이 텍스트는 25자(영문 기준)를 넘으면 잘릴 수도 있습니다. † | 항상 | 필수 |
이미지(동영상 광고가 아닌 경우) | 커다란 기본 이미지입니다. | 항상 | 권장 |
동영상(동영상 광고의 경우) | 동영상 광고를 재생하기 위해 필요한 모든 애셋을 포함한 동영상 VAST 응답입니다. | 항상 | 필수* |
아이콘 | 정사각형 가로세로 비율(1:1)의 작은 앱 아이콘 또는 광고주 로고입니다. | 앱 설치 광고의 경우 항상 포함됨콘텐츠 광고의 경우 항상 포함되지는 않음 | 제공되는 경우 필수 |
클릭 유도 문구 | 사용자의 액션을 유도하는 버튼 또는 텍스트 필드입니다(예: '사이트 방문', '설치').버튼이나 텍스트 대신 앱 다운로드 아이콘을 표시할 수 있습니다. 이 텍스트는 15자(영문 기준)를 넘으면 잘릴 수도 있습니다. † |
항상 | 필수 |
본문 | 보조 본문 텍스트입니다(예: 기사 또는 앱 설명).이 텍스트는 90자(영문 기준)를 넘으면 잘릴 수도 있습니다. † | 항상 | 권장 |
별표 평점(앱 설치 광고) | 스토어에 있는 앱의 평점을 나타내는 0에서 5까지의 별점 | 항상 포함되지는 않음 | 권장 |
광고주 이름 | 광고주를 식별하는 텍스트입니다(예: 광고주 이름, 브랜드 이름, 표시 URL).이 텍스트는 25자(영문 기준)를 넘으면 잘릴 수도 있습니다. † | 콘텐츠 광고의 경우 항상 포함됨 | 권장 |
이번 네이티브 광고에는 필수 요구 사항인 광고배지, 제목, 영상 및 이미지, 아이콘, 클릭 유도 문구와 추가로 별표 평점까지 UI에 반영하도록 구현하였습니다.
<com.google.android.gms.ads.nativead.NativeAdView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/parentNative"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="ResourceName">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible">
<com.google.android.gms.ads.nativead.MediaView
android:id="@+id/adMedia"
android:layout_width="0dp"
android:layout_height="120dp"
android:layout_gravity="center_horizontal"
app:layout_constraintDimensionRatio="4:3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:background="@color/core_admob_yellow"
android:text="@string/core_admob_ad"
android:textColor="@color/core_admob_text"
android:textSize="12dp"
app:layout_constraintStart_toStartOf="@+id/adMedia"
app:layout_constraintTop_toTopOf="@id/adMedia"
tools:ignore="SpUsage" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/actionLayout"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/adMedia"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/adAppIcon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="8dp"
app:layout_constraintBottom_toTopOf="@id/ad_call_to_action"
app:layout_constraintEnd_toStartOf="@id/adHeadline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/adHeadline"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="3"
android:text=""
android:textSize="12dp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/adStars"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/adAppIcon"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="SpUsage" />
<RatingBar
android:id="@+id/adStars"
style="?android:attr/ratingBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:isIndicator="true"
android:numStars="5"
android:stepSize="0.5"
app:layout_constraintBottom_toTopOf="@id/ad_call_to_action"
app:layout_constraintStart_toStartOf="@id/adHeadline"
app:layout_constraintTop_toBottomOf="@id/adHeadline" />
<Button
android:id="@+id/ad_call_to_action"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/core_admob_primary"
android:gravity="center"
android:text=""
android:textSize="12dp"
app:layout_constraintBottom_toBottomOf="parent"
tools:ignore="SpUsage" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.gms.ads.nativead.NativeAdView>
앱의 분위기와 맞추며 최대한 간단하게 디자인할 수 있었습니다.
고려해야 할 부분은 MediaView와 아이콘 등의 크기와 텍스트 길이가 광고 요구사항에 맞아야 합니다.
동영상 광고의 동영상 요소는 가로세로 비율을 4:3, 16:9, 1:1, 3:4, 9:16 중 한 가지 또는 Google에서 승인한 비율로 유지해야 합니다. 비인스트림 광고는 더 긴 쪽의 크기가 256픽셀 이상이어야 합니다. 위의 가로 세로 비율은 각각 256x192, 256x144, 256x256, 192x256 또는 144x256 이상으로 변환됩니다.
textSize의 경우 sp로 설정할 경우, 사용자의 디바이스에 설정된 글자 크기에 따라 광고 layout이 너무 커다랗게 되어 앱의 UI를 가리게 되거나, 텍스트가 짤리는 경우가 발생하여 광고 정책에 위반되는 경우가 발생할 가능성이 있었습니다. 그렇기 때문에 textSize를 dp로 설정하였습니다. 디바이스의 글자 크기와 상관없이 광고 layout이 설정돼도 상관없다면 sp로 설정하여 글자 크기에 자유롭게 구성할 수도 있을 것 같습니다.
https://support.google.com/admanager/answer/7031536
네이티브 광고 고급형을 사용하는 프로그래매틱 네이티브 광고 - Google Ad Manager 고객센터
도움이 되었나요? 어떻게 하면 개선할 수 있을까요? 예아니요
support.google.com
더 자세한 가이드라인은 여기에서 확인할 수 있습니다.
광고 불러오기
다음은 광고를 불러오는 로직을 작성하였습니다.
fun loadNativeAd(context: Context, onAdLoaded: (NativeAd) -> Unit) {
val adLoader =
AdLoader.Builder(context, BuildConfig.ADMOB_NATIVE_ADS_ID)
.forNativeAd { nativeAd -> onAdLoaded(nativeAd) }
.withAdListener(object : AdListener() {
override fun onAdFailedToLoad(error: LoadAdError) {
super.onAdFailedToLoad(error)
println("Native ad failed to load: ${error.message}")
}
override fun onAdLoaded() {
super.onAdLoaded()
println("Native ad was loaded.")
}
override fun onAdImpression() {
super.onAdImpression()
println("Native ad recorded an impression.")
}
override fun onAdClicked() {
super.onAdClicked()
println("Native ad was clicked.")
}
}).withNativeAdOptions(
NativeAdOptions.Builder().setAdChoicesPlacement(
NativeAdOptions.ADCHOICES_TOP_RIGHT
).build()
).build()
adLoader.loadAd(AdRequest.Builder().build())
}
AdLoader 빌더를 사용하고, local.properties에 작성해 둔 광고 아이디를 BuildConfig를 사용하여 불러와 광고를 불러올 수 있습니다. 광고가 불러와지면 onAdLoaded 함수에 넘겨줄 수 있도록 하며, Listener를 설정하여 이벤트를 전달받을 수 있습니다. 하지만 특별히 리스너에서 필요한 동작이 없다면 생략해도 됩니다.
추가로 테스트용 Native Ads의 광고 ID는 ca-app-pub-3940256099942544/2247696110 입니다.
NativeAdOptions를 통해 AdChoices의 위치를 설정할 수 있습니다.
사용자에게 광고임을 알리는 광고 표시와 광고에 대한 정보를 제공할 수 있는 AdChoices, 두 가지를 필수로 설정해야 합니다.
Composable과 xml Layout 연결하기
@Composable
private fun NativeAds(
nativeAd: NativeAd,
modifier: Modifier = Modifier
) {
val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
AndroidViewBinding(
modifier = modifier,
factory = SmallNativeAdsViewBinding::inflate
) {
val adView = root.also { view ->
view.headlineView = this.adHeadline
view.callToActionView = this.adCallToAction
view.starRatingView = this.adStars
view.iconView = this.adAppIcon
view.mediaView = this.adMedia
}
this.container.setBackgroundColor(backgroundColor)
nativeAd.icon?.let {
this.adAppIcon.setImageDrawable(it.drawable)
} ?: run {
adAppIcon.visibility = View.INVISIBLE
}
nativeAd.headline?.let {
this.adHeadline.text = it
this.adHeadline.setTextColor(textColor)
} ?: run {
adHeadline.visibility = View.INVISIBLE
}
nativeAd.callToAction?.let {
this.adCallToAction.text = it
} ?: run {
adCallToAction.visibility = View.INVISIBLE
}
nativeAd.starRating?.let {
this.adStars.rating = it.toFloat()
} ?: run {
adStars.visibility = View.INVISIBLE
}
nativeAd.mediaContent?.let {
this.adMedia.mediaContent = it
} ?: run {
adMedia.visibility = View.INVISIBLE
}
adView.setNativeAd(nativeAd)
}
}
nativeAd를 전달받아 xml layout을 그리는 Composable을 만들었습니다.
AndroidViewBinding을 사용하여 viewBinding을 쉽게 사용할 수 있도록 합니다.
small_native_ads_view.xml의 바인딩 객체를 통해 연결하고, NativeAdView에 연결하는 작업이 필요합니다.
그리고 NativeAd 객체에 포함된 광고 정보를 layout에 만들어둔 id와 연결하여 UI에 반영합니다.
광고마다 값이 있을 수도 없을 수도 있기 때문에 null을 처리하는 것도 중요합니다.
mediaContent는 MediaView에 설정하면 동영상 광고의 경우 영상을 렌더링 하고, 비디오 요소가 없으면 자동으로 이미지를 렌더링 합니다.
광고 불러오기를 포함한 Composable
@Composable
fun SmallNativeAd(
modifier: Modifier = Modifier
) {
var nativeAd by remember { mutableStateOf<NativeAd?>(null) }
val context = LocalContext.current
var isDisposed by remember { mutableStateOf(false) }
DisposableEffect(Unit) {
loadNativeAd(
context = context,
onAdLoaded = { ad ->
if (!isDisposed) {
nativeAd = ad
} else {
ad.destroy()
}
},
)
onDispose {
isDisposed = true
nativeAd?.destroy()
nativeAd = null
}
}
nativeAd?.let { ad -> NativeAds(ad, modifier) }
}
NativeAd를 불러오는 로직을 포함하여 정상적으로 불러왔을 경우 NativeAds를 보여주는 Composable을 만듭니다.
여기서는 DisposableEffect를 사용하여 nativeAd를 올바르게 destroy 하여 리소스를 해제하고 메모리 누수를 방지합니다.
isDisposed 변수의 경우, 사용하지 않아도 괜찮지 않을까 했지만, 광고를 불러오는 로직이 비동기로 수행되기 때문에 dispose 된 상태에서 광고가 불러와지면 메모리 누수가 일어날 수 있기에 더 안전하게 처리하는 방법이라고 합니다.
UI에 적용해 보기
앱의 홈 화면 하단에 광고를 추가하였습니다. 저는 기본 광고 Composable에 Card를 활용하여 커스텀하였습니다.
Card(
modifier = Modifier
.fillMaxWidth()
.padding(Paddings.small),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(
defaultElevation = 2.dp
)
) {
SmallNativeAd(
modifier = Modifier.fillMaxWidth()
)
}
테스트 광고를 적용하면 하단에 광고가 적용되어 잘 나오게 됩니다.
테스트 광고일 경우 광고가 잘 적용되었는지 validator가 함께 나오게 됩니다.
우측 사진은 MediaView의 높이를 100dp로 설정하여 최소 길이인 120dp보다 작게 설정하였을 경우, 광고가 제대로 적용되지 않고 정책에 위반되었다는 것을 알려줍니다.
See issues를 통해 어떤 부분이 잘못되었는지 바로 확인할 수 있습니다.
<application>
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="${ADMOB_APP_ID}" />
<meta-data
android:name="com.google.android.gms.ads.flag.NATIVE_AD_DEBUGGER_ENABLED"
android:value="false" />
</application>
validator는 테스트 광고일 때 기본적으로 함께 포함되어 나오게 됩니다.테스트 광고일 경우에도 validator가 출력되지 않도록 하려면 AndroidManifest.xml에 meta-data를 추가하여 디버거를 false로 설정합니다.
마무리
전반적으로 앱 디자인에 맞게 광고를 적용할 수 있다는 점에서 네이티브 광고는 아주 좋은 것 같습니다.
Compose와 완벽히 호환되지는 않는 것 같지만, 충분히 xml Layout과 함께 Compose에서 사용할 수 있을 것 같습니다.
지금은 앱의 Home 화면의 하단에 고정적으로 위치하도록 하였지만, Lazy Column과 함께 활용하여 알람 아이템의 중간중간에 네이티브 광고를 위치하도록 하는 것도 고려해 볼 수 있을 것 같습니다.
추가 참고자료
https://developers.google.com/admob/android/native?hl=ko
https://admob.google.com/home/resources/native-ads-playbook/?hl=ko
Google AdMob 네이티브 광고 플레이북 | Google AdMob
네이티브 광고를 사용하면 앱에 표시되는 광고를 맞춤설정할 수 있습니다. 권장사항을 알아보고 네이티브 광고를 앱에 사용해야 하는 이유를 확인해 보세요.
admob.google.com
googleads-mobile-android-examples/kotlin/advanced/JetpackComposeDemo at main · googleads/googleads-mobile-android-examples
googleads-mobile-android. Contribute to googleads/googleads-mobile-android-examples development by creating an account on GitHub.
github.com
https://jaeryo2357.tistory.com/103
[Android] Paging3 + Admob Native Ad
안녕하세요 점냥입니다:) 좋은 앱 서비스를 완성했다면 그다음 고민은 수익 모델이죠. AWS 등 클라우드 서비스를 통해 서버를 구축했다면 다달이 나오는 서버 비용을 무시할 수 없기 때문이에요.
jaeryo2357.tistory.com