Rose
Rose
🎻 루키 파트너
🚀 SNS 챌린지 달성자

안드로이드 앱에 전면 광고 넣기

소개

React Native로 만든 Android 앱에 Admob 전면 광고를 넣었습니다. 앱 사용 시간이 1시간 누적될 때마다 한 번씩 광고를 노출합니다.

진행 방법

먼저, Admob 전면 광고를 넣는 방법을 딥 리서치로 조사해서 md 파일로 만들었습니다.

# **React Native CLI 환경에서의 Google AdMob 전면 광고 통합 및 시간 기반 노출 로직 구현에 관한 종합 기술 보고서**

## **1\. 서론 (Introduction)**

모바일 애플리케이션 수익화 전략에서 인앱 광고(In-App Advertising, IAA)는 가장 보편적이고 효과적인 모델 중 하나이다. 특히 전면 광고(Interstitial Ads)는 화면 전체를 활용하여 높은 시인성과 클릭률(CTR)을 제공하므로, 배너 광고 대비 높은 eCPM(1,000회 노출당 유효 비용)을 기대할 수 있다. 본 보고서는 React Native CLI 환경으로 개발된 Android 애플리케이션에 Google AdMob 전면 광고를 탑재하고, 단순한 시각적 빈도 제어가 아닌 '누적 앱 사용 시간 1시간'이라는 정교한 조건에 따라 광고를 노출하는 기술적 구현 방안을 심층적으로 다룬다.

특히, 네이티브 코드(Java/Kotlin) 개발 경험이 없는 개발자를 위해 React Native 생태계 내의 라이브러리와 설정 파일 수정만으로 기능을 구현하는 'No-Code Native' 접근 방식을 채택한다. 또한, Play Store 배포 전 단계에서의 테스트 전략, 대한민국 30\~40대 타겟 사용자층을 고려한 브랜드 안전성(Brand Safety) 설정, 그리고 강화된 개인정보 보호 규정에 따른 사용자 동의(Consent) 관리까지 포괄적인 가이드를 제시한다.

본 문서는 단순한 코드 스니펫의 나열을 넘어, AdMob의 작동 원리, 안드로이드 빌드 시스템의 구성, 그리고 광고 네트워크의 정책적 제약을 분석하여 안정적이고 지속 가능한 수익화 모델을 구축하는 데 필요한 모든 기술적, 관리적 통찰을 제공하는 것을 목적으로 한다.

## ---

**2\. 개발 환경 및 아키텍처 분석 (Environment & Architecture)**

### **2.1 라이브러리 선정 및 생태계 현황**

React Native 생태계에서 AdMob을 연동하기 위한 라이브러리는 역사적으로 다양하게 존재해왔으나, 2025년 현재 가장 안정적이고 Google의 최신 SDK 기능을 지원하는 표준 라이브러리는 \*\*react-native-google-mobile-ads\*\*이다. 과거 react-native-firebase의 일부로 제공되던 AdMob 모듈이 분리되어 독립적으로 유지보수되고 있으며, React Native의 New Architecture(Fabric/TurboModules)와의 호환성을 보장한다.

| 라이브러리명 | 특징 및 선정 이유 |
| :---- | :---- |
| **react-native-google-mobile-ads** | **\[권장\]** Google Mobile Ads SDK의 최신 기능을 1:1로 매핑하며, UMP(User Messaging Platform) 등 최신 개인정보 보호 API를 기본 지원함. 1 |
| expo-ads-admob | Expo 관리 워크플로우에 최적화되어 있으나, CLI 환경에서는 설정의 유연성이 떨어지고 업데이트 주기가 상대적으로 늦을 수 있음. 3 |
| react-native-admob (Legacy) | 더 이상 유지보수되지 않는 경우가 많아 최신 Android API 레벨 및 AdMob 정책 변화에 대응하기 어려움. |

### **2.2 안드로이드 빌드 아키텍처와 네이티브 설정**

