본문 바로가기

Capacitor에서 AdMob GDPR 동의 처리하기

Capacitor 앱에서 Google UMP SDK로 AdMob GDPR 동의 처리: 동의 양식, 개인정보 옵션, ATT 순서, 디버그 지리 정보 및 오류 처리, code 포함.

Capacitor 앱에서 AdMob GDPR 동의 처리를 위한 개인정보 및 동의 일러스트

Capacitor 앱에서 AdMob GDPR 동의를 처리하려면 Google의 User Messaging Platform(UMP) SDK을 사용하세요. 모든 앱 실행 시 동의 정보를 요청하고, 필요 시 동의 양식을 표시하며, 광고는 오직 canRequestAds 이 true일 때만 초기화해야 합니다. 사용자가 선택을 변경할 수 있는 개인정보 옵션 진입점도 필요합니다. 또한 iOS에서는 UMP 플로우가 App Tracking Transparency 프롬프트보다 먼저 실행되어야 합니다. 이 가이드는 UMP를 JavaScript로 래핑하는 플러그인과, 두 가지 설정을 통한 전체 플로우를 보여줍니다. @capgo/capacitor-admob 작은 native consent bridge를 통해 가능합니다.

UMP의 역할과 AdMob에서 이를 요구하는 이유

2024년 1월부터 Google은 EEA, 영국, 스위스 사용자에게 광고를 게재하는 퍼블리셔가 IAB Transparency and Consent Framework(TCF)와 통합된 Google 인증 동의 관리 플랫폼(CMP)을 사용하도록 요구합니다. UMP SDK는 Google이 직접 제공하는 인증 CMP로, 무료이며 AdMob 콘솔에 내장되어 있습니다.

UMP는 세 가지 일을 합니다:

  1. 사용자가 동의가 필요한 지역에 있는지 확인합니다.
  2. AdMob에서 설정한 동의 메시지를 사용자의 언어로 표시합니다.
  3. 사용자의 선택을 기기에 TC 문자열로 저장합니다(표준 IABTCF_ 키는 UserDefaults iOS의 SharedPreferences Android)에서 Google Mobile Ads SDK와 미디에이션 파트너가 광고를 요청할 때 읽습니다.

같은 콘솔에서 AdMob이 “US states” 메시지라고 부르는 미국 주 개인정보 보호 규정에도 UMP를 사용할 수 있습니다.

1단계: AdMob에서 메시지 만들기

AdMob 콘솔에서 다음으로 이동하세요: 개인정보 및 메시지:

  • 프로젝트 루트 디렉토리에 유럽 규정 메시지(GDPR)를 생성합니다. 적용할 앱, 언어, 광고 파트너 목록을 선택하세요.
  • 선택적으로 미국 주 메시지.
  • iOS에서는 IDFA 설명 앱 추적 투명성(ATT)을 요청할 계획이 있다면 메시지를 생성하세요. UMP는 시스템 ATT 프롬프트 표시 전에 이를 보여줍니다.
  • 메시지를 게시하세요. 게시되지 않은 메시지는 절대 표시되지 않습니다.

나중에 앱에서 양식이 전혀 나타나지 않는다면, 게시되지 않았거나 할당되지 않은 메시지를 가장 먼저 확인하세요.

2단계: 작업 순서 결정

대부분의 앱에서 통하는 순서는 다음과 같습니다:

  1. 앱 시작.
  2. UMP로 동의 정보 업데이트 요청 (매 실행 시).
  3. 필요한 경우, 동의 양식을 로드하고 표시합니다.
  4. 만약 canRequestAds true이면, Mobile Ads SDK를 시작하고 광고를 로드합니다.
  5. UMP에서 개인정보 보호 옵션이 필요하다고 응답하면, 설정 화면에 “개인정보 설정” 항목을 표시합니다.

Google은 또한 다음과 같은 경우 동의 요청과 동시에 Mobile Ads SDK를 시작할 수 있도록 허용합니다. canRequestAds 이전 세션에서 이미 true였던 경우입니다. 이를 통해 재방문 사용자의 첫 광고 노출 시간을 단축할 수 있습니다.

옵션 A: JavaScript로 UMP를 래핑하는 플러그인

@capacitor-community/admob (버전 8은 Capacitor 8을 지원합니다) UMP 메서드를 직접 노출합니다: requestConsentInfo, showConsentForm, showPrivacyOptionsForm 및 resetConsentInfo.

bun add @capacitor-community/admob
bunx cap sync
import {
  AdMob,
  AdmobConsentStatus,
  AdmobConsentDebugGeography,
} from '@capacitor-community/admob';

