서버 간 SKAdNetwork 3 구현 가이드

문서
최신 버전의 SKAdNetwork를 구현하려면 SKAdNetwork 4 구현 가이드를 참조하세요.

SKAdNetwork 개요

SKAdNetwork는 iOS에서 앱 인스톨 광고 캠페인을 개인정보 보호 친화적으로 측정할 수 있도록 Apple에서 제공하는 프레임워크입니다(자세히 알아보기). 이 프레임워크는 사용자의 식별 정보를 침해하지 않고 앱 인스톨 캠페인의 전환율을 측정할 수 있도록 도와줍니다.

SKAdNetwork는 어떻게 작동하나요?

SKAdNetwork에서 어트리뷰션 프로세스는 앱스토어가 Apple의 서버를 통해 수행합니다. 그런 다음 어트리뷰션 정보는 사용자 식별자 및 일시적 정보와 분리되어 네트워크로 전송됩니다.

Screen_Shot_2020-09-16_at_18.57.56.png

광고를 클릭하고 스토어가 열리면 퍼블리싱 앱과 네트워크는 네트워크, 퍼블리셔, 캠페인 ID와 같은 몇 가지 기본 정보를 제공합니다. 광고주 앱이 실행되고 SKAdNetwork에 등록된 경우, 디바이스는 전환 성공 알림을 네트워크에 전송합니다. 이 알림은 광고 앱이 보고할 수 있는 전환 값과 함께 첨부된 값을 보고합니다.

이 알림은 첫 실행 후 최소 24시간 후에 전송되며, 기기 또는 사용자 식별 정보는 포함되지 않습니다.

또한, 앱 스토어는 광고된 앱이 원래 광고와 퍼블리셔에 대해 알지 못하도록 프로세스를 진행합니다. 이렇게 하면 네트워크는 설치하는 사용자에 대해 아무것도 모른 채 설치에 대한 알림을 받게 됩니다.

SKAdNetwork를 사용할 때 기대할 수 있는 것은 무엇인가요?

SKAdNetwork에는 몇 가지 주요 장점이 있습니다. 다음과 같은 정보를 모두 제공합니다:

  • 동의 없이 작동하는 라스트 클릭 어트리뷰션
  • 소스, 캠페인, 퍼블리셔 분석 정보
  • 인스톨 후 전환 값(최대 64개의 개별 값)
  • 인스톨 검증을 위한 암호화 서명

그러나 현재 형태의 SKAdNetwork는 기본 기능만 제공하므로, 제대로 작동하려면 여러 주체 간의 신중한 구현과 조율이 필요합니다.

현재 SKAdNetwork의 몇 가지 제한 사항은 다음과 같습니다:

  • 사용자 수준 데이터 없음
  • 뷰스루 어트리뷰션 없음
  • 제한된 범위의 전환 값:
    • 설치/재설치당 Singular 전환 이벤트 1개
    • 최대 64개의 전환 값(6비트)
  • 제한된 세분성:
    • 최대 100개의 캠페인 값
    • 광고 그룹 및 크리에이티브 레벨에 대한 표시 없음
  • LTV 없음 / 긴 코호트
  • 사기 노출:
    • 전환 값 자체에 서명되지 않음(조작 가능)
    • 포스트백은 중복될 수 있습니다.

이러한 문제를 극복하기 위해 Singular는 SKAdNetwork 구현을 위한 공개 표준을 발표하고, SKAdNEtwork를 탐색하는 데 도움이 되는 여러 블로그 게시물을 공개했습니다:

SKAdNetwork S2S 구현

Singular SKAdNetwork 솔루션은 다음과 같은 구성 요소로 이루어져 있습니다:

  • SKAdNetwork를 구현하기 위한 클라이언트 측 코드
  • 모든 네트워크의 포스트백 검증 및 어그리게이션
  • 사기 방지:
    • 서명 유효성 검사 및 트랜잭션 ID 중복 제거.
    • 서명되지 않은 파라미터(전환 값 및 지오데이터)를 검증하기 위한 네트워크 보안 설정.
  • 전환 가치 관리: Singular의 대시보드에서 동적으로 구성할 수 있는 기능을 제공하여 설치 후 활동을 SKAdNetwork 전환 가치로 인코딩할 수 있습니다.
  • 리포팅: 제한된 SKAdNetwork 캠페인 ID를 변환하고 더 많은 마케팅 파라미터와 세분화로 데이터를 보강합니다.
  • 파트너 포스트백: 최적화에 중요한 전환 가치를 이벤트 및 구매으로 디코딩하여 SKAdNetwork 포스트백을 전송할 수 있습니다.

