모바일 앱 수익화: 리워드 광고와 AdMob 전략

모바일 개발

AdMob리워드 광고앱 수익화iOSAndroid

이 글은 누구를 위한 것인가

  • 광고 수익을 추가하고 싶지만 UX 저하가 걱정인 팀
  • AdMob 리워드 광고와 IAP를 함께 운용하려는 개발자
  • 광고 eCPM을 높이기 위한 최적화 전략이 필요한 팀

들어가며

광고는 무료 앱의 핵심 수익원이지만, 잘못된 배치는 사용자를 이탈시킨다. 리워드 광고는 사용자가 자발적으로 시청하고 보상을 받는 구조라 거부감이 가장 낮다.

이 글은 bluefoxdev.kr의 앱 수익화 전략 가이드 를 참고하여 작성했습니다.


1. 광고 유형별 특성과 배치 전략

[광고 유형 비교]

배너 광고:
  위치: 화면 상단/하단 고정
  eCPM: 낮음 ($0.3–1.5)
  UX 영향: 레이아웃 밀림
  적합: 유틸리티 앱, 지속 노출

전면 광고 (Interstitial):
  위치: 화면 전환 시 전체 화면
  eCPM: 중간 ($2–8)
  UX 영향: 높음 (강제 노출)
  적합: 게임 레벨 사이, 콘텐츠 소비 후
  주의: 너무 자주 보여주면 이탈율 급증

리워드 광고 (Rewarded):
  위치: 사용자 요청 시
  eCPM: 높음 ($10–30)
  UX 영향: 낮음 (자발적)
  적합: 추가 생명, 프리미엄 콘텐츠 해금, 힌트

리워드 전면 광고:
  전면 + 리워드 혼합
  eCPM: 높음
  완료 시 보상 제공

[배치 전략]
  콘텐츠 소비 → 전면 (2–3번에 1번)
  게임 오버 → 리워드 (계속하기 옵션)
  프리미엄 기능 → 리워드 (임시 해금)
  로딩 중 → 절대 금지

[미디에이션]
  AdMob + Meta Audience Network + Unity Ads 동시 운용
  eCPM 높은 광고 우선 노출
  Fill Rate 상승 → 수익 20-40% 증가

2. AdMob 리워드 광고 구현

import GoogleMobileAds
import SwiftUI

class AdManager: NSObject, ObservableObject {
    @Published var isRewardedAdReady = false
    @Published var rewardEarned = false
    
    private var rewardedAd: GADRewardedAd?
    private let adUnitID = "ca-app-pub-3940256099942544/1712485313"  // 테스트 ID
    
    override init() {
        super.init()
        GADMobileAds.sharedInstance().start()
        loadRewardedAd()
    }
    
    func loadRewardedAd() {
        let request = GADRequest()
        GADRewardedAd.load(withAdUnitID: adUnitID, request: request) { [weak self] ad, error in
            DispatchQueue.main.async {
                if let error = error {
                    print("리워드 광고 로드 실패: \(error)")
                    self?.isRewardedAdReady = false
                    // 30초 후 재시도
                    DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
                        self?.loadRewardedAd()
                    }
                    return
                }
                self?.rewardedAd = ad
                self?.rewardedAd?.fullScreenContentDelegate = self
                self?.isRewardedAdReady = true
            }
        }
    }
    
    func showRewardedAd(from viewController: UIViewController, onReward: @escaping (GADAdReward) -> Void) {
        guard let ad = rewardedAd else {
            print("광고 준비 안됨")
            return
        }
        
        ad.present(fromRootViewController: viewController) { [weak self] in
            let reward = ad.adReward
            onReward(reward)
            
            // 리워드 획득 후 다음 광고 미리 로드
            self?.loadRewardedAd()
        }
        
        isRewardedAdReady = false
    }
}

extension AdManager: GADFullScreenContentDelegate {
    func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
        loadRewardedAd()  // 닫힌 후 다음 광고 로드
    }
    
    func ad(_ ad: GADFullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
        print("광고 표시 실패: \(error)")
        loadRewardedAd()
    }
}

// 리워드 광고를 통한 힌트 제공 뷰
struct HintView: View {
    @EnvironmentObject var adManager: AdManager
    @State private var hintsRemaining = 3
    @State private var showingAd = false
    @State private var earnedCoins = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("남은 힌트: \(hintsRemaining)")
                .font(.headline)
            
            Text("획득 코인: \(earnedCoins)")
                .font(.subheadline)
                .foregroundColor(.yellow)
            
