본문 바로가기

[애드몹] 2025년 다시 정리해보는 내 앱에 애드몹(admob) 광고 붙이기

ironwhale 2025. 3. 11.

1인 개발을 하고 있는 저에게 앱의 수익모델은 아직은 광고, 즉 애드몹을 제가 만든 어플 곳곳에 설치하여 얻는 수익이 전부입니다. 지금 10개 미만의 앱을 출시 중이고 애드몹 수익은 4년동안 아직 2번 출금했을 정도로 미미한 상태입니다.

그러던 중 현재 운영하고 있는 앱 중 노출수가 200건도 안되는 앱이 노출수 9,000이 넘는 앱의 광고 수익을 넘어서는 이상한 현상을 발견하고 원인을 생각하던 중 애드몹 정책 위반으로 광고가 정지되고 커스텀하게 구현한 네이티브광고에서 템플릿을 이용한 광고 형태로 변경한것 때문이지 않을까 싶어 현재 실험 중이니 나중에 결과가 나오면 공유하도록 하겠습니다. 

네이티브 광고 템플릿 버전으로 전환

 제가 바꾼 광고 형태는 네이티브 광고인것은 동일하나 이전에는 일일히 커스텀하게 광고 UI를 만드는 방식이었다면 이번 포스팅에서는 그냥 플러터에 google_mobile_ads에서 제공하는 네이티브 광고 템플릿을 이용해서 구현하는 방법을 알아보도록 하겠습니다. 

애드몹_네이티브_플러터_flutter
플러터로 애드몹 광고 보여주기

패키지 설치

애드몹 광고를 구현하기 위해서는 패키지를 설치해야합니다. 필요한 패키지는 google_mobile_ads 입니다. 구글 광고를 하기 위해서는 이 패키지 설치가 필수 입니다. 

google_mobile_ads: ^5.3.1

main 함수에서 초기화

애드몹 광고를 앱에 구현하기 위해서는 main함수에서 초기화를 해주어야 합니다. main함수 내부에   MobileAds.instance.initialize();를 입력하시면 됩니다.

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  MobileAds.instance.initialize();
 }

광고 아이디 설정

애드몹 광고에는 앱의 자체 아이디와 광고별 아이디가 존재합니다. 앱의 자체 아이디는 애드몹 가입 후 애드몹 사이트에서 앱을 등록하면 하나 생성이 되고 광고 별 아이디는 광고를 생성할때마다 아이디를 생성할 수 있습니다. 저 같은 경우 광고는 한개만 생성해서 사용하고 있지만 여러개 만들어서 각 페이지별로 구현한다면 어떤 페이지에서 광고 수익이 잘 나오는지 확인하기 쉬울것 같습니다.  

앱의 아이디 입력

앱의 애드몹 광고 아이디는 android/app/src/main/AndroidManifest.xml에 <application ~</application> 사이에 아이디를 입력하는데 ca-app-pub-3940256099942544~3347511713 이것은 일단 샘플로 주는 아이디이니 배포하실때는 실제 아이디를 입력하셔야 합니다. 

<!-- AndroidManifest.xml
Sample AdMob app ID: ca-app-pub-3940256099942544~3347511713 -->
<meta-data
    android:name="com.google.android.gms.ads.APPLICATION_ID"
    android:value="ca-app-pub-3940256099942544~3347511713"/>

 

각 광고별 아이디 입력

kReleaseMode를 사용하여 디버그 모드인지 배포 모드인지 자동으로 감지하여 자동으로 애드몹 아이디가 입력되도록 합니다. 

const Map<String, String> REWARD_UNIT_ID =
    kReleaseMode
        ? {
          'ios': '[YOUR iOS AD UNIT ID]',
          'android': '[YOUR Android AD UNIT ID]',
        }
        : {
          'ios': 'ca-app-pub-3940256099942544/1712485313',
          'android': 'ca-app-pub-3940256099942544/5224354917',
        };

애드몹 광고 위젯 만들기 

이제 본격적으로 애드몹 광고 중 하나인 네이티브 광고를 보여주기 위한 별도의 StatefulWidget을 만들어보도록 하겠습니다. 저는 리워드 광고를 보면 전체 광고가 안보이도록 하기위해서 리버팟의 ConsumerStatefulWidget을 이용해서 만들어 보도록 하겠습니다. 

광고 상태구분 

광고는 바로 나오는 것이 아니라 인터넷이 연결된 상태에서 요청을 보내고 받는 과정이 필요하여 그래서 로딩하는 상태, 로드된 상태, 광고 로드가 실패한 상태로 구분 됩니다. 저는 간단하게 enum으로 표현하였습니다. 

enum NativeAdState { load, fail, loading }

외부에서 템플릿 타입 받기