export async function initAdsWithConsent(isDebug: boolean) {
  await AdMob.initialize();

  let info = await AdMob.requestConsentInfo(
    isDebug
      ? {
          debugGeography: AdmobConsentDebugGeography.EEA,
          testDeviceIdentifiers: ['YOUR_TEST_DEVICE_ID'],
        }
      : undefined,
  );

  if (info.isConsentFormAvailable && info.status === AdmobConsentStatus.REQUIRED) {
    info = await AdMob.showConsentForm();
  }

  // The enum value is the string 'REQUIRED'
  const showPrivacyEntry = String(info.privacyOptionsRequirementStatus) === 'REQUIRED';

  return { canRequestAds: info.canRequestAds, showPrivacyEntry };
}

export async function openPrivacyOptions() {
  await AdMob.showPrivacyOptionsForm();
}

이 플러그인은 위 순서에서 유일한 예외입니다. README에서 요구하는데, AdMob.initialize() 첫 번째 제출이 승인되기 requestConsentInfo(), 그리고 iOS에서는 다음이 실행될 때까지 동의 양식을 표시할 수 없습니다: initialize() 가 실행된 후입니다. initialize() 는 SDK를 시작하지만 광고를 요청하지는 않습니다. 따라서 핵심 규칙은 다음이 실행되기 전까지는 광고를 절대 로드하지 않는 것입니다: canRequestAds 가 true인 경우입니다. 사용자가 응답할 때까지 SDK를 중지 상태로 유지해야 한다면, 동의가 먼저 실행되는 Option B를 사용하세요. Option B에서는 동의가 AdMob.start().

체크 항목 canRequestAds 시작할 때뿐 아니라 매 광고 로드 전에 실행됩니다. 사용자가 개인정보 보호 옵션 양식에서 동의를 철회할 수 있기 때문입니다.

Option B: 네이티브 UMP 브리지를 사용하는 @capgo/capacitor-admob

@capgo/capacitor-admob 제공하는 것은 AdMob.start(), 요청 설정, 배너, 인터스티셜, 리워드 및 리워드 인터스티셜 광고, 그리고 iOS 추적 권한을 처리합니다. UMP를 래핑하지 않으므로, 호출하기 전에 동의 플로우를 실행하는 작은 앱 로컬 플러그인을 추가해야 합니다. AdMob.start().

bun add @capgo/capacitor-admob
bunx cap sync

UMP SDK 추가

  • iOS: Google의 GoogleUserMessagingPlatform 패키지를 Swift Package Manager(Capacitor 8 프로젝트는 기본적으로 SPM 사용)로 추가하거나, 프로젝트가 여전히 CocoaPods를 사용한다면 동일한 이름의 CocoaPods 포드를 사용하세요.
  • Android: 에서 android/app/build.gradle:
dependencies {
    implementation "com.google.android.ump:user-messaging-platform:<latest version>"
}

Google의 Maven 저장소에서 최신 릴리스를 사용하세요.

생성 ios/App/App/ConsentPlugin.swift:

import Capacitor
import UserMessagingPlatform

@objc(ConsentPlugin)
public class ConsentPlugin: CAPPlugin, CAPBridgedPlugin {
    public let identifier = "ConsentPlugin"
    public let jsName = "Consent"
    public let pluginMethods: [CAPPluginMethod] = [
        CAPPluginMethod(name: "gather", returnType: CAPPluginReturnPromise),
        CAPPluginMethod(name: "showPrivacyOptions", returnType: CAPPluginReturnPromise),
    ]

    @objc func gather(_ call: CAPPluginCall) {
        let parameters = RequestParameters()
        DispatchQueue.main.async {
            ConsentInformation.shared.requestConsentInfoUpdate(with: parameters) { error in
                if let error = error {
                    call.reject(error.localizedDescription)
                    return
                }
                guard let vc = self.bridge?.viewController else {
                    call.reject("No view controller")
                    return
                }
                ConsentForm.loadAndPresentIfRequired(from: vc) { formError in
                    if let formError = formError {
                        call.reject(formError.localizedDescription)
                        return
                    }
                    call.resolve(self.state())
                }
            }
        }
    }

    @objc func showPrivacyOptions(_ call: CAPPluginCall) {
        DispatchQueue.main.async {
            guard let vc = self.bridge?.viewController else {
                call.reject("No view controller")
                return
            }
            ConsentForm.presentPrivacyOptionsForm(from: vc) { formError in
                if let formError = formError {
                    call.reject(formError.localizedDescription)
                    return
                }
                call.resolve(self.state())
            }
        }
    }

    private func state() -> [String: Any] {
        return [
            "canRequestAds": ConsentInformation.shared.canRequestAds,
            "privacyOptionsRequired":
                ConsentInformation.shared.privacyOptionsRequirementStatus == .required,
        ]
    }
}

