애드몹 GDPR 메시지 대응하기 (최종)

liko apps·2024년 1월 5일
1

iOS 앱 개발

목록 보기
2/2

이전에 iOS앱에 애드몹 GDPR 메시지 대응하기 글을 올렸었는데 (이전글)

그 사이 애드몹의 UserMessagingPlatform 업데이트가 있어서 다시 수정한 버전으로 올려봅니다.

GDPR이 뭔지,
애드몹의 GDPR 메시지는 어떻게 작성하는지는 이전글을 참고해주세요.

여기서는 구현 예시만 다시 정리해서 올려봅니다.

1. 구현 개요

  1. GDPR은 유럽의 개인 정보보호 방침에 따라 사용자 기기에 어떤 데이터를 추가로 저장하고 (쿠키 같은) 그리고 사용자의 데이터가 광고 네트워크사 또는 서버에 어떤 사용자 데이터가 전달 될 지 등등을 사용자의 동의를 받는 과정입니다.

    처음에는 UMPConsentForm.load { } 으로 form 을 로드한 뒤에 form.present { } 로 form을 노출해서 사용자에게 동의를 요청합니다.

  2. 만약 사용자가 이를 동의하지 않는 경우에는 (링크) 를 보면 목적 1에 따라 사용자의 기기에 데이터를 저장하지 않는다고 하면 광고 노출도 불가 합니다.

  3. 이에 따라 클라이언트 개발자는 사용자가 광고 노출에 동의하지 않는 경우에 어떻게 할지 결정이 필요합니다.

4.1. 사용자에게 반드시 광고를 노출해야 할 경우

  • 이 경우에는 링크 를 참고해서 GDPR의 어떤 항목을 사용자가 동의했는지 체크해서, 광고 노출이 가능한지, 개인화된 광고가 노출이 가능한지를 확인합니다.

  • 이 때 개인화된 광고 노출은 안되더라도 광고 노출이 가능하면 계속 진행.

  • 개인화된 광고 노출이 가능하다면 애플의 플랫폼의 ATTrackingManager.requestTrackingAuthorization { }을 활용해 추가로 광고 식별자를 사용자에게 요구 합니다. (사용자가 거절 할 경우에는 개인 식별 안된 광고가 노출)

  • 만약 광고 노출도 되지 않을 경우에는 애드몹의 UserMessagingPlatform 에 있는 UMPConsentForm.presentPrivacyOptionsForm { } 을 활용해서 사용자에게 다시 GDPR 동의를 요구합니다.

4.2. 사용자에게 광고를 노출하지 않아도 될 경우

  • 그냥 진행하면 됩니다.



2. 구현

아래에서 canShowGDPRForm() 를 먼저 호출해서 GDPR 동의를 받아야 하는지를 체크합니다.
GDPRForm을 보여줘야 할 경우 사용자에게 showGDPRForm() 를 호출해서 GDPR 동의를 요청합니다.

static func updateGDPRInfo(completionHandler: @escaping () -> Void) {
    guard UMPConsentInformation.sharedInstance.consentStatus == .unknown else {
        completionHandler()
        return
    }
    
    UMPConsentInformation.sharedInstance
        .requestConsentInfoUpdate(with: gdprParameters,
                                  completionHandler: { _ in
                                      completionHandler()
                                  })
}


static func canShowGDPRForm() async -> Bool {
    await withCheckedContinuation { continuation in
        updateGDPRInfo {
            let formStatus = UMPConsentInformation.sharedInstance.formStatus
            continuation.resume(returning: formStatus == UMPFormStatus.available)
        }
    }
}