사용자는 네이티브 코드 경험이 없음을 명시하였으나, AdMob SDK는 본질적으로 Android 네이티브 라이브러리(.aar)이다. 따라서 React Native 브리지(Bridge)가 이 네이티브 코드와 통신하기 위해서는 최소한의 빌드 설정(Configuration)이 필요하다. 이는 Java나 Kotlin 코드를 작성하는 것이 아니라, 프로젝트의 구성 파일(build.gradle, AndroidManifest.xml)을 수정하여 빌드 도구가 SDK를 올바르게 로드하도록 지시하는 과정이다.

특히, AdMob SDK와 같이 거대한 라이브러리를 포함할 경우 안드로이드 앱의 메소드 참조 개수가 65,536개를 초과하는 '64k 참조 제한' 문제에 직면할 가능성이 높다. 이를 해결하기 위해 **Multidex** 설정이 필수적이다.

## ---

**3\. AdMob 계정 설정 및 정책적 준비 (AdMob Setup & Policy)**

기술적 구현에 앞서 AdMob 플랫폼 상에서의 설정이 선행되어야 한다. 특히 Play Store 배포 전 테스트 가능 여부와 타겟 오디언스 설정은 이 단계에서 결정된다.

### **3.1 애플리케이션 등록 및 '앱 준비(App Readiness)' 상태**

AdMob은 트래픽 품질 관리를 위해 새로 등록된 앱에 대해 검토 과정을 거친다.

* **게시된 앱(Published):** 스토어에 이미 배포된 경우, 스토어 링크를 통해 즉시 검토가 시작된다.  
* **게시되지 않은 앱(Unpublished):** 배포 전 단계에서는 '게시되지 않은 앱'으로 등록하여 App ID와 광고 단위 ID(Ad Unit ID)를 발급받을 수 있다. 4

**중요한 제약 사항:** 게시되지 않은 앱 상태에서는 광고 게재가 제한(Limited Ad Serving)될 수 있다. 이는 실제 광고 요청을 보내도 '채워짐(Fill)'이 발생하지 않고 오류 코드 3(ERROR\_CODE\_NO\_FILL)이 반환될 수 있음을 의미한다. 따라서 개발 및 테스트 단계에서는 반드시 **테스트 광고 단위 ID**를 사용하거나, **테스트 기기(Test Device)** 등록 절차를 거쳐야 한다. 6

### **3.2 한국 30-40대 타겟 및 민감한 카테고리 차단**

대한민국 30\~40대 사용자는 구매력이 높으나 앱의 품질과 사용자 경험(UX)에 민감하다. 불쾌감을 줄 수 있는 광고는 앱 삭제의 직접적인 원인이 되므로, 브랜드 안전성(Brand Safety) 관리가 필수적이다.

**민감한 카테고리 차단 전략:**

AdMob 콘솔의 차단 관리(Blocking Controls) \> 민감한 카테고리(Sensitive Categories) 섹션에서 다음 항목들을 차단할 것을 권장한다.

| 카테고리 (한국어/영어) | 차단 권장 사유 | 관련 데이터 |
| :---- | :---- | :---- |
| **성적인 내용 (References to Sex)** | 성인 광고 차단 요청 준수 및 앱의 전문성 유지 | 8 |
| **상당한 피부 노출 (Significant Skin Exposure)** | 시각적 불쾌감 방지. 30-40대 직장인 타겟 시 부적절 | 8 |
| **소비자 대출 (Consumer Loans)** | **\[한국 특화\]** 한국 시장에서 대부업 광고는 부정적 인식이 강하며 스팸으로 인식될 소지가 높음 | 8 |
| **자극적인 내용 (Sensationalism)** | 클릭 유도형(Click-bait) 광고로 인한 앱 신뢰도 하락 방지 | 8 |
| **도박 및 베팅 (Gambling & Betting)** | 사행성 조장 우려 및 성인 인증 이슈 회피 | 9 |

이러한 설정은 코드가 아닌 AdMob 서버단에서 처리되므로, 앱 업데이트 없이 즉시 적용된다.

## ---

**4\. 단계별 구현 가이드: 환경 설정 (Implementation Phase 1: Configuration)**

이 섹션에서는 네이티브 코드를 작성하지 않고 구성 파일 수정만으로 AdMob을 연동하는 절차를 상세히 기술한다.

### **4.1 패키지 설치**

프로젝트 루트 디렉토리에서 다음 명령어를 실행하여 라이브러리를 설치한다.