네이브티브 광고를 템플릿으로 구현하면 스몰 타입과 미디엄타입 두 종류로 만들 수 있습니다.  저는 일단 small 타입을 기본형으로 지정하였고 이 small 타입은 우리가 흔히 하는 배너 타입의 네이티브 광고이고 미디엄 타입은 정상각형의 화면의 절반정도를 차지하는 큰 광고로 생각하시면 됩니다.

class NativeAdsByTemplate extends ConsumerStatefulWidget {
  const NativeAdsByTemplate(
      {super.key, this.templateType = TemplateType.small});

  final TemplateType templateType;

  @override
  ConsumerState createState() => _NativeAdsByTemplateState();
}

기본 필드 구성

기본적으로 광고객체를 담을 _ad, 광고 아이디, 광고 상태, 재시도 한 횟수, 재시도 할 횟수, 그리고 광고가 담길 컨테이너 사이즈를 제약 할 부분이 필요합니다.

class _NativeAdsByTemplateState extends ConsumerState<NativeAdsByTemplate> {
  NativeAd? _ad;
  final String _adUnitId = NATIVE_ID[Platform.isIOS ? "ios" : "android"]!;
  NativeAdState status = NativeAdState.loading;
  int retryAttempt = 0;
  final int maxRetry = 3;
  late final BoxConstraints constraints;

광고 로드

initState에서 광고를 로드 할 수 있도록 별도의 함수를 만들어서 광고를 로드합니다. 여기서 ref.read(rewardStateProvider)은 리워드가 적용된 상태면 광고 로드가 되지 않도록 하는 부분이니 필요하신 분들만 사용하시면 됩니다. 

광고를 로드하기 위해서는 NativeAd(...).load(); 를 실행하면 광고가 로드됩니다. 그리고 여러 파라미터 중 NativeAdListener는 네이티브 광고가 로드되었을때, 실패했을때, 클릭했을때 등 각 상황에 맞는 상태가 되면 콜백함수가 실행됩니다. 

onAdLoaded

nAdLoaded은 로드가 완료되면 실행되는 콜백함수로 재시도 횟수를 초기화해주고 광고가 로드된 상태로 바꾸어주고 로드된 광고를 _ad에 입력해주고 위젯을 리프레시하기 위해서 setState를 해줘서 광고가 화면에 보이도록 합니다. 

onAdFailedToLoad

onAdFailedToLoad은 광고 로드가 실패했을 때 실행되는 부분으로 왜하는지는 모르겠지만 공식 샘플과 같이 ad.dispose() 해주고 광고 상태를 실패 상태로 바꾼뒤 재시도 횟수가 남았으면 재시도를 해줍니다. 

  void _loadAd() {
    final state = ref.read(rewardStateProvider);
    if (state is GetReward) {
      return;
    }

    NativeAd(
      adUnitId: _adUnitId,
      request: const AdRequest(),
      nativeTemplateStyle: NativeTemplateStyle(
          templateType: widget.templateType, cornerRadius: 8),
      listener: NativeAdListener(onAdLoaded: (ad) {
        setState(() {
          retryAttempt = 0;
          status = NativeAdState.load;
          _ad = ad;
        });
      }, onAdFailedToLoad: (ad, error) {
        ad.dispose();
        status = NativeAdState.fail;
        if (retryAttempt < maxRetry) {
          retryAttempt++;
          Future.delayed(Duration(seconds: retryAttempt), _loadAd);
        }
      }),
    ).load();
  }

initState에서 광고 크기 설정

처음에는 광고를 로드하는 함수를 실행하고 네이티브 광고의 크기를 정하기 위한 BoxConstraints를 지정해 줍니다. 아래 컨터네이너 제약조건은 해당 사이즈는 구글의 공식 예제를 참고해서 만든 제약 조건으로 small일 경우 구글에서는 maxHeight를 120을 했는데 좀 큰 것 같아 90으로 줄였습니다. 

