//구글콘솔 광고 추가가
728x90
반응형

AIStudio에서 Admob을 추가시켜서 작업한 뒤, 안드로이드 스튜디오로 테스트를 할 때 빌드된 앱이 켜지지 않는 버그가 있을 수 있다.
이런 경우 대부분 AdMob App ID가 AndroidManifest에 없는 경우라던지, Vite에서 base 설정이 없는 경우다.
파일 폴더를 열어서 수정해주자.
 
1) AndroidManifest에 AdMob App ID 추가 (앱 즉시 종료 방지)
>> AdMob을 쓰면 JS 코드만으로는 부족하다. Android 네이티브 설정이 필수다.

📌 파일:
android/app/src/main/AndroidManifest.xml
📌 위치:
<application ...> 태그 내부
  <application
    ...>

    <!-- AdMob App ID 추가 (TEST) -->
    <meta-data
        android:name="com.google.android.gms.ads.APPLICATION_ID"
        android:value="ca-app-pub-...~..." /> // 테스트용 App ID 추가 :"ca-app-pub-3940256099942544~3347511713"

    <activity ...>
      ...
    </activity>

    ...
</application>

 
>> 이게 없으면 앱 실행 직후 크래시가 난다, 배너가 아예 안 뜨는 문제도 발생할 수 있음. 반드시 확인해 보기.
 
2) ) Vite 설정: base 경로 수정 (흰 화면 방지)

📌 파일: vite.config.ts
return {
  base: './', //이거 추가.
  server: { port: 3000, host: '0.0.0.0' },
  plugins: [react()],
  ...
}

 
3) 빌드 산출물 반영
PowerShell에서 아래 복붙 후 엔터.

npm install
npm run build
npx cap sync android

 
4) 안드로이드 스튜디오 실행

npx cap open android

 
 


 
 
이제 다시 AdMob 배너를 앱 전체 공통으로 하단에 고정하고, adsRemoved 상태로 광고 표시/제거를 테스트해보자.
AIStudio에서 AdMob 배너를 붙였는데 App.tsx가 너무 무거워진 것 같아 나눠서 정리했다.
 

< 작업할 것 >
- 배너 광고는 앱 전체 공통 하단 고정
- adsRemoved 상태에 따라 false → 광고 표시 true → 광고 숨김
- 광고 로직은 App.tsx에서 분리하여 유지보수 가능한 구조로 만들기
- Android / Web 플랫폼 차이로 인한 크래시 방지

>> 광고는 네이티브(오버레이)로 뜨고, React(UI)는 그 광고를 제어만 하는 구조로 만들기.

 

폴더 정리
: React 코드 위치 vs Android 네이티브 코드 위치

 

Capacitor 앱에서:
- android/ 폴더 → Android 네이티브 코드(Gradle/Manifest/Kotlin)
- src/ 폴더 → React/Vite 웹 앱 코드(JS/TS/TSX)

 
React 코드는 절대 android/app/src 에 두지 않는다. Capacitor 구조에서 React는 항상 웹 앱이란다.
그래서 프로젝트 폴더에 아래처럼 src폴더를 추가해 줄 것이다. 그리고 나머지도 추가해 주자.

프로젝트 폴더/
├─ android/                  # 네이티브(Android)
├─ src/                      # React/Vite 코드
│  ├─ components/
│  │  └─ GlobalAdBanner.tsx  # 전 앱 공통 배너 컨트롤러
│  ├─ services/
│  │  └─ ads/
│  │     └─ admob.ts         # AdMob 제어 로직
│  └─ ...
├─ App.tsx                   # 루트 App (UI 중심)
├─ capacitor.config.ts
├─ vite.config.ts
└─ package.json

 
이제 GlobalAdBanner.tsx와 admob.ts를 만들어 주자. 
 

GlobalAdBanner란?
: GlobalAdBanner.tsx 는 화면에 버튼/텍스트/레이아웃을 그리는 컴포넌트 X

 
▶ 광고 표시/숨김을 제어하는 “컨트롤러 컴포넌트” 다. 아래 코드를 복사 붙여 넣기 해서 만들어주자.
>> 역할 : 앱이 실행되면 자동으로 AdMob을 초기화하고, adsRemoved 값에 따라 show/hide를 실행하는 역할.

// src/components/GlobalAdBanner.tsx
import { useEffect } from "react";
import {
  bindBannerListenersOnce,
  initAdMobOnce,
  showGlobalBanner,
  hideGlobalBanner,
} from "../services/ads/admob";

/**
 * ✅ 컨트롤러 컴포넌트
 * - UI 렌더링 X (return null)
 * - adsRemoved 상태로 배너 show/hide만 담당
 */