Bash

npm install react-native-google-mobile-ads  
\# 또는  
yarn add react-native-google-mobile-ads

### **4.2 Android Manifest 설정 (필수)**

AdMob App ID는 런타임이 아닌 빌드 타임에 앱의 메타데이터로 포함되어야 한다. 이를 누락할 경우 앱 실행 즉시 크래시(Crash)가 발생한다.

* **파일 위치:** android/app/src/main/AndroidManifest.xml  
* **수정 내용:** \<application\> 태그 내부에 \<meta-data\> 요소를 추가한다.

XML

\<manifest xmlns:android\="http://schemas.android.com/apk/res/android" package\="com.example.yourapp"\>  
    \<application  
      android:name\=".MainApplication"  
      android:label\="@string/app\_name"  
      android:icon\="@mipmap/ic\_launcher"  
      android:theme\="@style/AppTheme"\>

      \<meta-data  
        android:name\="com.google.android.gms.ads.APPLICATION\_ID"  
        android:value\="ca-app-pub-3940256099942544\~3347511713"/\>

    \</application\>  
\</manifest\>

**주의:** 위 예시에 사용된 ID(ca-app-pub-3940256099942544\~3347511713)는 구글이 제공하는 테스트용 샘플 App ID이다. 실제 AdMob 대시보드에서 생성한 App ID를 넣고 테스트할 경우, 트래픽 품질 이슈가 발생할 수 있으므로 초기 설정 확인 단계에서는 샘플 ID 사용을 권장한다. 10

### **4.3 Multidex 설정 및 빌드 구성**

AdMob SDK의 방대한 메소드 수로 인한 빌드 에러를 방지하기 위해 Multidex를 활성화한다.

* **파일 위치:** android/app/build.gradle (프로젝트 루트의 android/build.gradle이 아님에 유의)

Groovy

android {  
    defaultConfig {  
        //... 기존 설정...  
        minSdkVersion 21 // React Native 최신 버전은 보통 21 이상 요구  
        targetSdkVersion 34 // 최신 안드로이드 타겟 버전

        // \[중요\] Multidex 활성화  
        multiDexEnabled true   
    }  
    //...  
}

dependencies {  
    implementation("com.facebook.react:react-android")  
      
    // \[중요\] Multidex 라이브러리 의존성 추가  
    implementation 'androidx.multidex:multidex:2.0.1'  
      
    // 참고: react-native-google-mobile-ads 라이브러리가 자동으로   
    // Play Services Ads 의존성을 관리하므로 별도 추가 불필요  
}

안드로이드 5.0 (API 레벨 21\) 이상을 타겟팅하는 경우(minSdkVersion 21), 추가적인 Java 코드 수정 없이 build.gradle 설정만으로 Multidex가 기본 지원된다. 따라서 MainApplication.java를 수정하여 MultiDexApplication을 상속받게 하는 복잡한 과정은 불필요하다. 12

## ---

**5\. 단계별 구현 가이드: 논리 엔진 (Implementation Phase 2: Logic Engine)**

사용자의 요구사항인 \*\*"앱 사용 1시간마다 노출"\*\*은 AdMob 콘솔에서 제공하는 기본 '빈도 설정(Frequency Capping)'으로는 구현이 불가능하다. 콘솔의 빈도 설정은 **절대 시간(Wall-clock time)** 기준이기 때문이다. 예를 들어, 사용자가 1시에 앱을 켜고 5분만 사용한 뒤 종료하고 2시에 다시 켜면, 콘솔 기준으로는 1시간이 지났으므로 광고가 노출된다. 하지만 사용자의 요구사항은 \*\*앱을 실제로 켜놓고 사용한 누적 시간(Cumulative Foreground Time)\*\*이 1시간이 되었을 때 노출하는 것이다.

이를 위해 React Native의 AppState API와 AsyncStorage를 결합한 커스텀 Hook을 구현해야 한다.

### **5.1 기술적 난제와 해결 방안**