에서 등록하고 CAPBridgeViewController 서브클래스를 만들고, 해당 클래스를 뷰 컨트롤러에 설정하세요. Main.storyboard:

import Capacitor

class MainViewController: CAPBridgeViewController {
    override open func capacitorDidLoad() {
        bridge?.registerPluginInstance(ConsentPlugin())
    }
}

생성 android/app/src/main/java/com/example/app/ConsentPlugin.java (패키지 이름을 사용하세요):

package com.example.app;

import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.google.android.ump.ConsentInformation;
import com.google.android.ump.ConsentRequestParameters;
import com.google.android.ump.UserMessagingPlatform;

@CapacitorPlugin(name = "Consent")
public class ConsentPlugin extends Plugin {

    @PluginMethod
    public void gather(PluginCall call) {
        ConsentInformation info = UserMessagingPlatform.getConsentInformation(getContext());
        ConsentRequestParameters params = new ConsentRequestParameters.Builder().build();

        getActivity().runOnUiThread(() ->
            info.requestConsentInfoUpdate(
                getActivity(),
                params,
                () -> UserMessagingPlatform.loadAndShowConsentFormIfRequired(getActivity(), formError -> {
                    if (formError != null) {
                        call.reject(formError.getMessage());
                        return;
                    }
                    call.resolve(state(info));
                }),
                requestError -> call.reject(requestError.getMessage())
            )
        );
    }

    @PluginMethod
    public void showPrivacyOptions(PluginCall call) {
        getActivity().runOnUiThread(() ->
            UserMessagingPlatform.showPrivacyOptionsForm(getActivity(), formError -> {
                if (formError != null) {
                    call.reject(formError.getMessage());
                    return;
                }
                call.resolve(state(UserMessagingPlatform.getConsentInformation(getContext())));
            })
        );
    }

    private JSObject state(ConsentInformation info) {
        JSObject ret = new JSObject();
        ret.put("canRequestAds", info.canRequestAds());
        ret.put(
            "privacyOptionsRequired",
            info.getPrivacyOptionsRequirementStatus() ==
                ConsentInformation.PrivacyOptionsRequirementStatus.REQUIRED
        );
        return ret;
    }
}

다음에 등록하세요: MainActivity.java 첫 번째 제출이 승인되기 super.onCreate:

public class MainActivity extends BridgeActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        registerPlugin(ConsentPlugin.class);
        super.onCreate(savedInstanceState);
    }
}

TypeScript에서 호출

import { registerPlugin } from '@capacitor/core';
import { AdMob, BannerAd } from '@capgo/capacitor-admob';

interface ConsentState {
  canRequestAds: boolean;
  privacyOptionsRequired: boolean;
}

interface ConsentPlugin {
  gather(): Promise<ConsentState>;
  showPrivacyOptions(): Promise<ConsentState>;
}

const Consent = registerPlugin<ConsentPlugin>('Consent');

export async function startAds() {
  let state: ConsentState;
  try {
    state = await Consent.gather();
  } catch (err) {
    console.warn('Consent flow failed', err);
    return { showPrivacyEntry: false };
  }

  if (state.canRequestAds) {
    await AdMob.start();
    const banner = new BannerAd({
      adUnitId: 'ca-app-pub-3940256099942544/2934735716', // Google test banner (iOS)
      position: 'bottom',
    });
    await banner.show();
  }

  return { showPrivacyEntry: state.privacyOptionsRequired };
}

export const openPrivacyOptions = () => Consent.showPrivacyOptions();

Native code 변경 사항은 새 스토어 빌드가 필요합니다. 그 후에는 설정 화면의 동의 문구와 광고 배치 로직을 함께 배포할 수 있습니다. Capgo Live Update 사용하기.

3단계: 개인정보 보호 옵션 진입점 추가

UMP에서 개인정보 보호 옵션이 필요하다고 보고하면, Google은 동의 선택지를 다시 열 수 있는 눈에 보이는 방법을 기대합니다. 설정 페이지에 “개인정보 보호 설정” 항목을 추가하고, 필요하지 않을 때는 숨기세요:

const { showPrivacyEntry } = await startAds();
document.querySelector('#privacy-row')?.toggleAttribute('hidden', !showPrivacyEntry);
document.querySelector('#privacy-row')?.addEventListener('click', () => openPrivacyOptions());

사용자가 선택 사항을 변경한 후에는, canRequestAds 다음 광고 요청 전에 다시 읽으세요.

4단계: iOS에서 App Tracking Transparency

ATT는 GDPR과 별개입니다. ATT는 iOS에서 IDFA 접근 권한을 제어하고, GDPR 동의는 개인 데이터의 처리 여부와 방식을 제어합니다. iOS를 사용하는 EU 사용자라면 보통 둘 다 필요합니다.

  • 추가 NSUserTrackingUsageDescription 대상 Info.plist.
  • AdMob에서 해당 메시지를 설정했다면 UMP가 IDFA 설명 메시지를 표시하고 ATT를 트리거하도록 하세요. 그렇지 않다면 consent flow가 끝난 뒤 직접 ATT를 요청하세요.

