Unity 2021.3.36f1
GoogleMobileAds 9.0.0
AppsFlyer 6.13.10
----
[추가] 2024-04-05
GDPR 동의가 엮이면서 '사용자 동의'를 받은 후로 앱 측정을 지연 시켜야 돼서,
GoogleMobileAdsSettings.asset에서 'Delay app measurement'를 체크해 줘야 되는 것 같다.
[참조] https://developers.google.com/admob/unity/privacy/gdpr?hl=ko
----
이번에 GDPR 동의 관련 작업을 하게 되면서 이런저런 엮이는 것들이 많았는데..
문제가 됐던 이슈 리스트 정리해 봄
1) GDPR 동의를 추가하고나니 iOS의 IDFA(AppTrackingTransparency) 동의와 내용이 겹친다고
"앞에서 거부했는데 왜 뒤에 또 동의창을 뛰우냐~" 라는 내용으로 iOS 검수 리젝 됨.
- 참고로 iOS ATT(IDFA)와 GDPR의 순서는 GDPR먼저 뛰우고 ATT(IDFA)를 판단하라고 함
https://developers.google.com/admob/android/privacy/gdpr?hl=ko
그래서 확인해보니, Admob(GoogleMobileAds)에서 GDPR과 IDFA 둘 다 제공하고 Admob에서 진행하면 알아서 유기적으로 처리해준다.
- [EEA( European Economic Area )인 경우] iOS에서 GDPR 동의 거부하면 IDFA 동의창 안 뜸, GDPR 동의하면 IDFA 동의 창 뜸.
- [EEA( European Economic Area )가 아닌 경우] iOS에서 IDFA 안내 및 동의창만 뜸.
[참고] https://blueasa.tistory.com/2789
[검수리젝] GDPR/IDFA(ATT) 관련 검수 리젝
Unity 2021.3.35f1 GoogleMobileAds 8.7.0 ---- GDPR 동의 로직 추가하고 iOS 검수 넣었더니 리젝 됐다. 사유는 대충 정리하면, GDPR 팝업에서 '거부'를 했는데, 같은 이슈인 'IDFA(AppTrackingTransparency)' 동의 여부를
blueasa.tistory.com
2) AppsFlyer도 이번에 버전업(6.13.0) 해서 보니, GDPR 관련 설정 옵션(AppsFlyerConsent 클래스가 생김)이 생겼다.
아래 예시와 같이 GDPR 정보 셋팅 후, AppsFlyer를 Initialize 하라고 한다.
// If the user is subject to GDPR - collect the consent data
// or retrieve it from the storage
...
// Set the consent data to the SDK:
AppsFlyerConsent consent = AppsFlyerConsent.ForGDPRUser(true, true);
AppsFlyer.setConsentData(consent);
AppsFlyer.startSDK();
자세한 내용은 아래 AppsFlyer 문서를 참고하자.
[참고] https://ko.dev.appsflyer.com/hc/docs/basicintegration
연동
AppsFlyerObject 프리팹을 사용하거나 수동으로 플러그인을 초기화할 수 있습니다. AppsFlyerObject.prefabManual의 integrationCollect IDFA를 ATTrackingManagerSending SKAN 포스트백과 함께 사용하여 AppsflyerMacOS initializat
ko.dev.appsflyer.com
3) 위 AppsFlyer 연동하려고 보니 아래와 같이 GDPR 일 때, 넘겨주는 매개변수 2개가 보인다.
- hasConsentForDataUsage : GDPR 동의 상태
- hasConsentForAdsPersonaliation : 광고 개인화 동의 상태
public static AppsFlyerConsent ForGDPRUser(bool hasConsentForDataUsage, bool hasConsentForAdsPersonalization)
{
return new AppsFlyerConsent(true, hasConsentForDataUsage, hasConsentForAdsPersonalization);
}
public static AppsFlyerConsent ForNonGDPRUser()
{
return new AppsFlyerConsent(false, false, false);
}
- hasConsentForDataUsage는 GDPR 동의 상태라서 Admob에도 정보가 있어서 받아오면 되겠는데,
- hasConsentForAdsPersonaliation은 GDPR 동의 창 세부 상태에서 광고 개인화 동의를 체크를 하는데 정보를 어떻게 받는지 몰라서 이리저리 찾아보다가 IABTCF_VendorConsents가 광고 개인화 관련 동의 값이란 걸 알게 됐다.
[참고] https://stackoverflow.com/questions/69307205/mandatory-consent-for-admob-user-messaging-platform
Mandatory Consent for Admob User Messaging Platform
I switched from the deprecated GDPR Consent Library to the new User Messaging Platform, and used the code as stated in the documentation. I noticed that when the user clicks on Manage Options then
stackoverflow.com
관련해서 잘 정리해 놓은 글을 찾아서 블로그에도 올려뒀다.
[링크] https://blueasa.tistory.com/2791
[펌] GDPR에 대해 알아보자 (Feat 애드몹)
1 얘들아 안녕, 다들 개발 잘 하고 있어? 저번 달에 GDPR 구현하다가 막혀서 여기다 글을 올렸었는데, 다행히 이젠 해결한 것 같아. 그 당시 해결하면 팁 남겨달라던 친구의 댓글이 기억나서 다시
blueasa.tistory.com
위의 링크글에서 동의 정보 관련에 필요한 CurrentGDPR 클래스 설명이 잘 돼 있다.
원래 CurrentGDPR 클래스는 GoogleMobileAds 8.6.0 이전 버전 기준으로 만들어져서,
GoogleMobileAds 8.7.0에 추가된 ApplicationPreferences를 사용해서 OS에 따라 분기하지 않아도 되도록 수정해서 아래 올려둔다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using GoogleMobileAds.Api;
public static class CurrentGDPR
{
private static bool _isGdprOn;
private static string _purposeConsent, _vendorConsent, _vendorLi, _purposeLi, _partnerConsent;
static CurrentGDPR()
{
InitData();
}
public static void InitData()
{
int gdprNum = 0;
// [GoogleMobileAds 8.7.0]에서 ApplicationPreferences 추가됨
// [참고] https://stackoverflow.com/questions/77838024/admob-gdpr-ump-issue-empty-iab-tcf-strings-on-android-after-user-consent
// [참고] https://developers.google.com/admob/android/privacy/gdpr?hl=ko
gdprNum = ApplicationPreferences.GetInt("IABTCF_gdprApplies");
_purposeConsent = ApplicationPreferences.GetString("IABTCF_PurposeConsents");
_vendorConsent = ApplicationPreferences.GetString("IABTCF_VendorConsents");
_vendorLi = ApplicationPreferences.GetString("IABTCF_VendorLegitimateInterests");
_purposeLi = ApplicationPreferences.GetString("IABTCF_PurposeLegitimateInterests");
_partnerConsent = ApplicationPreferences.GetString("IABTCF_AddtlConsent");
#region [GoogleMobileAds 8.6.0 이전] 버전
//#if UNITY_EDITOR // 에디터에서는 자바 호출이 에러나서 에외처리
// gdprNum = 1;
// _purposeConsent = "0000000000";
// _vendorConsent = "0000000000";
// _vendorLi = "";
// _purposeLi = "";
// _partnerConsent = "";
//#elif UNITY_ANDROID
// AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
// AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
// AndroidJavaClass preferenceManagerClass = new AndroidJavaClass("android.preference.PreferenceManager");
// AndroidJavaObject sharedPreferences =
// preferenceManagerClass.CallStatic<AndroidJavaObject>("getDefaultSharedPreferences", currentActivity);
// gdprNum = sharedPreferences.Call<int>("getInt", "IABTCF_gdprApplies", 0);
// _purposeConsent = sharedPreferences.Call<string>("getString", "IABTCF_PurposeConsents", "");
// _vendorConsent = sharedPreferences.Call<string>("getString", "IABTCF_VendorConsents", "");
// _vendorLi = sharedPreferences.Call<string>("getString", "IABTCF_VendorLegitimateInterests", "");
// _purposeLi = sharedPreferences.Call<string>("getString", "IABTCF_PurposeLegitimateInterests", "");
// _partnerConsent = sharedPreferences.Call<string>("getString", "IABTCF_AddtlConsent", "");
//#elif UNITY_IOS
// gdprNum = PlayerPrefs.GetInt("IABTCF_gdprApplies", 0);
// _purposeConsent = PlayerPrefs.GetString("IABTCF_PurposeConsents", "");
// _vendorConsent = PlayerPrefs.GetString("IABTCF_VendorConsents", "");
// _vendorLi = PlayerPrefs.GetString("IABTCF_VendorLegitimateInterests", "");
// _purposeLi = PlayerPrefs.GetString("IABTCF_PurposeLegitimateInterests", "");
// _partnerConsent = PlayerPrefs.GetString("IABTCF_AddtlConsent", "");
//#endif
#endregion
// 0 이면 아예 GDPR 대상이 아님. 1이어야 GDPR
if (gdprNum == 1)
_isGdprOn = true;
else
_isGdprOn = false;
Debug.Log("GDPR을 띄우는가? " + _isGdprOn);
Debug.Log("광고에 필요한 권한 동의: " + _purposeConsent);
Debug.Log("광고에 필요한 적법관심(광고 개인화?) 동의: " + _vendorConsent);
Debug.Log("구글에 동의처리가 되어있는가?: " + _vendorLi);
Debug.Log("구글에 적법관심(광고 개인화?) 처리 여부: " + _purposeLi);
Debug.Log("파트너 네트워크 여부: " + _partnerConsent);
}
/// <summary>
/// GDPR을 띄워야 할 유저인지(EEA = 유럽 + 영국) 리턴
/// </summary>
/// <returns>EEA 여부</returns>
public static bool IsGDPR()
{
return _isGdprOn;
}
/// <summary>
/// 광고에 필요한 권한 동의 여부
/// </summary>
/// <returns>동의 여부</returns>
public static bool IsPurposeConsents()
{
// Example value:
// 동의 : "1111111111"
// 비동의 : "0"
if (true == string.IsNullOrEmpty(_purposeConsent))
{
return false;
}
Debug.LogFormat("[IsPurposeConsents] {0}", HasAttribute(_purposeConsent, 1));
// Purposes are zero-indexed. Index 0 contains information about Purpose 1.
// _purposeConsent[0]에 1(동의)이 있는지 체크
return HasAttribute(_purposeConsent, 1);
}
/// <summary>
/// 광고에 필요한 적법관심(광고 개인화?) 동의 여부
/// </summary>
/// <returns>동의 여부</returns>
public static bool IsVendorConsents()
{
if (true == string.IsNullOrEmpty(_vendorConsent))
{
return false;
}
// _vendorConsent[0]에 1(동의)이 있는지 체크
return HasAttribute(_vendorConsent, 1);
}
// 광고가 보여지는지 여부 리턴
public static bool CanAdShow()
{
int googleId = 755;
bool hasGoogleVendorConsent = HasAttribute(_vendorConsent, googleId);
bool hasGoogleVendorLi = HasAttribute(_vendorLi, googleId);
// 광고 가능 - 비개인화 광고
// return HasConsentFor(new List<int> { 1 }, _purposeConsent, hasGoogleVendorConsent)
// && HasConsentOrLegitimateInterestFor(new List<int> { 2, 7, 9, 10 },
// _purposeConsent, _purposeLi, hasGoogleVendorConsent, hasGoogleVendorLi);
// 광고 가능 - 제한적인 광고 - 1에 대한 권한이 없어도 됨 ㅇㅇ
return HasConsentOrLegitimateInterestFor(new List<int> { 2, 7, 9, 10 },
_purposeConsent, _purposeLi, hasGoogleVendorConsent, hasGoogleVendorLi);
}
// 개인화 광고가 보여지는지 여부 리턴
public static bool CanShowPersonalizedAds()
{
int googleId = 755;
bool hasGoogleVendorConsent = HasAttribute(_vendorConsent, googleId);
bool hasGoogleVendorLi = HasAttribute(_vendorLi, googleId);
return HasConsentFor(new List<int> { 1, 3, 4 }, _purposeConsent, hasGoogleVendorConsent)
&& HasConsentOrLegitimateInterestFor(new List<int> { 2, 7, 9, 10 },
_purposeConsent, _purposeLi, hasGoogleVendorConsent, hasGoogleVendorLi);
}
public static bool IsPartnerConsent(string partnerID) // 파트너 권한 있는지 확인
{
return _partnerConsent.Contains(partnerID);
}
// 이진 문자열의 "index" 위치에 "1"이 있는지 확인합니다(1 기반).
private static bool HasAttribute(string input, int index)
{
return (index <= input.Length) && (input[index - 1] == '1');
}
// 목적 목록에 대한 동의가 주어졌는지 확인합니다.
private static bool HasConsentFor(List<int> purposes, string purposeConsent, bool hasVendorConsent)
{
return purposes.All(p => HasAttribute(purposeConsent, p)) && hasVendorConsent;
}
// 목적 목록에 대한 공급자의 동의 또는 정당한 이익이 있는지 확인합니다.
private static bool HasConsentOrLegitimateInterestFor(List<int> purposes, string purposeConsent, string purposeLI, bool hasVendorConsent, bool hasVendorLI)
{
return purposes.All(p =>
(HasAttribute(purposeLI, p) && hasVendorLI) ||
(HasAttribute(purposeConsent, p) && hasVendorConsent));
}
}
[결론]
여기서 내가 필요했던
AppsFlyer에 넘기려던 정보는 CurrentGDPR.CanShowPersonalizedAds() 였다.
CurrentGDPR .IsGDPR()도 써도 될 것 같긴한데..
GoogleMobileAds에서 ConsentInformation.ConsentStatus를 사용중인데 뭘 써야될지는 좀 테스트 봐야 될 것 같다.
[초기화 순서]
1. GoogleMobileAds -
1.1. EEA 일 경우
1.1.1. GDPR 동의 진행
1.1.1.1. GDPR 동의 하면, (iOS만)IDFA 동의 진행(GDPR 동의 요청하면 IDFA도 필요하면 자동으로 진행 된다.)
1.1.1.2. GDPR 동의 안하면, IDFA 동의 Skip(IDFA도 거부로 판단)
1.2. EEA가 아닐 경우
1.2.1. GDPR 동의 진행 안함
1.2.2. (iOS만) IDFA 동의 진행
1.3. GoogleMobileAds 초기화 진행
2. GDPR / (iOS만)IDFA 동의 진행 완료 후, AppsFlyer 초기화 진행
2.1. AppsFlyerConsent.ForGDPRUser() || AppsFlyerConsent.ForNonGDPRUser() 셋팅
2.2. AppsFlyer.startSDK() 실행
[추가] GDPR 동의 여부 읽는 방법(위 소스에서 IsPurposeConsents() 함수 참조 )
GDPR 동의가 수집된 후에는 TCF v2 사양 에 따라 로컬 저장소에서 동의 여부를 읽을 수 있습니다. IABTCF_PurposeConsents 키는 각 TCF 목적 에 대한 동의를 나타냅니다.
다음 코드 스니펫은 목적 1에 대한 동의를 확인하는 방법을 보여줍니다.
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
// Example value: "1111111111"
String purposeConsents = sharedPref.getString("IABTCF_PurposeConsents", "");
// Purposes are zero-indexed. Index 0 contains information about Purpose 1.
if (!purposeConsents.isEmpty()) {
String purposeOneString = purposeConsents.charAt(0);
boolean hasConsentForPurposeOne = purposeOneString.equals("1");
}
[출처] https://developers.google.com/admob/android/privacy/gdpr?hl=ko
GDPR IAB 지원 | Android | Google for Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. GDPR IAB 지원 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 가이드에서는 UMP SDK의 일부로 GDPR IAB TCF
developers.google.com
[참조] https://developers.google.com/admob/unity/privacy/gdpr?hl=ko
[참조] https://stackoverflow.com/questions/69307205/mandatory-consent-for-admob-user-messaging-platform
[참조] https://stackoverflow.com/questions/77838024/admob-gdpr-ump-issue-empty-iab-tcf-strings-on-android-after-user-consent
[참조] https://stackoverflow.com/questions/69307205/mandatory-consent-for-admob-user-messaging-platform
[참조] https://gall.dcinside.com/mgallery/board/view/?id=game_dev&no=150987
[참조] https://developers.google.com/admob/unity/privacy/gdpr?hl=ko
[참조] https://ko.dev.appsflyer.com/hc/docs/basicintegration