* **난제:** 앱이 백그라운드로 내려가거나(Background), 완전히 종료(Kill)되면 JavaScript 타이머(setInterval)는 멈추거나 초기화된다.  
* **해결 방안:**  
  1. 앱이 Active(포그라운드) 상태일 때만 시간을 측정한다.  
  2. 앱 상태가 변경될 때마다 현재까지의 누적 시간을 로컬 저장소(AsyncStorage)에 저장한다.  
  3. 앱이 재시작되면 저장소에서 이전 누적 시간을 불러와 이어서 측정한다.

### **5.2 누적 사용 시간 추적 Hook 구현 (useUsageTimer)**

이 로직은 순수 TypeScript/JavaScript로 구현되며, 어떠한 네이티브 코드도 필요하지 않다.

**사전 준비:** AsyncStorage 라이브러리 설치

Bash

npm install @react-native-async-storage/async-storage

**코드 구현 (src/hooks/useUsageTimer.ts):**

TypeScript

import { useState, useEffect, useRef } from 'react';  
import { AppState, AppStateStatus } from 'react-native';  
import AsyncStorage from '@react-native-async-storage/async-storage';

const STORAGE\_KEY \= 'cumulative\_usage\_time\_ms';  
const TARGET\_INTERVAL\_MS \= 60 \* 60 \* 1000; // 1시간 (3600초 \* 1000\)

export const useUsageTimer \= () \=\> {  
  const \= useState(false);  
  const appState \= useRef\<AppStateStatus\>(AppState.currentState);  
  const startTime \= useRef\<number | null\>(null);  
  const accumulatedTime \= useRef\<number\>(0);

  // 저장된 시간 불러오기  
  useEffect(() \=\> {  
    const loadTime \= async () \=\> {  
      try {  
        const savedTime \= await AsyncStorage.getItem(STORAGE\_KEY);  
        if (savedTime) {  
          accumulatedTime.current \= parseInt(savedTime, 10);  
        }  
        // 앱이 시작되면서 타이머 시작 (현재 포그라운드라면)  
        if (AppState.currentState \=== 'active') {  
          startTime.current \= Date.now();  
        }  
      } catch (error) {  
        console.error('Failed to load usage time', error);  
      }  
    };  
    loadTime();  
  },);

  // 상태 변경 감지 및 시간 저장 로직  
  useEffect(() \=\> {  
    const handleAppStateChange \= (nextAppState: AppStateStatus) \=\> {  
      const now \= Date.now();

      // Active \-\> Inactive/Background (사용 중단)  
      if (appState.current \=== 'active' && nextAppState.match(/inactive|background/)) {  
        if (startTime.current) {  
          const sessionDuration \= now \- startTime.current;  
          accumulatedTime.current \+= sessionDuration;  
          AsyncStorage.setItem(STORAGE\_KEY, accumulatedTime.current.toString());  
          startTime.current \= null; // 타이머 정지  
        }  
      }  
      // Inactive/Background \-\> Active (사용 재개)  
      else if (appState.current.match(/inactive|background/) && nextAppState \=== 'active') {  
        startTime.current \= now; // 타이머 재시작  
      }

      appState.current \= nextAppState;  
    };

    const subscription \= AppState.addEventListener('change', handleAppStateChange);

    // 주기적 체크 (1분마다): 앱을 끄지 않고 계속 켜두는 경우를 위해  
    const interval \= setInterval(() \=\> {  
      if (appState.current \=== 'active' && startTime.current) {  
        const currentSession \= Date.now() \- startTime.current;  
        const totalTime \= accumulatedTime.current \+ currentSession;

        if (totalTime \>= TARGET\_INTERVAL\_MS) {  
          setIsAdReady(true);  
        }  
      }  
    }, 60000); // 1분마다 검사

    return () \=\> {  
      subscription.remove();  
      clearInterval(interval);  
    };  
  },);

  // 광고 노출 후 호출하여 타이머 리셋  
  const resetTimer \= async () \=\> {  
    accumulatedTime.current \= 0;  
    startTime.current \= Date.now(); // 리셋 후 다시 측정 시작  
    setIsAdReady(false);  
    await AsyncStorage.setItem(STORAGE\_KEY, '0');  
  };

  return { isAdReady, resetTimer };  
};

**로직 설명:**