서버 간 Singular 연동을 사용하는 고객의 경우, SKAdNetwork 구현은 크게 두 부분으로 구성됩니다:

  1. SKAdNetwork 클라이언트 측 구현: 이 부분은 앱을 SKAdNetwork에 등록하고 SKAdNetwork 전환 가치를 지능적으로 관리하는 데 매우 중요합니다. 즉, 이를 구현하면 SKAdNetwork 어트리뷰션 및 관련 인스톨 후 활동을 기반으로 캠페인을 최적화할 수 있습니다.
  2. S2S 연동 업데이트(선택 사항): 이 부분은 클라이언트 측 구현을 검증하고 문제를 해결하는 데 중요합니다. 현재 Singular의 S2S 세션과 이벤트 엔드포인트를 통해 Singular로 전송되는 이벤트와 세션을 SKAdNetwork 데이터(전환 값 및 업데이트 타임스탬프)로 보강함으로써, 앱 측에서 구현이 제대로 수행되었는지 검증할 수 있습니다.

SKAdNetwork 클라이언트 측 구현

Singular는 SKAdNetwork 등록 및 전환 값 관리를 지원하는 코드 샘플을 제공합니다. 이 코드 샘플은 다음과 같은 부분을 담당합니다:

  1. SKAdNetwork 지원 및 등록
  2. 전환 가치 관리:
    • 이 코드는 Singular의 엔드포인트와 동기식으로 통신하여 설정된 전환 모델에 따라 다음 전환 값을 수신합니다. 이벤트/세션/구매을 보고하고, 이에 대한 응답으로 다음 전환 값을 가져오는데, 이는 Singular의 대시보드에서 측정하도록 구성된 설치 후 활동을 나타내는 인코딩된 숫자입니다.
    • 또한 이 코드는 유효성 검사 및 다음 전환 값 계산에 사용되는 SKAdnetwork 메타데이터를 수집합니다:
      • 기본 SKAdNetwork 프레임워크에 대한 첫 번째 호출 타임스탬프
      • 기본 SKAdNetwork 프레임워크에 대한 마지막 호출 타임스탬프
      • 마지막으로 업데이트된 전환 값

S2S 연동 업데이트

필수

활성 전환 모델이 있으면 S2S 엔드포인트는 클라이언트 측 코드에서 업데이트할 다음 값을 포함하는 "conversion_value"라는 새 int 필드를 반환하기 시작합니다.

선택 사항

연동을 검증하고 잠재적인 구현 문제를 해결하려면 코드 샘플을 사용하여 현재 S2S 연동을 확장하는 것이 좋습니다. Singular의 S2S 이벤트 및 세션 엔드포인트는 이미 다음과 같은 추가 SKAdNetwork 파라미터 가져오기를 지원합니다:

  • 최근 전환 값
  • 기본 SKAdNetwork 프레임워크에 대한 호출의 마지막 타임스탬프
  • 기본 SKAdNetwork 프레임워크에 대한 호출의 첫 번째 타임스탬프

연동 흐름

Screen_Shot_2020-09-16_at_18.59.13.png

위의 다이어그램은 S2S 고객에 대한 SKAdNetwork의 흐름을 보여줍니다:

  1. 첫째, 앱의 코드는 앱에서 발생하는 이벤트/세션/구매 이벤트를 기반으로 최신 전환 값을 동기화하기 위해 Singular 서버(SKAdNetwork 전용 엔드포인트를 통해)와 통신하고, 이 값으로 SKAdNetwork 프레임워크를 업데이트합니다.
  2. 둘째, 앱은 기존 이벤트와 세션을 SKAdNetwork 데이터로 보강하여 나중에 유효성 검사에 사용합니다.
  3. 앱이 새로운 전환 값으로 SKAdNetwork 프레임워크 업데이트를 완료하고 SKAdNetwork 타이머가 만료되면, SKAdNetwork 포스트백이 네트워크에 전송됩니다.
  4. 네트워크는 보안 설정 또는 일반 설정을 통해 이를 Singular로 전달합니다.
  5. Singular는 포스트백을 처리합니다:
    • 서명 검증
    • 구성된 전환 모델을 기반으로 전환 값을 디코딩합니다.
    • 파트너와의 연동 및 SKAdNetwork 내부 캠페인 ID를 기반으로 더 많은 외부 마케팅 파라미터로 포스트백을 보강합니다.
    • 디코딩된 포스트백을 BI 및 파트너에게 전송하기