export default function GlobalAdBanner({ adsRemoved }: { adsRemoved: boolean }) {
  // 1) 앱 시작 시 딱 1회: 리스너 등록 + AdMob 초기화
  useEffect(() => {
    bindBannerListenersOnce();
    initAdMobOnce();
  }, []);

  // 2) adsRemoved 값이 바뀔 때마다: show/hide 실행
  useEffect(() => {
    const run = async () => {
      if (adsRemoved) {
        await hideGlobalBanner();
      } else {
        await showGlobalBanner();
      }
    };

    run();

    // (3) 핫리로드/언마운트 안정장치
    return () => {
      hideGlobalBanner();
    };
  }, [adsRemoved]);

  // 배너는 네이티브 오버레이로 뜨므로 React UI는 렌더하지 않음
  return null;
}

 
위의 return null을 통해

  • React DOM에 아무것도 렌더 하지 않고
  • useEffect()만 실행해서
  • 네이티브 AdMob 배너를 띄우거나/숨긴다.

▶ 광고는 React가 그리는 게 아니라, Android 네이티브 레이어(WebView 위)에 오버레이로 표시되기 때문에 이 방식이 정석이다.

★ 왜 return null이 중요한가.
* 배너는 네이티브 오버레이로 뜸 → React DOM에 뭘 추가할 필요가 없음.
* return으로 <div> 같은 걸 렌더 하면 오히려 “배너가 웹 레이아웃 일부인 것처럼” 혼동만 생김.
* GlobalAdBanner는 “앱 실행 시 자동으로 동작하는 기능 모듈”로 보는 게 맞음.

 

AdMob 제어 로직은 services로 분리 (파일 위치 + 역할)
: 여기는 “실제로 AdMob 플러그인을 호출하는 코드”를 모아둔다.
- initialize를 중복 호출하지 않게 방지.
- 배너 이벤트 리스너를 중복 등록하지 않게 방지.
- show/hide 토글 시 Android에서 다시 안 뜨는 문제 해결을 위해 hide + remove / show 전 remove 패턴 적용.
// src/services/ads/admob.ts
import { Capacitor } from "@capacitor/core";
import {
  AdMob,
  BannerAdPluginEvents,
  BannerAdOptions,
  BannerAdPosition,
  BannerAdSize,
} from "@capacitor-community/admob";

/**
 * < Google TEST Ad Unit ID (Banner) >
 * - App ID는 AndroidManifest.xml <application> 아래 meta-data로 넣어야 함
 * - 여기서는 배너 ad unit id만 사용
 */
export const TEST_BANNER_ID = "ca-app-pub-3940256099942544/6300978111";

/** 내부 상태: 중복 init / 중복 listener 등록 방지 */
let didInit = false;
let didBind = false;

const isNative = () => Capacitor.getPlatform() !== "web";

/**
 * < 리스너 1회만 등록 >
 * - 리렌더링마다 addListener 붙으면 로그/이벤트가 중복되는 버그가 생김
 */
export function bindBannerListenersOnce() {
  if (!isNative() || didBind) return;

  AdMob.addListener(BannerAdPluginEvents.Loaded, () => {
    console.log("admob: banner_loaded_success");
  });

  AdMob.addListener(BannerAdPluginEvents.FailedToLoad, (info) => {
    console.error("admob: banner_load_failed", info);
  });

  didBind = true;
}

/**
 * < AdMob 초기화 1회만 >
 */
export async function initAdMobOnce() {
  if (!isNative() || didInit) return;

  console.log("admob: admob_init_start");
  try {
    await AdMob.initialize();
    didInit = true;
    console.log("admob: admob_init_complete");
  } catch (e) {
    console.error("admob: admob_init_failed", e);
  }
}

/**
 * < 전 앱 공통 하단 배너 표시 >
 * - Adaptive 배너 사용 (기기 폭에 맞게)
 * - isTesting: true (테스트 배너)
 */
export async function showGlobalBanner(adId: string = TEST_BANNER_ID) {
  if (!isNative()) return;

  console.log("admob: showBanner_called");
  const options: BannerAdOptions = {
    adId,
    adSize: BannerAdSize.ADAPTIVE_BANNER,
    position: BannerAdPosition.BOTTOM_CENTER,
    margin: 0,
    isTesting: true,
  };

  try {
    await AdMob.showBanner(options);
    console.log("admob: showBanner_success");
  } catch (e) {
    console.error("admob: showBanner_exception", e);
  }
}

/**
 * < 배너 숨김 >
 * - removeBanner는 show/hide 반복 시 꼬이는 케이스가 있어서 테스트 단계에서는 hide만 권장
 */
export async function hideGlobalBanner() {
  if (!isNative()) return;

  console.log("admob: hideBanner_called");
  try {
    await AdMob.hideBanner();
    console.log("admob: hideBanner_success");
  } catch (e) {
    console.error("admob: hideBanner_exception", e);
  }
}

 
▶  광고에서 가장 중요한 건 중복 호출 방지와 플랫폼 가드.

const isNative = () => Capacitor.getPlatform() !== "web";

 
▶  초기화/ 리스너는 1회만 하기.

let didInit = false;
let didBind = false;

 

  • 리렌더링마다 initialize() / addListener() 호출하면 버그 발생.
  • 반드시 내부 플래그로 막아야 한다.

**** 만약 위의 admob 코드 사용할 예정이라면 아래쪽의 admob 코드 사용하자. 추가됐다. ****
 