  @override
  void initState() {
    _loadAd();
    constraints = widget.templateType == TemplateType.medium
        ? const BoxConstraints(
            maxWidth: double.infinity,
            maxHeight: 400,
            minWidth: 320,
            minHeight: 320)
        : const BoxConstraints(
            maxWidth: double.infinity,
            maxHeight: 90,
            minHeight: 45,
            minWidth: double.infinity);

    super.initState();
  }

광고 송출하는 build() 함수

빌드 함수에서는 컨테이너 내부에 조건에 따라 광고를 내보내거나 광고가 로딩 중이라면 원하는 문구를 내보낼 수도 있습니다. ref.watch(rewardStateProvider);` 를 통해 리워드가 있다면 그냥 빈 컨테이너를 리턴하여 광고가 안보이게 할수도 있어 일일히 각 화면 별로 조건에 따라 광고를 안보이게 하는 반복적인 코드를 한개로 줄일 수 있었습니다. 

랜덤으로 메시지 보여주기

랜덤으로 메시지로 보여주기 위해서는 final random = Random();를이용해서 messages[random.nextInt(messages.length)]을 사용하면 랜덤으로 광고 로딩 중에 메세지를 보여줄수 있습니다. 더는 여기에 앱의 기능을 소개하는 문구를 주로 넣습니다.

final random = Random();

  @override
  Widget build(BuildContext context) {
    final rewardState = ref.watch(rewardStateProvider);

    if (rewardState is GetReward) {
      return Container();
    }

    return Container(
      constraints: constraints,
      child: status == NativeAdState.load
          ? AdWidget(ad: _ad)
          : status == NativeAdState.loading
              ? Center(
                  child: Text(
                    messages[random.nextInt(messages.length)],
                    style: const TextStyle(fontSize: 16),
                  ),
                )
              : const SizedBox.shrink(),
    );
  }
}

final List<String> messages = [
  '로딩 중입니다.',
  '리워드 광고를 보시면 광고가 일시적으로 없어집니다.'
];

전체 코드

어느 정도 애드몹 구현을 해보신 분이라면 전체코드를 먼저 보시고 이해되지 않는 부분만 보시는 걸 추전드립니다.

enum NativeAdState { load, fail, loading }

class NativeAdsByTemplate extends ConsumerStatefulWidget {
  const NativeAdsByTemplate(
      {super.key, this.templateType = TemplateType.small});

  final TemplateType templateType;

  @override
  ConsumerState createState() => _NativeAdsByTemplateState();
}

class _NativeAdsByTemplateState extends ConsumerState<NativeAdsByTemplate> {
  NativeAd? _ad;
  final String _adUnitId = NATIVE_ID[Platform.isIOS ? "ios" : "android"]!;
  NativeAdState status = NativeAdState.loading;
  int retryAttempt = 0;
  final int maxRetry = 3;

  late final BoxConstraints constraints;

  void _loadAd() {
    final state = ref.read(rewardStateProvider);
    if (state is GetReward) {
      return;
    }

    NativeAd(
      adUnitId: _adUnitId,
      request: const AdRequest(),
      nativeTemplateStyle: NativeTemplateStyle(
          templateType: widget.templateType, cornerRadius: 8),
      listener: NativeAdListener(onAdLoaded: (ad) {
        setState(() {
          retryAttempt = 0;
          status = NativeAdState.load;
          _ad = ad;
        });
      }, onAdFailedToLoad: (ad, error) {
        ad.dispose();
        status = NativeAdState.fail;
        if (retryAttempt < maxRetry) {
          retryAttempt++;
          Future.delayed(Duration(seconds: retryAttempt), _loadAd);
        }
      }),
    ).load();
  }

  @override
  void initState() {
    _loadAd();
    constraints = widget.templateType == TemplateType.medium
        ? const BoxConstraints(
            maxWidth: double.infinity,
            maxHeight: 400,
            minWidth: 320,
            minHeight: 320)
        : const BoxConstraints(
            maxWidth: double.infinity,
            maxHeight: 90,
            minHeight: 45,
            minWidth: double.infinity);

    super.initState();
  }

  @override
  void dispose() {
    _ad.dispose();
    super.dispose();
  }

  final random = Random();

  @override
  Widget build(BuildContext context) {
    final rewardState = ref.watch(rewardStateProvider);

    if (rewardState is GetReward) {
      return Container();
    }

    return Container(
      constraints: constraints,
      child: status == NativeAdState.load
          ? AdWidget(ad: _ad)
          : status == NativeAdState.loading
              ? Center(
                  child: Text(
                    messages[random.nextInt(messages.length)],
                    style: const TextStyle(fontSize: 16),
                  ),
                )
              : const SizedBox.shrink(),
    );
  }
}

final List<String> messages = [
  '로딩 중입니다.',
  '리워드 광고를 보시면 광고가 일시적으로 없어집니다.'
];

마치며

애드몹관련 포스팅을 굉장히 여러번 한거 같습니다. 플러터와 파이어베이스가 업데이트 되면서 파이어베이스 초기 세팅 방법은 과거 플러터 자료와 달라진적이 있었지만 애드몹 세팅 방법은 이전과 동일한거 같습니다. 다만 조건에 따라 광고를 보여주지 않는 부분이나 로드 함수가 실행되지 않도록 하는 세세한 로직 부분은 이전 포스팅과 다르게 적용하여 저의 앱도 한단계 업그레이드를 할 수 있었던 계기가 되었던거 같습니다. 

네이티브광고를 템플릿 방식 적용 전후 수익금의 차이는 어느 정도 데이터가 쌓인뒤 포스팅 해보도록 하겠습니다.  

댓글