중요한 점은 인스톨 및 디코딩된 이벤트를 포함한 SKAdNetwork 정보는 기존 데이터 세트와 혼용되지 않도록 다른 보고서/API/ETL 테이블 및 포스트백 세트를 통해 액세스할 수 있다는 것입니다. 이는 특히 다음 몇 주 동안 기존 캠페인 활동과 SKAdNetwork를 나란히 측정하고 테스트할 수 있도록 하는 데 중요합니다.

SKAdNetwork 네이티브 iOS 코드 샘플

SKAdNetwork 인터페이스

이 인터페이스에는 다음과 같은 SKAdNetwork 구성 요소가 포함되어 있습니다:

SKAdNetwork 등록:

  • 이 메서드는 앱을 SKAdNetwork에 등록하는 작업을 담당합니다. 기본 Apple API 메서드는 디바이스에 해당 앱에 대한 어트리뷰션 데이터가 있는 경우 알림을 생성하고 24시간 타이머를 시작합니다.
  • 디바이스는 타이머가 만료된 후 0~24시간 이내에 광고 네트워크의 포스트백 엔드포인트로 인스톨 알림을 전송합니다.
  • 전환 값을 이전 값보다 더 큰 값으로 업데이트하면 첫 번째 타이머가 24시간 간격으로 재설정됩니다(자세한 내용은 여기를 참조하세요).

전환 값 관리 및 업데이트:

  • 전환 값은 아래 방법으로 캡처한 디바이스의 설치 후 활동과 사용자가 동적으로 구성할 수 있는 선택한 전환 모델을 기반으로 계산됩니다.
  • 이 섹션의 메서드는 선택한 전환 모델과 보고된 설치 후 활동에 따라 Singular의 엔드포인트에서 다음 전환 값을 가져옵니다(위 문서 참조).
  • 아래 방법은 다음과 같은 인스톨 후 활동을 기반으로 전환 값을 업데이트합니다:
      • 세션: SKAdNetwork를 사용한 사용자의 리텐션 측정에 중요합니다.
      • 전환 이벤트: SKAdNetwork를 통한 인스톨 후 전환 이벤트 측정에 중요합니다.
      • 구매 이벤트: SKAdNetwork를 통한 구매 측정에 필수적입니다.
//  SKANSnippet.h


#import <Foundation/Foundation.h>

@interface
SKANSnippet : NSObject

// Register for SKAdNetwork attribution.

// You should call this method as soon as possible once app is launched

+ (void)registerAppForAdNetworkAttribution;

// To track retention and cohorts you need to call this method after 

// each session. It reports the session details and updates the conversion 

// value due to this session if needed. The conversion value will be 

// updated only if the new value is greater than the previous value.

// The callback passed to the method is optional, you can use it to 

// run code once the conversion value is updated. 

+ (void)updateConversionValueAsync:(void(^)(int, NSError*))handler;

// To track conversion events with SKAdNetwork you need to call this 

// method after each event. It reports the event details and updates the 

// conversion value due to this event if needed.  

// The callback passed to the method is optional, you can use it to 

// run code once the conversion value is updated.

+ (void)updateConversionValueAsync:(NSString*)eventName
  withCompletionHandler:(void(^)(int, NSError*))handler;

// To track revenue with SKAdNetwork you need to call this 

// method before each  revenue event .

// It will update the total revenue, so when you call 'updateConversionValueAsync', 

// the new conversion value will be determined according to the total amount of revenue.

// Note:

// 1. Call this method before calling 'updateConversionValueAsync' to 

// make sure that revenue is updated.

// 2. In case of retrying an event, avoid calling this method

// so the same revenue will not count twice.

+ (void)updateRevenue:(double)amount andCurrency:(NSString*)currency;

// Gets the current conversion value (nil if none)

+ (NSNumber *)getConversionValue;

 @end

SKAdNetwork 인터페이스 구현

//  SKANSnippet.m


#import "SKANSnippet.h"
#import <StoreKit/SKAdNetwork.h>

#define SESSION_EVENT_NAME @”__SESSION__” 
#define SINGULAR_API_URL @"https://sdk-api-v1.singular.net/api/v1/conversion_value"

// Keys for UserDefaults storage