static func showGDPRForm(completionHandler: @escaping (Bool) -> Void) async {
    guard await canShowGDPRForm() else {
        completionHandler(false)
        return
    }
    
    let formStatus = UMPConsentInformation.sharedInstance.formStatus
    
    guard formStatus == UMPFormStatus.available else {
        completionHandler(false)
        return
    }
    
    guard UMPConsentInformation.sharedInstance.consentStatus == UMPConsentStatus.required else {
        completionHandler(false)
        return
    }
    
    DispatchQueue.main.async {
        guard let vc = UIApplication.topViewController() else {
            completionHandler(false)
            return
        }
        
        UMPConsentForm.load { form, _ in
            guard let form = form else {
                completionHandler(false)
                return
            }
                            
            form.present(
                from: vc,
                completionHandler: { _ in
                    Log.d("UMPConsentInformation.sharedInstance.consentStatus after : \(UMPConsentInformation.sharedInstance.consentStatus)")
                    
                    if UMPConsentInformation.sharedInstance.consentStatus == UMPConsentStatus.obtained {
                        // App can start requesting ads.
                        
                        completionHandler(true)
                        return
                    }
                    
                    // Handle dismissal by reloading form.
                    Task.detached {
                        await self.showGDPRForm(completionHandler: completionHandler)
                    }
                })
        }
    }
}

GDPR 동의 요청 이후 또는 GDPRForm을 보여줄 수 없는 상태인 경우 (동의를 받았거나, 또는 거절당했거나, 동의가 필요없는 경우 (유럽 외 지역)) 에는 canShowAds() 를 호출합니다.
(아무튼 무조건 호출합니다.)

아래 코드는 https://stackoverflow.com/questions/65351543/how-to-implement-ump-sdk-correctly-for-eu-consent/68310602#68310602 에서 가져왔습니다.

아래 코드를 이용해서 canShowAds() 를 체크해서 광고를 보여줄 수 있는 상태인지를 체크합니다.
GDPR 사용지역이 아닌 경우, 또는 이미 동의를 받은 경우에 true 가 return 됩니다.

// Check if a binary string has a "1" at position "index" (1-based)
private static func hasAttribute(input: String, index: Int) -> Bool {
    return input.count >= index && String(Array(input)[index - 1]) == "1"
}

// Check if consent is given for a list of purposes
private static func hasConsentFor(_ purposes: [Int], _ purposeConsent: String, _ hasVendorConsent: Bool) -> Bool {
    return purposes.allSatisfy { i in hasAttribute(input: purposeConsent, index: i) } && hasVendorConsent
}

// Check if a vendor either has consent or legitimate interest for a list of purposes
private static func hasConsentOrLegitimateInterestFor(_ purposes: [Int], _ purposeConsent: String,
                                                      _ purposeLI: String, _ hasVendorConsent: Bool, _ hasVendorLI: Bool) -> Bool {
    return purposes.allSatisfy { i in
        (hasAttribute(input: purposeLI, index: i) && hasVendorLI) ||
            (hasAttribute(input: purposeConsent, index: i) && hasVendorConsent)
    }
}

static func canShowAds() -> Bool {
    if UMPConsentInformation.sharedInstance.consentStatus == .notRequired {
        return true
    }
    
    let settings = UserDefaults.standard
    
    // https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details
    // https://support.google.com/admob/answer/9760862?hl=en&ref_topic=9756841
    
    let purposeConsent = settings.string(forKey: "IABTCF_PurposeConsents") ?? ""
    let vendorConsent = settings.string(forKey: "IABTCF_VendorConsents") ?? ""
    let vendorLI = settings.string(forKey: "IABTCF_VendorLegitimateInterests") ?? ""
    let purposeLI = settings.string(forKey: "IABTCF_PurposeLegitimateInterests") ?? ""
    
    let googleId = 755
    let hasGoogleVendorConsent = hasAttribute(input: vendorConsent, index: googleId)
    let hasGoogleVendorLI = hasAttribute(input: vendorLI, index: googleId)
    
    // Minimum required for at least non-personalized ads
    return hasConsentFor([1], purposeConsent, hasGoogleVendorConsent)
        && hasConsentOrLegitimateInterestFor([2, 7, 9, 10], purposeConsent, purposeLI, hasGoogleVendorConsent, hasGoogleVendorLI)
}