1. **세션 지속성:** AsyncStorage를 사용하여 앱을 완전히 종료했다가 며칠 뒤에 켜더라도 이전 사용 시간을 기억한다. 14  
2. **포그라운드 감지:** AppState 이벤트를 통해 사용자가 실제로 화면을 보고 있을 때만 시간을 누적한다. 15  
3. **실시간성:** 앱을 백그라운드로 보내지 않고 1시간 동안 계속 사용하는 헤비 유저를 위해 setInterval로 주기적으로 조건을 검사하여 isAdReady 상태를 업데이트한다.

## ---

**6\. 단계별 구현 가이드: 광고 표시 (Implementation Phase 3: Ad Display)**

이제 react-native-google-mobile-ads의 useInterstitialAd 훅과 위에서 만든 useUsageTimer를 결합하여 실제 광고를 노출한다.

### **6.1 SDK 초기화**

앱 진입점(예: App.tsx)에서 SDK를 초기화한다. 이는 광고 로드 속도를 최적화하고 UMP 동의 상태를 반영하기 위해 필수적이다.

TypeScript

import mobileAds from 'react-native-google-mobile-ads';

useEffect(() \=\> {  
  mobileAds()  
   .initialize()  
   .then(adapterStatuses \=\> {  
      console.log('Mobile Ads SDK Initialized');  
    });  
},);

### **6.2 전면 광고 통합 컴포넌트**

전면 광고는 화면 전환 시점(예: 게임 레벨 종료, 탭 이동 등)에 보여주는 것이 UX상 가장 자연스럽다. 1시간이 지났다고 해서 사용자가 글을 쓰고 있는 도중에 갑자기 광고를 띄우는 것은 정책 위반(Accidental Click 유발) 소지가 있다. 따라서 isAdReady 상태를 확인한 후, **"다음 화면 전환 시"** 또는 \*\*"특정 액션 완료 시"\*\*에 광고를 보여주는 것이 좋다. 10

TypeScript

import React, { useEffect } from 'react';  
import { Button, View, Text } from 'react-native';  
import { TestIds, useInterstitialAd } from 'react-native-google-mobile-ads';  
import { useUsageTimer } from './hooks/useUsageTimer';

// 개발 중에는 TestIds.INTERSTITIAL 사용, 배포 시 실제 ID 사용  
const AD\_UNIT\_ID \= \_\_DEV\_\_? TestIds.INTERSTITIAL : 'ca-app-pub-xxxxxxxx/xxxxxxxx';

const HomeScreen \= () \=\> {  
  const { isAdReady, resetTimer } \= useUsageTimer();  
  const { isLoaded, isClosed, load, show } \= useInterstitialAd(AD\_UNIT\_ID, {  
    requestNonPersonalizedAdsOnly: true, // 초기 개인정보 설정  
  });

  // 1\. 광고 미리 로드 (Preload)  
  useEffect(() \=\> {  
    load();  
  }, \[load\]);

  // 2\. 광고가 닫히면 타이머 리셋 및 다음 광고 로드  
  useEffect(() \=\> {  
    if (isClosed) {  
      resetTimer(); // 사용 시간 0으로 초기화  
      load(); // 다음 광고 미리 로드  
    }  
  },);

  // 3\. 사용자 액션 핸들러 (예: 다음 레벨로 이동)  
  const handleNextLevel \= () \=\> {  
    // 1시간이 지났고(isAdReady) \+ 광고가 로드되었다면(isLoaded)  
    if (isAdReady && isLoaded) {  
      show(); // 광고 표시  
    } else {  
      // 조건 미충족 시 바로 기능 수행  
      console.log("Navigating to next level without ad...");  
      // navigation.navigate('NextLevel');  
    }  
  };

  return (  
    \<View style\={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}\>  
      \<Text\>누적 사용 시간 충족 여부: {isAdReady? 'YES' : 'NO'}\</Text\>  
      \<Button title\="다음 단계로 이동" onPress\={handleNextLevel} /\>  
    \</View\>  
  );  
};

export default HomeScreen;

**구현 핵심 포인트:**