#define CONVERSION_VALUE_KEY @"skan_conversion_value"
#define FIRST_SKAN_CALL_TIMESTAMP @"skan_first_call_to_skadnetwork_timestamp"
#define LAST_SKAN_CALL_TIMESTAMP @"skan_last_call_to_skadnetwork_timestamp"
#define TOTAL_REVENUE_BY_CURRENCY_KEY @"skan_total_revenue_by_currency"
#define SECONDS_PER_DAY 86400

@implementation
SKANSnippet

+ (void)registerAppForAdNetworkAttribution {
    if ([SKANSnippet getFirstSkanCallTimestamp] != 0) {
        return;
    }

    if (@available(iOS 11.3, *) ) {
        [SKAdNetwork registerAppForAdNetworkAttribution];
        [SKANSnippet setFirstSkanCallTimestamp];
        [SKANSnippet setLastSkanCallTimestamp];
    }
}

+ (void)updateConversionValueAsync:(void(^)(int, NSError*))handler {
    [SKANSnippet updateConversionValueAsync:SESSION_EVENT_NAME withCompletionHandler:handler];
}

+ (void)updateConversionValueAsync:(NSString*)eventName withCompletionHandler:(void(^)(int, NSError*))handler {
    if (@available(iOS 14, *)) {
        if ([SKANSnippet isSkanUpdateWindowOver]) {
            return;
        }
        
        [SKANSnippet getConversionValueFromServer:eventName withCompletionHandler:handler];
    }
}

+ (void)updateRevenue:(double)amount andCurrency:(NSString*)currency {
    if (@available(iOS 14, *)) {
        
        // Update total revenues

        if (amount != 0 && currency) {
            NSMutableDictionary* revenues = [[SKANSnippet getTotalRevenue] mutableCopy];
            NSNumber* currentRevenue = 0;
            
            if ([revenues objectForKey:currency]) {
                currentRevenue = [revenues objectForKey:currency];
            }
            
            currentRevenue = @([currentRevenue floatValue] + amount);
            [revenues setObject:currentRevenue forKey:currency];
            
            [SKANSnippet setTotalRevenue:revenues];
        }
    }
}

+ (NSNumber *)getConversionValue {
    NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
    if (![userDefaults objectForKey:CONVERSION_VALUE_KEY]) {
        return nil;
    }
    return @([userDefaults integerForKey:CONVERSION_VALUE_KEY]);
}

+ (void)getConversionValueFromServer:(NSString*)eventName withCompletionHandler:(void(^)(int, NSError*))handler {
    if (!lockObject) {
        lockObject = [NSLock new];
    }
    
    // Making the lock async so it will not freeze the calling thread

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
        
        [lockObject lock];
        
        NSURLComponents *components = [NSURLComponents componentsWithString:SINGULAR_API_URL];
        
        NSString* bundleIdentifier = @"";
        
        components.queryItems = @[
            [NSURLQueryItem queryItemWithName:@"a" value:@""],
            [NSURLQueryItem queryItemWithName:@"i" value:bundleIdentifier],
            [NSURLQueryItem queryItemWithName:@"app_v" value:@""],
            [NSURLQueryItem queryItemWithName:@"n" value:eventName],
            [NSURLQueryItem queryItemWithName:@"p" value:@"iOS"],
            [NSURLQueryItem queryItemWithName:@"idfv" value:@""],
            [NSURLQueryItem queryItemWithName:@"idfa" value:@""],
            [NSURLQueryItem queryItemWithName:@"conversion_value" 
 value:[[SKANSnippet getConversionValue] stringValue]],
            [NSURLQueryItem queryItemWithName:@"total_revenue_by_currency" 
 value:[SKANSnippet dictionaryToJsonString:[SKANSnippet getTotalRevenue]]],
            [NSURLQueryItem queryItemWithName:@"first_call_to_skadnetwork_timestamp" 
 value:[NSString stringWithFormat:@"%ld", [SKANSnippet getFirstSkanCallTimestamp]]],
            [NSURLQueryItem queryItemWithName:@"last_call_to_skadnetwork_timestamp" 
 value:[NSString stringWithFormat:@"%ld", [SKANSnippet getLastSkanCallTimestamp]]],
        ];
        
        [[[NSURLSession sharedSession] dataTaskWithURL:components.URL
                                     completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            if (error) {
                [lockObject unlock];
                if (handler) {
                    handler(-1, error);
                }
                return;
            }
            
            NSDictionary* parsedResponse = [SKANSnippet jsonDataToDictionary:data];
            
            if (!parsedResponse) {
                [lockObject unlock];
                if (handler) {
                    handler(-1, [NSError errorWithDomain:bundleIdentifier
                                                    code:0
                                                userInfo:@{NSLocalizedDescriptionKey:@"Failed parsing server response"}]);
                }
                return;
            }
            
            NSNumber *conversionValue = [parsedResponse objectForKey:@"conversion_value"];
            
            if (!conversionValue) {
                [lockObject unlock];
                NSString *status = [parsedResponse objectForKey:@"status"];
                
                if (!status || ![status isEqualToString:@"ok"]) {
                    if (handler) {
                    		NSString *reason = [parsedResponse objectForKey:@"reason"];
                        if (!reason) {
                            reason = @"Got error from server";
                        }
                        
                        handler(-1, [NSError errorWithDomain:bundleIdentifier
                                                        code:0
                                                    userInfo:@{NSLocalizedDescriptionKey:reason}]);
                    }
                }
                return;
            }
            
            NSNumber* currentValue = [SKANSnippet getConversionValue];
            if ([conversionValue intValue] <= [currentValue intValue]) {
                [lockObject unlock];
                return;
            }
            
            [SKANSnippet setConversionValue:[conversionValue intValue]];
            [SKANSnippet setLastSkanCallTimestamp];
            
            if (![SKANSnippet getFirstSkanCallTimestamp]) {
                [SKANSnippet setFirstSkanCallTimestamp];
            }
            
            [lockObject unlock];
            
            if (handler) {
                handler([conversionValue intValue], error);
            }
        }] resume];
    });
}