static func canShowPersonalizedAds() -> Bool {
    if CoreConstants.isiOSAppOnMac
        || ATTrackingManager.trackingAuthorizationStatus == .denied
        || ATTrackingManager.trackingAuthorizationStatus == .restricted {
        return false
    }
    
    if UMPConsentInformation.sharedInstance.consentStatus == .notRequired {
        return true
    }
    
    let settings = UserDefaults.standard
    
    // https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details
    // https://support.google.com/admob/answer/9760862?hl=en&ref_topic=9756841
    
    // required for personalized ads
    let purposeConsent = settings.string(forKey: "IABTCF_PurposeConsents") ?? ""
    let vendorConsent = settings.string(forKey: "IABTCF_VendorConsents") ?? ""
    let vendorLI = settings.string(forKey: "IABTCF_VendorLegitimateInterests") ?? ""
    let purposeLI = settings.string(forKey: "IABTCF_PurposeLegitimateInterests") ?? ""
    
    let googleId = 755
    let hasGoogleVendorConsent = hasAttribute(input: vendorConsent, index: googleId)
    let hasGoogleVendorLI = hasAttribute(input: vendorLI, index: googleId)
    
    return hasConsentFor([1, 3, 4], purposeConsent, hasGoogleVendorConsent)
        && hasConsentOrLegitimateInterestFor([2, 7, 9, 10], purposeConsent, purposeLI, hasGoogleVendorConsent, hasGoogleVendorLI)
}

만약 canShowAds() 가 true 이면 계속 진행하고,
canShowAds() 가 false 인 경우에는 사용자에게 광고를 반드시 보여줘야 하는 경우에는 재동의를 받기 위한 화면을 띄웁니다. 사용자에게 기능을 제한 할 경우에는 적절히 기능 제한을 하면 됩니다.

재동의는 아래 코드를 이용해 받습니다.

static func reRequestGDPR(completionHandler: @escaping (Bool) -> Void) {
    let formStatus = UMPConsentInformation.sharedInstance.formStatus
    
    guard formStatus == UMPFormStatus.available else {
        completionHandler(false)
        return
    }

    DispatchQueue.main.async {
        guard let vc = UIApplication.topViewController() else {
            completionHandler(false)
            return
        }
        
        UMPConsentForm.presentPrivacyOptionsForm(from: vc) { error in
            if error != nil {
                completionHandler(false)
                return
            }
            
            if UMPConsentInformation.sharedInstance.consentStatus == UMPConsentStatus.obtained {
                // App can start requesting ads.
                
                completionHandler(true)
                return
            }
            
            // Handle dismissal by reloading form.
            DispatchQueue.main.async { [self] in
                self.reRequestGDPR(completionHandler: completionHandler)
            }
        }
    }
}

(제가 작성했지만 함수 이름은 맘에 안드네요. ㅠ)

아무튼 여기에 UMPConsentForm.presentPrivacyOptionsForm() 을 호출하면 사용자에게 재동의를 받을 수 있습니다. 이 함수는 Google UMP SDK (UserMessagingPlatform) 2.1.0 버전 2023년 7월 24일 출시 (글 쓰는 2024년 1월 5일 최신 버전)에 추가되었습니다.

(이전 글이 2023년 7월 10일에 작성했는데 ㅠ 이때는 없던 함수네요. 그래서 이전 글에는 reset()을 호출하라고 했었는데 reset()은 개발모드에서만 사용하고 프로덕션 모드에서는 사용하지 말라고 되어있습니다.)

암튼 이렇게 하면

사용자에게 동의를 구하고 -> 동의 상태 확인 및 광고 노출 가능 여부 확인 -> 광고 노출 불가 상태에서 다시 재동의를 요청. 까지의 일련의 과정을 이용할 수 있습니다.


// 추가로 이전에 애드몹 메시지에 동의 거부 버튼을 노출 안시키면 되지 않냐는 질문이 있었는데, 동의 버튼을 노출하지 않더라도 gdpr 동의 화면에서 옵션을 누르고 아무것도 선택하지 않고 confirm 할 경우 동의 거절과 동일한 동작을 하기 때문에 원천적으로 사용자의 동의를 거부하지 못하게는 할 수 없습니다.

// 끝.

profile
제주에서 iOS 앱을 만들고 있습니다. 개발관련 회고 / 정리 블로그

0개의 댓글