* **사전 로드 (Preloading):** 전면 광고는 용량이 크기 때문에 show()를 호출하기 전에 미리 load()해두어야 지연 없이 표시된다. 10  
* **이벤트 기반 리셋:** 광고가 닫히는 시점(isClosed)에 타이머를 리셋하여 정확히 1시간 주기를 유지한다.  
* **조건부 노출:** isAdReady가 true여도 광고 로드에 실패했을 수 있으므로 isLoaded를 함께 체크하여 앱의 흐름이 끊기지 않도록 한다.

## ---

**7\. 개인정보 보호 및 규정 준수 (Privacy & Compliance)**

한국 시장, 특히 30-40대 타겟의 앱 서비스에서 개인정보 보호법(PIPA)과 구글의 정책 준수는 필수적이다. Google은 이를 위해 **UMP (User Messaging Platform)** SDK 사용을 의무화하고 있다.

### **7.1 UMP SDK 통합의 필요성**

과거에는 GDPR(유럽) 지역에만 동의가 필요했으나, 현재 Google 정책은 한국을 포함한 전 세계 사용자에게 개인화 광고 동의를 받거나, 최소한 개인정보 처리 방침을 고지할 것을 권장한다. 특히 react-native-google-mobile-ads는 UMP SDK를 내장하고 있어 별도 설치 없이 API 호출만으로 구현 가능하다. 16

### **7.2 한국 연령 제한 대응 (만 14세 미만)**

한국 정보통신망법에 따라 만 14세 미만 아동의 개인정보 수집 시 법정대리인의 동의가 필요하다. 앱 타겟이 30-40대라 하더라도, 미성년자가 접근할 가능성을 기술적으로 배제해야 한다.

**TFUA (Tag For Under Age of Consent) 설정:**

앱에 로그인 기능이 없어 사용자의 정확한 나이를 알 수 없다면, 보수적인 접근으로 **아동 대상 설정**을 제어하거나, UMP 설정을 통해 미성년자 태그를 적용할 수 있다.

TypeScript

import { MobileAds } from 'react-native-google-mobile-ads';

// 앱 초기화 시 설정  
MobileAds().setRequestConfiguration({  
  // COPPA (미국 아동 보호법) 준수 여부: false (타겟이 30-40대이므로)  
  tagForChildDirectedTreatment: false,  
    
  // 유럽/한국 등 동의 연령 미만 태그 (TFUA)  
  // 사용자가 14세 미만임을 확신할 수 없다면 false로 두되,  
  // 앱 스토어의 콘텐츠 등급을 '만 12세 이상' 또는 '만 17세 이상'으로 설정하여 1차 필터링  
  tagForUnderAgeOfConsent: false,   
});

**주의사항:** tagForUnderAgeOfConsent를 true로 설정하면 UMP 동의 폼이 표시되지 않으며, 자동으로 비개인화 광고(Non-personalized ads)만 송출된다. 이는 광고 수익 감소로 이어질 수 있으나, 법적 리스크를 최소화하는 안전장치가 된다. 18

### **7.3 ATT (iOS) 및 Android ID 추적**

Android에서도 Advertising ID (AAID) 사용에 대한 투명성이 요구된다. AndroidManifest.xml에 다음 권한이 자동으로 포함되지만, Play Console의 '앱 콘텐츠' 섹션에서 광고 ID 사용 여부를 명확히 신고해야 한다.

XML

\<uses-permission android:name\="com.google.android.gms.permission.AD\_ID"/\>

## ---

**8\. 테스트 및 배포 전 검증 전략 (Testing & Validation)**

사용자는 "Play Store 배포 전 테스트 가능 여부"를 중요한 조건으로 제시하였다. 이 단계는 많은 개발자가 계정 정지(Account Suspension)를 당하는 위험 구간이므로 각별한 주의가 필요하다.

### **8.1 사이드로딩(Sideloading) 상태에서의 테스트**

Play Store에 앱이 게시되지 않은 상태(Unpublished)에서 서명된 APK(Signed APK)를 기기에 직접 설치(Sideloading)하여 테스트할 수 있다. 하지만 이때 **실제 광고 ID**를 사용하면 두 가지 문제가 발생한다:

1. **광고 미노출 (Error Code 3):** AdMob은 트래픽 품질이 검증되지 않은 신규 앱/미게시 앱에 대해 광고 송출을 제한한다.  
2. **무효 트래픽(Invalid Traffic) 제재:** 개발자가 본인의 앱 광고를 테스트하다가 실수로 클릭하거나 노출을 많이 발생시키면 계정이 영구 정지될 수 있다. 20

### **8.2 안전한 테스트 절차 (Step-by-Step)**

**Step 1: 테스트 광고 ID 사용 (개발 단계)**

코드 작성 및 기능 구현 단계에서는 무조건 구글이 제공하는 테스트 ID(TestIds.INTERSTITIAL)를 사용해야 한다. 이 ID는 언제나 100% 광고를 채워주므로 로직 검증에 최적화되어 있다.

**Step 2: 테스트 기기 등록 (실제 ID 검증 단계)**

배포 직전, 실제 Ad Unit ID로 교체한 후 광고가 잘 나오는지 보고 싶다면, 반드시 AdMob 콘솔에서 본인의 테스트 기기를 등록해야 한다.

* **방법:** AdMob 콘솔 \> 설정 \> 테스트 기기 \> 기기 추가 (Advertising ID 입력)  
* **효과:** 테스트 기기로 등록된 단말기에서는 실제 Ad Unit ID를 호출해도 'Test Ad'라는 라벨이 붙은 테스트 광고가 송출된다. 이는 정책 위반 없이 실제 ID 연동 상태를 확인할 수 있는 유일한 방법이다. 6

**Step 3: 서명된 APK(Release Build) 테스트**

디버그 모드와 릴리즈 모드는 난독화(ProGuard/R8) 설정으로 인해 동작이 다를 수 있다.

cd android &&./gradlew assembleRelease 명령어로 생성된 APK를 테스트 기기에 설치하여 최종 확인한다. 이때 Multidex 관련 크래시가 없는지 중점적으로 확인한다.

## ---

**9\. 문제 해결 및 최적화 (Troubleshooting)**

### **9.1 일반적인 오류 코드 및 대응**

| 오류 코드 | 의미 | 원인 및 해결책 |
| :---- | :---- | :---- |
| **Error Code 0 (INTERNAL\_ERROR)** | 내부 오류 | 주로 응답 처리 중 발생. SDK 초기화가 완료되었는지 확인 필요. |
| **Error Code 1 (INVALID\_REQUEST)** | 잘못된 요청 | Ad Unit ID 오타 혹은 AdMob 계정 비활성화 상태. ID 확인 요망. |
| **Error Code 2 (NETWORK\_ERROR)** | 네트워크 오류 | 인터넷 연결 확인. 에뮬레이터의 경우 네트워크 브리지 설정 확인. |
| **Error Code 3 (NO\_FILL)** | 광고 없음 | **가장 흔한 오류.** 인벤토리가 부족하거나 앱이 아직 검토 중인 경우. 테스트 기기 등록 후 확인하거나, 앱 출시 후 시간이 지나면 해결됨. 22 |

### **9.2 Android 빌드 실패 (Duplicate Class / Dex Issue)**

빌드 시 Duplicate class 또는 Cannot fit requested classes 에러가 발생하면 Multidex 설정이 제대로 되었는지 확인한다. 또한 MainApplication.java 패키지 경로와 build.gradle의 namespace가 일치하는지 확인해야 한다.

### **9.3 메모리 누수 방지**

useUsageTimer나 useInterstitialAd에서 생성한 이벤트 리스너(subscription)와 타이머(interval)는 컴포넌트가 언마운트(Unmount)될 때 반드시 remove() 및 clearInterval()로 정리(Clean-up)해주어야 한다. 그렇지 않으면 백그라운드에서 좀비 타이머가 돌아가며 배터리를 소모하고 예기치 않은 광고 요청을 보낼 수 있다.

## ---

**10\. 결론 (Conclusion)**

본 보고서는 React Native 환경에서 네이티브 코드 작성 없이, 고도화된 시간 기반 로직을 포함한 AdMob 전면 광고 구현 방안을 제시하였다.