+ (BOOL)isSkanUpdateWindowOver {        
    NSInteger timeDiff = [SKANSnippet getCurrentUnixTimestamp] - 
      [SKANSnippet getLastSkanCallTimestamp];
    return SECONDS_PER_DAY <= timeDiff;
}

SKAdNetwork 메타데이터 및 유틸리티 메소드

이 섹션에서는 위의 구현에 사용되는 유틸리티를 구현합니다. 이러한 유틸리티는 다음 전환 값을 계산하는 데 필요한 메타데이터를 유지하고 저장하는 데 매우 중요합니다.

다음과 같은 유틸리티가 구현되어 있습니다:

사용자 기본값에 값 저장(및 검색):

  • 기본 SKAdNetwork API에 대한 첫 번째 호출 타임스탬프
  • 기본 SKAdNetwork API에 대한 마지막 호출 타임스탬프
  • 통화별 총 구매

서로 다른 표현 간의 값 변환:

  • 사전을 JSON 문자열로
  • JSON을 사전으로
+ (NSInteger)getFirstSkanCallTimestamp {
    NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
    return [userDefaults integerForKey:FIRST_SKAN_CALL_TIMESTAMP];
}

+ (NSInteger)getLastSkanCallTimestamp {
    NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
    return [userDefaults integerForKey:LAST_SKAN_CALL_TIMESTAMP];
}

+ (NSDictionary*)getTotalRevenue {
    NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
    if (![userDefaults objectForKey:TOTAL_REVENUE_BY_CURRENCY_KEY]) {
        return [NSDictionary new];
    }
    return [userDefaults objectForKey:TOTAL_REVENUE_BY_CURRENCY_KEY];
}

+ (NSInteger)getCurrentUnixTimestamp {
    return [[NSDate date]timeIntervalSince1970];
}

+ (void)setConversionValue:(int)value {
    if (@available(iOS 14.0, *)) {
        if (value <= [[SKANSnippet getConversionValue] intValue]) {
            return;
        }
        
        [SKAdNetwork updateConversionValue:value];
        
        NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
        [userDefaults setInteger:value forKey:CONVERSION_VALUE_KEY];
        [userDefaults synchronize];
    }
}

+ (void)setFirstSkanCallTimestamp {
    NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp] 
      forKey:FIRST_SKAN_CALL_TIMESTAMP];
    [userDefaults synchronize];
}

+ (void)setLastSkanCallTimestamp {
    NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp] 
      forKey:LAST_SKAN_CALL_TIMESTAMP];
    [userDefaults synchronize];
}

+ (void)setTotalRevenue:(NSDictionary *)values {
    NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setObject:values forKey:TOTAL_REVENUE_BY_CURRENCY_KEY];
    [userDefaults synchronize];
}