App.tsx에서는 “한 줄로 전역 배너 연결”만 한다 (삽입 위치)

 
App.tsx에서 광고를 직접 제어하지 않게 해야 한다. 전역 배너 컨트롤러만 호출하자. AdMob에 관련된 건 삭제해 주고 새로 만든 GlobalAdBanner를 import 해주고 한 줄만 추가해주면 된다.

import GlobalAdBanner from "./src/components/GlobalAdBanner";
<GlobalAdBanner adsRemoved={settings.adsRemoved} /> // App 컴포넌트 return 최상단에 한줄 추가


// < 추가 위치 > div 사이에 껴 넣어 주자.
return (
  <div className="...">
    <GlobalAdBanner adsRemoved={settings.adsRemoved} />

    {/* 기존 UI는 전부 그대로 */}
    ...
  </div>
);

 

  • React 컴포넌트 트리 상 App 최상단에서 한 번만 마운트되면 되는 역할이므로 루트 div 바로 아래에 두는 게 가장 안전하다.

 

테스트 광고 토글 문제
: hide 하고 다시 show 할 때 안 뜨는 오류.

 
내 경우 발생했던 오류였다. adsRemoved=true로 hide는 되는데, 다시 false로 show 했는데 배너가 안 뜨는 현상.
 
▶ 해결 방법 :
우선 admob.ts 를 켜주자.
Android에서는 hide만 하면 내부 상태가 꼬일 수 있어서 hideBanner 후 removeBanner까지 호출하는 패턴으로 가야 함.
 
 

  • hide 시: hideBanner() → removeBanner()
  • show 시: removeBanner()(안전용) → showBanner()
// src/services/ads/admob.ts
import { Capacitor } from "@capacitor/core";
import {
  AdMob,
  BannerAdPluginEvents,
  BannerAdOptions,
  BannerAdPosition,
  BannerAdSize,
} from "@capacitor-community/admob";

/**
 * < Google TEST Ad Unit ID (Banner) >
 * - App ID는 AndroidManifest.xml <application> 아래 meta-data로 넣어야 함
 * - 여기서는 배너 ad unit id만 사용
 */
export const TEST_BANNER_ID = "ca-app-pub-3940256099942544/6300978111";

/** 내부 상태: 중복 init / 중복 listener 등록 방지 */
let didInit = false;
let didBind = false;

const isNative = () => Capacitor.getPlatform() !== "web";

/**
 * < 리스너 1회만 등록 >
 * - 리렌더링마다 addListener 붙으면 로그/이벤트가 중복되는 버그가 생김
 */
export function bindBannerListenersOnce() {
  if (!isNative() || didBind) return;

  AdMob.addListener(BannerAdPluginEvents.Loaded, () => {
    console.log("admob: banner_loaded_success");
  });

  AdMob.addListener(BannerAdPluginEvents.FailedToLoad, (info) => {
    console.error("admob: banner_load_failed", info);
  });

  didBind = true;
}

/**
 * < AdMob 초기화 1회만 >
 */
export async function initAdMobOnce() {
  if (!isNative() || didInit) return;

  console.log("admob: admob_init_start");
  try {
    await AdMob.initialize();
    didInit = true;
    console.log("admob: admob_init_complete");
  } catch (e) {
    console.error("admob: admob_init_failed", e);
  }
}

/**
 * < 전 앱 공통 하단 배너 표시 >
 * - Adaptive 배너 사용 (기기 폭에 맞게)
 * - isTesting: true (테스트 배너)
 */
export async function showGlobalBanner(adId: string = TEST_BANNER_ID) {
  if (!isNative()) return;

  console.log("admob: showBanner_called");
  const options: BannerAdOptions = {
    adId,
    adSize: BannerAdSize.ADAPTIVE_BANNER,
    position: BannerAdPosition.BOTTOM_CENTER,
    margin: 0,
    isTesting: true,
  };

  try {
    // 혹시 남아있는 배너 인스턴스가 있으면 제거 시도 (에러 나도 무시)
    await AdMob.removeBanner().catch(() => {});
    await AdMob.showBanner(options);
    console.log("admob: showBanner_success");
  } catch (e) {
    console.error("admob: showBanner_exception", e);
  }
}

/**
 * < 배너 숨김 >
 * - removeBanner는 show/hide 반복 시 꼬이는 케이스가 있어서 테스트 단계에서는 hide만 권장
 */
export async function hideGlobalBanner() {
  if (!isNative()) return;

  console.log("admob: hideBanner_called");
  try {
    // 1) 일단 숨김
    await AdMob.hideBanner();
    // 2) 그리고 완전히 제거 (토글 복구 안정화)
    await AdMob.removeBanner();
    console.log("admob: hide/remove_banner_success");
  } catch (e) {
    console.error("admob: hide/remove_banner_exception", e);
  }
}

 
이 패턴 적용 후 정상 복구 확인.
 

마지막으로 powerShell에서 실제 반영 명령 하기
npm run build
npx cap sync android

 
그리고 다시 안드로이드 스튜디오 열어주고 실행시켜 보면 잘 되는 것을 확인할 수 있다.

npx cap open android

 
 
 
 

728x90
반응형

+ Recent posts