            if hintsRemaining > 0 {
                Button("힌트 사용") {
                    hintsRemaining -= 1
                }
                .buttonStyle(.borderedProminent)
            } else {
                // 힌트 소진 시 광고 시청 제안
                VStack(spacing: 12) {
                    Text("힌트를 모두 사용했습니다")
                        .foregroundColor(.secondary)
                    
                    if adManager.isRewardedAdReady {
                        Button {
                            showAd()
                        } label: {
                            Label("광고 보고 힌트 3개 받기", systemImage: "play.rectangle.fill")
                                .padding(.horizontal, 20)
                                .padding(.vertical, 12)
                                .background(Color.green)
                                .foregroundColor(.white)
                                .cornerRadius(10)
                        }
                    } else {
                        ProgressView("광고 로드 중...")
                    }
                    
                    // IAP 대안 제공
                    Button("100코인으로 힌트 구매") {
                        purchaseHints()
                    }
                    .foregroundColor(.blue)
                }
            }
        }
    }
    
    private func showAd() {
        guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
              let vc = scene.windows.first?.rootViewController else { return }
        
        adManager.showRewardedAd(from: vc) { reward in
            DispatchQueue.main.async {
                hintsRemaining += 3
                earnedCoins += Int(reward.amount)
            }
        }
    }
    
    private func purchaseHints() {
        // StoreKit 2 구매 플로우
    }
}

// 배너 광고 SwiftUI 래퍼
struct BannerAdView: UIViewRepresentable {
    let adUnitID: String
    
    func makeUIView(context: Context) -> GADBannerView {
        let banner = GADBannerView(adSize: GADAdSizeBanner)
        banner.adUnitID = adUnitID
        
        if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
           let vc = scene.windows.first?.rootViewController {
            banner.rootViewController = vc
        }
        
        banner.load(GADRequest())
        return banner
    }
    
    func updateUIView(_ uiView: GADBannerView, context: Context) {}
}

// 전면 광고 관리
class InterstitialAdManager: NSObject {
    private var interstitialAd: GADInterstitialAd?
    private var showCount = 0
    private let showInterval = 3  // 3번에 1번
    
    func loadAd() {
        GADInterstitialAd.load(
            withAdUnitID: "ca-app-pub-3940256099942544/4411468910",
            request: GADRequest()
        ) { [weak self] ad, _ in
            self?.interstitialAd = ad
        }
    }
    
    func tryShowAd(from vc: UIViewController) {
        showCount += 1
        guard showCount % showInterval == 0,
              let ad = interstitialAd else { return }
        
        ad.present(fromRootViewController: vc)
        interstitialAd = nil
        loadAd()
    }
}
// Android - AdMob 리워드 광고
import com.google.android.gms.ads.*
import com.google.android.gms.ads.rewarded.RewardedAd
import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback

class AdViewModel : ViewModel() {
    private var rewardedAd: RewardedAd? = null
    
    val isAdReady = MutableStateFlow(false)
    val rewardCoins = MutableStateFlow(0)
    
    fun loadRewardedAd(context: Context) {
        val adRequest = AdRequest.Builder().build()
        
        RewardedAd.load(
            context,
            "ca-app-pub-3940256099942544/5224354917",
            adRequest,
            object : RewardedAdLoadCallback() {
                override fun onAdLoaded(ad: RewardedAd) {
                    rewardedAd = ad
                    isAdReady.value = true
                }
                
                override fun onAdFailedToLoad(error: LoadAdError) {
                    rewardedAd = null
                    isAdReady.value = false
                    // 30초 후 재시도
                    viewModelScope.launch {
                        delay(30_000)
                        loadRewardedAd(context)
                    }
                }
            }
        )
    }
    
    fun showRewardedAd(activity: Activity, onReward: (Int) -> Unit) {
        rewardedAd?.let { ad ->
            ad.fullScreenContentCallback = object : FullScreenContentCallback() {
                override fun onAdDismissedFullScreenContent() {
                    rewardedAd = null
                    isAdReady.value = false
                    loadRewardedAd(activity)
                }
            }
            
            ad.show(activity) { rewardItem ->
                val coins = rewardItem.amount
                rewardCoins.value += coins
                onReward(coins)
            }
        }
    }
}

마무리

리워드 광고는 사용자가 통제권을 갖는 유일한 광고 형태다. 핵심은 보상의 가치를 명확히 하고, 광고 준비 상태를 미리 표시해 사용자 기대를 관리하는 것이다. IAP와 리워드 광고를 병행하면 프리미엄 사용자는 구매, 일반 사용자는 광고로 수익을 이중화할 수 있다. eCPM 최적화는 미디에이션으로 여러 네트워크를 경쟁시키는 것이 가장 효과적이다.