+ (NSString*)dictionaryToJSONString:(NSDictionary*)dictionary {
    if (!dictionary || [dictionary count] == 0){
        return @"{}";
    }
    
    NSError *error;
    NSData *JSONData = [NSJSONSerialization dataWithJSONObject:dictionary
      options:0
      error:&error];
    if (error || !JSONData) {
        return @"{}";
    }
    
    return [[NSString alloc] initWithData:JSONData 
      encoding:NSUTF8StringEncoding];
}

+ (NSDictionary*)JSONDataToDictionary:(NSData*)JSONData {
    if (!JSONData) {
        return nil;
    }

    NSError * error;
    NSDictionary * parsedData = [NSJSONSerialization 
      JSONObjectWithData:JSONData
      options:kNilOptions error:&error];
      
    if (error || !parsedData) {
       return nil;
    }
    return parsedData;
}

@end

S2S 연동 업데이트 - 구현(선택 사항)

다음 SKAdNetwork 메타데이터로 S2S 연동을 업데이트하세요(이 메타데이터는 SKAdNetwork 구현 검증을 위해 Singular에 보고되는 모든 세션 및 이벤트에 대해 전달되어야 합니다):

  • skan_conversion_value - 최신 전환 값
  • skan_first_call_timestamp - 기본 SKAdNetwork API에 대한 첫 번째 호출의 유닉스 타임스탬프
  • skan_last_call_timestamp - 기본 SKAdNetwork API에 대한 마지막 호출의 유닉스 타임스탬프

다음 코드 스니펫은 이러한 값을 추출하는 방법을 보여줍니다:

NSDictionary *values = @{
    @"skan_conversion_value":
      [[SKANSnippet getConversionValue] stringValue]],
    @"skan_first_call_timestamp": 
      [NSString stringWithFormat:@"%ld", [SKANSnippet getFirstSkanCallTimestamp]],
    @"skan_last_call_timestamp": 
      [NSString stringWithFormat:@"%ld", [SKANSnippet getLastSkanCallTimestamp]]
};

이제 이러한 파라미터를 서버 측으로 전송한 후, 서버 간 API 엔드포인트를 통해 전달할 수 있습니다. 자세한 내용은 서버 간 API 참조에서 이러한 파라미터를 검색하세요.

앱 라이프사이클 흐름 예시

// On app launch

[SKANSnippet registerAppForAdNetworkAttribution];

// After each session is handled

[SKANSnippet updateConversionValueAsync:handler];

// After handling non-revenue events

[SKANSnippet updateConversionValueAsync:@"event_name" 
  withCompletionHandler:handler];

// After handling revenue events

[SKANSnippet updateRevenue:15.53 andCurrency:@"KRW"];
[SKANSnippet updateConversionValueAsync:@"revenue_event_name" withCompletionHandler:handler];

코드 변경 로그

  • 2020년 10월 1일
    • 중요]수정: `getConversionValueFromServer`의 잠금 메커니즘이올바르게 초기화되지 않았습니다.
    • 개선: 서버 응답에서 실패 이유 반환
  • 2020/09/23
    • 중요]수정: `setConversionValue`가 이제 `updateConversionValue`를 호출합니다.
    • 개선: 서버에서 최신 전환값을 가져올 때 오류 처리개선
  • 2020/09/15
    • 변경됨 [중요]: 전환 값이 설정되지 않은 경우, `getConversionValue`가 반환하는 기본값은 null이 아닌 0입니다.
    • 개선되었습니다: 구매 보고 및 처리
    • 개선되었습니다: 다음 전환 값 비동기적으로 가져오기

구현 테스트

전환 관리

앱 흐름을 시뮬레이션하고 구현을 테스트하려면 전환 관리 프로덕션 엔드포인트(SINGULAR_API_URL) 를 다음 테스트 엔드포인트로 교체하세요.

제안된 테스트 흐름:

  • 앱 내에서 3개의 서로 다른 이벤트를 생성합니다.
    • 모든 것이 올바르게 구현되면
      • 각 이벤트가 발생할 때마다 updateConversionValueAsync가 호출되어야 합니다(기본 전환 값이 0으로 초기화되어 있는지 확인).
      • 테스트 엔드포인트는 앱에서 현재 전환 값을 수신하고 다음 값을 반환합니다.
  • 반환된 전환 값을 로깅하여 모든 것이 예상대로 작동하는지 확인합니다.
    • 테스트 엔드포인트는 0-2 사이의 값을 반환하도록 구현되어 있습니다.
    • 따라서 세 번째 호출 이후에는 빈 문자열을 반환하고 마지막 값이 최종 SKAdNetwork 전환 값으로 사용됩니다.