1. **아키텍처:** react-native-google-mobile-ads와 Multidex 설정을 통해 안정적인 빌드 환경을 구축하였다.  
2. **로직:** AppState와 AsyncStorage를 활용한 커스텀 Hook을 통해, 단순 시간 경과가 아닌 '실사용 시간 1시간'이라는 정교한 조건을 충족시켰다.  
3. **정책:** 한국 30-40대 타겟에 맞춘 민감한 카테고리 차단과 UMP 설정을 통해 브랜드 안전성과 법적 리스크를 관리하였다.  
4. **검증:** 테스트 기기 등록을 통한 안전한 사전 테스트 절차를 확립하여 계정 정지 위험을 최소화하였다.

이 가이드라인을 준수함으로써 개발자는 기술적 복잡성을 최소화하면서도 사용자 경험을 해치지 않는 지속 가능한 수익 모델을 앱에 탑재할 수 있을 것이다. 최종적으로 Play Store 배포 시에는 AndroidManifest.xml과 코드 내의 테스트 ID를 실제 프로덕션 ID로 교체하는 것을 잊지 말아야 한다.

## ---

## **Play Store 배포 준비**

### **전면 광고 정책 준수사항**

Google Play의 **Better Ads Experience** 정책을 반드시 준수해야 합니다:

**허용되는 광고 타이밍:**

* 게임 레벨 완료 후 [Google](https://developers.google.com/admob/ios/interstitial)  
* 점수 화면 이후  
* 콘텐츠 세그먼트 종료 시  
* 자연스러운 전환 지점 [Google](https://developers.google.com/admob/ios/interstitial)

**금지되는 광고 타이밍:**

* 앱 시작 직후 (App Open 광고는 별도)  
* 게임 플레이 중  
* 레벨 시작 시  
* 다른 전면 광고 직후  
* 로딩 화면 전

**빈도 제한:** 사용자 행동 **2회당 최대 1회** 전면 광고. 1시간 간격은 정책을 충분히 준수합니다.

**닫기 요구사항:** 모든 전면 광고는 **15초 이내**에 닫을 수 있어야 합니다.

### **Play Console 설정**

1. **광고 포함 선언**: Policy and programmes → App content → Manage → **"Yes, my app contains ads"** 선택  
2. **개인정보처리방침 URL**: 앱 등록 시 필수 입력

Admob 광고를 넣는 여러 가지 방법이 있으나,

일단 테스트 ID를 쓰는 방법을 이용했습니다.

PlayStore에 배포하지 않고, 실제 광고 비용을 받지 않고,

광고를 넣는 기능만을 테스트하는 방법입니다.

https://admob.google.com/home/

여기서 계정을 만듭니다.

그 다음에 프로젝트 폴더 안에 docs 폴더를 만들고

위에서 만든 md 파일을 참조하여 기능을 구현했습니다.

다음의 코드를 쓰면 테스트 앱, 테스트 ID를 써서 테스트 모드로 광고를 넣을 수 있습니다.

// Google AdMob Test IDs (safe for development)
const TEST_APP_ID = 'ca-app-pub-3940256099942544~3347511713';
const TEST_INTERSTITIAL_AD_UNIT_ID = 'ca-app-pub-3940256099942544/1033173712';

테스트 시에는 1분에 한 번 광고가 보이도록 했습니다.

그리고~!! 꼭 에뮬레이터의 PlayStore에서 로그인~!! 을 해두어야

광고가 정상적으로 나옵니다. 위에 Test Ad라고 붙은 것이 나옵니다.

프로웨인 무역 박람회 - 스크린샷

결과와 배운 점

실제 배포하고 광고 수익을 얻으려면 Apps -> Ad units에서 전면 광고를 추가한 후

나오는 ID를 소스에 붙여넣기 하면 됩니다.

Google Play 스토어의 광고 단위
// Production IDs - Replace these with your actual AdMob IDs
const PRODUCTION_APP_ID = 'YOUR_ADMOB_APP_ID';
const PRODUCTION_INTERSTITIAL_AD_UNIT_ID = 'YOUR_INTERSTITIAL_AD_UNIT_ID';

그리고 실 운영 시에는 1분을 1시간으로 설정해야 합니다.

1
2개의 답글
지피터스 뉴스레터

이번 주 AI, 딱 쓸 것만

매주 월요일 오후,
읽고 바로 써먹을 AI 소식만