사용하면 @capgo/capacitor-admob 다음과 같이 ATT 상태를 직접 읽고 요청할 수 있습니다:

import { AdMob, TrackingAuthorizationStatus } from '@capgo/capacitor-admob';

const { status } = await AdMob.trackingAuthorizationStatus();
if (status === TrackingAuthorizationStatus.notDetermined) {
  await AdMob.requestTrackingAuthorization();
}

Capgo에는 별도의 App Tracking Transparency 플러그인도 있습니다. 다른 SDK용으로 ATT가 필요하다면 사용하세요. privacy manifest와 App Store privacy label에 추적을 선언하는 것도 잊지 마세요. 자세한 내용은 privacy manifest 가이드에서 확인하세요.

5단계: 디버그 지역으로 테스트

UMP은 EEA, 영국 또는 스위스에 있는 사용자에게만 GDPR 양식을 표시합니다. 다른 지역에서 테스트하려면:

  • 앱을 한 번 실행한 후, UMP 로그에서 기기의 해시된 ID를 찾습니다.
  • 이를 테스트 기기로 추가하고 디버그 지역을 EEA로 설정합니다. 커뮤니티 플러그인을 사용하는 경우, 다음을 전달합니다. debugGeography 및 testDeviceIdentifiers 대상 requestConsentInfo. 네이티브 브리지에서는 다음을 설정합니다. DebugSettings iOS의 ConsentDebugSettings Android 디버그 빌드에서만 적용됩니다.
  • 실행 간 동의 상태 초기화 (resetConsentInfo() 커뮤니티 플러그인에서 ConsentInformation.reset() 네이티브로) 양식이 다시 표시되도록 합니다.

릴리스 빌드에 디버그 설정을 포함하지 마세요. 빌드 플래그로 제어하세요.

6단계: 앱 블로킹 없이 오류 처리

동의 요청은 실패할 수 있습니다: 네트워크 오류, 앱 ID 설정 오류, 게시된 메시지 없음. 실패 시 '일시적 광고 비활성화'로 처리하고 크래시를 방지하세요:

  • 만약 requestConsentInfoUpdate 실패하지만 canRequestAds 이전 세션에서 true였다면, 여전히 광고를 요청할 수 있습니다.
  • 양식 로딩에 실패하면 로그를 기록하고 다음 시작 시 재시도하세요.
  • 동의 양식이 화면에 표시되는 동안 광고를 표시하지 마세요.

문제 해결

폼이 표시되지 않습니다. 메시지가 게시되지 않았거나, 이 앱 ID에 할당되지 않았거나, EEA 외부에 있으며 디버그 지리 설정이 아닙니다. 콘솔의 AdMob 앱 ID가 Info.plist 및 AndroidManifest.xml 일치하는지 확인하세요.

모든 실행 시 폼이 표시됩니다. 사용자가 선택 없이 닫았거나, 프로덕션에서 code를 호출했습니다.

canRequestAds 미국 사용자의 경우 false입니다. 미국 주 메시지를 게시했고 사용자가 판매 또는 공유를 거부했습니다. SDK를 통해 제한된 광고만 제공하며, 우회하지 마세요.

중개 파트너에게 동의 정보가 전달되지 않습니다. 각 파트너 어댑터가 최신 버전인지, GDPR 메시지의 광고 파트너 목록에 포함되어 있는지 확인하세요.

iOS ATT 프롬프트가 표시되지 않습니다. NSUserTrackingUsageDescription 누락되었거나, 사용자가 설정에서 “앱이 추적을 요청하도록 허용”을 꺼 둔 경우입니다.

양식만으로는 부족한 컴플라이언스

UMP는 광고 부분을 처리합니다. 앱에는 여전히 AdMob, 데이터 보존 규칙, 데이터 요청 처리 방법을 명시한 개인정보 처리방침이 필요합니다. 저희 GDPR 준수 체크리스트 가 앱의 나머지 부분을 다룹니다.

Capacitor 앱용 Live updates

웹 레이어 버그가 라이브 상태라면, 앱 스토어 승인을 며칠씩 기다리지 말고 Capgo를 통해 수정 사항을 배포하세요. 네이티브 변경 사항은 정상 리뷰 절차를 그대로 따르는 반면, 사용자는 백그라운드에서 업데이트를 받습니다.

블로그 최신 글

Capgo는 정말 프로페셔널한 모바일 앱을 만드는 데 필요한 최고의 인사이트를 제공합니다.