푸시 알림 데이터 기록하기
Braze SDK를 통해 푸시 알림 데이터를 기록하는 방법을 알아보세요.
Braze API를 사용한 데이터 기록(권장)
/users/track 엔드포인트를 호출하여 실시간으로 분석 데이터를 기록할 수 있습니다. 분석 데이터를 기록하려면 Braze 대시보드에서 braze_id 값을 전송하여 업데이트할 고객 프로필을 식별합니다.

수동으로 데이터 기록하기
페이로드의 세부 사항에 따라 FirebaseMessagingService.onMessageReceived 구현 또는 시작 액티비티 내에서 수동으로 분석 데이터를 기록할 수 있습니다. FirebaseMessagingService 서브클래스는 Android 시스템에 의해 플래그 지정되거나 종료되지 않도록 호출 후 9초 이내에 실행을 완료해야 합니다.
Braze API로 데이터 로깅(권장)
Braze API /users/track 엔드포인트를 사용하여 실시간으로 분석을 기록할 수 있습니다. 분석을 기록하려면 키-값 페어 필드에서 braze_id 값을 전송하여(아래 스크린샷 참조) 업데이트할 고객 프로필을 식별합니다.

수동으로 데이터 로깅하기
수동으로 로깅하려면 먼저 Xcode 내에서 워크스페이스를 구성한 다음, 분석 데이터를 생성, 저장, 검색해야 합니다. 이 과정에서 일부 커스텀 개발 작업이 필요합니다. 아래에 표시된 코드 스니펫이 이 작업을 수행하는 데 도움이 됩니다.
분석 데이터는 모바일 앱이 이후에 실행될 때까지 Braze로 전송되지 않는다는 점에 유의해야 합니다. 즉, 해제 설정에 따라 푸시 알림이 해제되는 시점과 모바일 앱이 실행되어 분석 데이터를 검색하는 시점 사이에 불확정한 시간 간격이 존재하는 경우가 많습니다. 이 시간 버퍼가 모든 사용 사례에 영향을 미치지는 않을 수 있지만, 이 영향을 고려하고 이 문제를 해결하기 위해 앱 열기를 포함하도록 사용자 여정을 조정해야 합니다.

1단계: Xcode 내에서 앱 그룹 구성하기
Xcode에서 App Groups 기능을 추가합니다. 앱에 워크스페이스가 없는 경우, 메인 앱 타겟의 기능으로 이동하여 App Groups를 켜고 + 추가 버튼을 클릭합니다. 그런 다음 앱의 번들 ID를 사용하여 워크스페이스를 만듭니다. 예를 들어, 앱의 번들 ID가 com.company.appname인 경우 워크스페이스 이름을 group.com.company.appname.xyz로 지정할 수 있습니다. 메인 앱 타겟과 콘텐츠 확장 타겟 모두에서 App Groups가 켜져 있는지 확인합니다.

2단계: 코드 스니펫 통합하기
다음 코드 스니펫은 커스텀 이벤트, 커스텀 속성, 사용자 속성을 저장하고 전송하는 방법에 대한 유용한 참조입니다. 이 가이드에서는 UserDefaults 용어로 설명하지만, 코드 표현은 헬퍼 파일 RemoteStorage 형식입니다. 사용자 속성을 전송하고 저장할 때 사용되는 추가 헬퍼 파일 UserAttributes와 EventName Dictionary가 있습니다.
커스텀 이벤트 저장하기
커스텀 이벤트를 저장하려면 처음부터 분석 데이터를 생성해야 합니다. 사전(dictionary)을 생성하고 메타데이터로 채운 다음 헬퍼 파일을 사용하여 데이터를 저장하면 됩니다.
- 이벤트 메타데이터로 사전을 초기화합니다
userDefaults를 초기화하여 이벤트 데이터를 검색하고 저장합니다- 기존 배열이 있으면 새 데이터를 기존 배열에 추가하고 저장합니다
- 기존 배열이 없으면 새 배열을
userDefaults에 저장합니다
func saveCustomEvent(with properties: [String: Any]? = nil) {
// 1
let customEventDictionary = Dictionary(eventName: "YOUR-EVENT-NAME", properties: properties)
// 2
let remoteStorage = RemoteStorage(storageType: .suite)
// 3
if var pendingEvents = remoteStorage.retrieve(forKey: .pendingCustomEvents) as? [[String: Any]] {
pendingEvents.append(contentsOf: [customEventDictionary])
remoteStorage.store(pendingEvents, forKey: .pendingCustomEvents)
} else {
// 4
remoteStorage.store([customEventDictionary], forKey: .pendingCustomEvents)
}
}
- (void)saveCustomEvent:(NSDictionary<NSString *, id> *)properties {
// 1
NSDictionary<NSString *, id> *customEventDictionary = [[NSDictionary alloc] initWithEventName:@"YOUR-EVENT-NAME" properties:properties];
// 2
RemoteStorage *remoteStorage = [[RemoteStorage alloc] initWithStorageType:StorageTypeSuite];
NSMutableArray *pendingEvents = [[remoteStorage retrieveForKey:RemoteStorageKeyPendingCustomEvents] mutableCopy];
// 3
if (pendingEvents) {
[pendingEvents addObject:customEventDictionary];
[remoteStorage store:pendingEvents forKey:RemoteStorageKeyPendingCustomAttributes];
} else {
// 4
[remoteStorage store:@[ customEventDictionary ] forKey:RemoteStorageKeyPendingCustomAttributes];
}
}
Braze로 커스텀 이벤트 전송하기
알림 콘텐츠 앱 확장에서 저장된 분석 데이터를 로깅하기에 가장 좋은 시점은 SDK가 초기화된 직후입니다. 대기 중인 이벤트를 순회하고, “Event Name” 키를 확인하여 Braze에서 적절한 값을 설정한 다음, 다음에 이 함수가 필요할 때를 위해 저장소를 비우면 됩니다.
- 대기 중인 이벤트 배열을 순회합니다
pendingEvents사전의 각 키-값 페어를 순회합니다- “Event Name” 키를 명시적으로 확인하여 값을 적절히 설정합니다
- 다른 모든 키-값은
properties사전에 추가됩니다 - 개별 커스텀 이벤트를 로깅합니다
- 저장소에서 모든 대기 중인 이벤트를 제거합니다
func logPendingCustomEventsIfNecessary() {
let remoteStorage = RemoteStorage(storageType: .suite)
guard let pendingEvents = remoteStorage.retrieve(forKey: .pendingCustomEvents) as? [[String: Any]] else { return }
// 1
for event in pendingEvents {
var eventName: String?
var properties: [AnyHashable: Any] = [:]
// 2
for (key, value) in event {
if key == PushNotificationKey.eventName.rawValue {
// 3
if let eventNameValue = value as? String {
eventName = eventNameValue
} else {
print("Invalid type for event_name key")
}
} else {
// 4
properties[key] = value
}
}
// 5
if let eventName = eventName {
AppDelegate.braze?.logCustomEvent(eventName, properties: properties)
}
}
// 6
remoteStorage.removeObject(forKey: .pendingCustomEvents)
}
- (void)logPendingEventsIfNecessary {
RemoteStorage *remoteStorage = [[RemoteStorage alloc] initWithStorageType:StorageTypeSuite];
NSArray *pendingEvents = [remoteStorage retrieveForKey:RemoteStorageKeyPendingCustomEvents];
// 1
for (NSDictionary<NSString *, id> *event in pendingEvents) {
NSString *eventName = nil;
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
// 2
for (NSString* key in event) {
if ([key isEqualToString:@"event_name"]) {
// 3
if ([[event objectForKey:key] isKindOfClass:[NSString class]]) {
eventName = [event objectForKey:key];
} else {
NSLog(@"Invalid type for event_name key");
}
} else {
// 4
properties[key] = event[key];
}
}
// 5
if (eventName != nil) {
[AppDelegate.braze logCustomEvent:eventName properties:properties];
}
}
// 6
[remoteStorage removeObjectForKey:RemoteStorageKeyPendingCustomEvents];
}
커스텀 속성 저장하기
커스텀 속성을 저장하려면 처음부터 분석 데이터를 생성해야 합니다. 사전(dictionary)을 생성하고 메타데이터로 채운 다음 헬퍼 파일을 사용하여 데이터를 저장하면 됩니다.
- 속성 메타데이터로 사전을 초기화합니다
userDefaults를 초기화하여 속성 데이터를 검색하고 저장합니다- 기존 배열이 있으면 새 데이터를 기존 배열에 추가하고 저장합니다
- 기존 배열이 없으면 새 배열을
userDefaults에 저장합니다
func saveCustomAttribute() {
// 1
let customAttributeDictionary: [String: Any] = ["YOUR-CUSTOM-ATTRIBUTE-KEY": "YOUR-CUSTOM-ATTRIBUTE-VALUE"]
// 2
let remoteStorage = RemoteStorage(storageType: .suite)
// 3
if var pendingAttributes = remoteStorage.retrieve(forKey: .pendingCustomAttributes) as? [[String: Any]] {
pendingAttributes.append(contentsOf: [customAttributeDictionary])
remoteStorage.store(pendingAttributes, forKey: .pendingCustomAttributes)
} else {
// 4
remoteStorage.store([customAttributeDictionary], forKey: .pendingCustomAttributes)
}
}
- (void)saveCustomAttribute {
// 1
NSDictionary<NSString *, id> *customAttributeDictionary = @{ @"YOUR-CUSTOM-ATTRIBUTE-KEY": @"YOUR-CUSTOM-ATTRIBUTE-VALUE" };
// 2
RemoteStorage *remoteStorage = [[RemoteStorage alloc] initWithStorageType:StorageTypeSuite];
NSMutableArray *pendingAttributes = [[remoteStorage retrieveForKey:RemoteStorageKeyPendingCustomAttributes] mutableCopy];
// 3
if (pendingAttributes) {
[pendingAttributes addObject:customAttributeDictionary];
[remoteStorage store:pendingAttributes forKey:RemoteStorageKeyPendingCustomAttributes];
} else {
// 4
[remoteStorage store:@[ customAttributeDictionary ] forKey:RemoteStorageKeyPendingCustomAttributes];
}
}
Braze로 커스텀 속성 전송하기
알림 콘텐츠 앱 확장에서 저장된 분석 데이터를 로깅하기에 가장 좋은 시점은 SDK가 초기화된 직후입니다. 대기 중인 속성을 순회하고, Braze에서 적절한 커스텀 속성을 설정한 다음, 다음에 이 함수가 필요할 때를 위해 저장소를 비우면 됩니다.
- 대기 중인 속성 배열을 순회합니다
pendingAttributes사전의 각 키-값 페어를 순회합니다- 해당 키와 값으로 개별 커스텀 속성을 로깅합니다
- 저장소에서 모든 대기 중인 속성을 제거합니다
func logPendingCustomAttributesIfNecessary() {
let remoteStorage = RemoteStorage(storageType: .suite)
guard let pendingAttributes = remoteStorage.retrieve(forKey: .pendingCustomAttributes) as? [[String: Any]] else { return }
// 1
pendingAttributes.forEach { setCustomAttributesWith(keysAndValues: $0) }
// 4
remoteStorage.removeObject(forKey: .pendingCustomAttributes)
}
func setCustomAttributesWith(keysAndValues: [String: Any]) {
// 2
for (key, value) in keysAndValues {
// 3
if let value = value as? [String] {
setCustomAttributeArrayWithKey(key, andValue: value)
} else {
setCustomAttributeWithKey(key, andValue: value)
}
}
}
- (void)logPendingCustomAttributesIfNecessary {
RemoteStorage *remoteStorage = [[RemoteStorage alloc] initWithStorageType:StorageTypeSuite];
NSArray *pendingAttributes = [remoteStorage retrieveForKey:RemoteStorageKeyPendingCustomAttributes];
// 1
for (NSDictionary<NSString*, id> *attribute in pendingAttributes) {
[self setCustomAttributeWith:attribute];
}
// 4
[remoteStorage removeObjectForKey:RemoteStorageKeyPendingCustomAttributes];
}
- (void)setCustomAttributeWith:(NSDictionary<NSString *, id> *)keysAndValues {
// 2
for (NSString *key in keysAndValues) {
// 3
[self setCustomAttributeWith:key andValue:[keysAndValues objectForKey:key]];
}
}
사용자 속성 저장하기
사용자 속성을 저장할 때는 업데이트되는 속성의 유형(email, first_name, phone_number 등)을 판별하기 위해 커스텀 오브젝트를 만드는 것이 좋습니다. 이 오브젝트는 UserDefaults에 저장/검색이 가능해야 합니다. 이를 수행하는 방법에 대한 예시는 UserAttribute 헬퍼 파일을 참조하세요.
- 해당 유형으로 인코딩된
UserAttribute오브젝트를 초기화합니다 userDefaults를 초기화하여 이벤트 데이터를 검색하고 저장합니다- 기존 배열이 있으면 새 데이터를 기존 배열에 추가하고 저장합니다
- 기존 배열이 없으면 새 배열을
userDefaults에 저장합니다
func saveUserAttribute() {
// 1
guard let data = try? PropertyListEncoder().encode(UserAttribute.userAttributeType("USER-ATTRIBUTE-VALUE")) else { return }
// 2
let remoteStorage = RemoteStorage(storageType: .suite)
// 3
if var pendingAttributes = remoteStorage.retrieve(forKey: .pendingUserAttributes) as? [Data] {
pendingAttributes.append(contentsOf: [data])
remoteStorage.store(pendingAttributes, forKey: .pendingUserAttributes)
} else {
// 4
remoteStorage.store([data], forKey: .pendingUserAttributes)
}
}
- (void)saveUserAttribute {
// 1
UserAttribute *userAttribute = [[UserAttribute alloc] initWithUserField:@"USER-ATTRIBUTE-VALUE" attributeType:UserAttributeTypeEmail];
NSError *error;
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:userAttribute requiringSecureCoding:YES error:&error];
if (error != nil) {
// log error
}
// 2
RemoteStorage *remoteStorage = [[RemoteStorage alloc] initWithStorageType:StorageTypeSuite];
NSMutableArray *pendingAttributes = [[remoteStorage retrieveForKey:RemoteStorageKeyPendingUserAttributes] mutableCopy];
// 3
if (pendingAttributes) {
[pendingAttributes addObject:data];
[remoteStorage store:pendingAttributes forKey:RemoteStorageKeyPendingUserAttributes];
} else {
// 4
[remoteStorage store:@[data] forKey:RemoteStorageKeyPendingUserAttributes];
}
}
Braze로 사용자 속성 전송하기
알림 콘텐츠 앱 확장에서 저장된 분석 데이터를 로깅하기에 가장 좋은 시점은 SDK가 초기화된 직후입니다. 대기 중인 속성을 순회하고, Braze에서 적절한 커스텀 속성을 설정한 다음, 다음에 이 함수가 필요할 때를 위해 저장소를 비우면 됩니다.
pendingAttributes데이터 배열을 순회합니다- 속성 데이터에서 인코딩된
UserAttribute오브젝트를 초기화합니다 - 사용자 속성 유형(이메일)에 따라 특정 사용자 필드를 설정합니다
- 저장소에서 모든 대기 중인 사용자 속성을 제거합니다
func logPendingUserAttributesIfNecessary() {
let remoteStorage = RemoteStorage(storageType: .suite)
guard let pendingAttributes = remoteStorage.retrieve(forKey: .pendingUserAttributes) as? [Data] else { return }
// 1
for attributeData in pendingAttributes {
// 2
guard let userAttribute = try? PropertyListDecoder().decode(UserAttribute.self, from: attributeData) else { continue }
// 3
switch userAttribute {
case .email(let email):
user?.email = email
}
}
// 4
remoteStorage.removeObject(forKey: .pendingUserAttributes)
}
- (void)logPendingUserAttributesIfNecessary {
RemoteStorage *remoteStorage = [[RemoteStorage alloc] initWithStorageType:StorageTypeSuite];
NSArray *pendingAttributes = [remoteStorage retrieveForKey:RemoteStorageKeyPendingUserAttributes];
// 1
for (NSData *attributeData in pendingAttributes) {
NSError *error;
// 2
UserAttribute *userAttribute = [NSKeyedUnarchiver unarchivedObjectOfClass:[UserAttribute class] fromData:attributeData error:&error];
if (error != nil) {
// log error
}
// 3
if (userAttribute) {
switch (userAttribute.attributeType) {
case UserAttributeTypeEmail:
[self user].email = userAttribute.userField;
break;
}
}
}
// 4
[remoteStorage removeObjectForKey:RemoteStorageKeyPendingUserAttributes];
}
헬퍼 파일
RemoteStorage 헬퍼 파일
enum RemoteStorageKey: String, CaseIterable {
// MARK: - Notification Content Extension Analytics
case pendingCustomEvents = "pending_custom_events"
case pendingCustomAttributes = "pending_custom_attributes"
case pendingUserAttributes = "pending_user_attributes"
}
enum RemoteStorageType {
case standard
case suite
}
class RemoteStorage: NSObject {
private var storageType: RemoteStorageType = .standard
private lazy var defaults: UserDefaults = {
switch storageType {
case .standard:
return .standard
case .suite:
return UserDefaults(suiteName: "YOUR-DOMAIN-IDENTIFIER")!
}
}()
init(storageType: RemoteStorageType = .standard) {
self.storageType = storageType
}
func store(_ value: Any, forKey key: RemoteStorageKey) {
defaults.set(value, forKey: key.rawValue)
}
func retrieve(forKey key: RemoteStorageKey) -> Any? {
return defaults.object(forKey: key.rawValue)
}
func removeObject(forKey key: RemoteStorageKey) {
defaults.removeObject(forKey: key.rawValue)
}
func resetStorageKeys() {
for key in RemoteStorageKey.allCases {
defaults.removeObject(forKey: key.rawValue)
}
}
}
@interface RemoteStorage ()
@property (nonatomic) StorageType storageType;
@property (nonatomic, strong) NSUserDefaults *defaults;
@end
@implementation RemoteStorage
- (id)initWithStorageType:(StorageType)storageType {
if (self = [super init]) {
self.storageType = storageType;
}
return self;
}
- (void)store:(id)value forKey:(RemoteStorageKey)key {
[[self defaults] setValue:value forKey:[self rawValueForKey:key]];
}
- (id)retrieveForKey:(RemoteStorageKey)key {
return [[self defaults] objectForKey:[self rawValueForKey:key]];
}
- (void)removeObjectForKey:(RemoteStorageKey)key {
[[self defaults] removeObjectForKey:[self rawValueForKey:key]];
}
- (void)resetStorageKeys {
[[self defaults] removeObjectForKey:[self rawValueForKey:RemoteStorageKeyPendingCustomEvents]];
[[self defaults] removeObjectForKey:[self rawValueForKey:RemoteStorageKeyPendingCustomAttributes]];
[[self defaults] removeObjectForKey:[self rawValueForKey:RemoteStorageKeyPendingUserAttributes]];
}
- (NSUserDefaults *)defaults {
if (!self.defaults) {
switch (self.storageType) {
case StorageTypeStandard:
return [NSUserDefaults standardUserDefaults];
break;
case StorageTypeSuite:
return [[NSUserDefaults alloc] initWithSuiteName:@"YOUR-DOMAIN-IDENTIFIER"];
}
} else {
return self.defaults;
}
}
- (NSString*)rawValueForKey:(RemoteStorageKey)remoteStorageKey {
switch(remoteStorageKey) {
case RemoteStorageKeyPendingCustomEvents:
return @"pending_custom_events";
case RemoteStorageKeyPendingCustomAttributes:
return @"pending_custom_attributes";
case RemoteStorageKeyPendingUserAttributes:
return @"pending_user_attributes";
default:
[NSException raise:NSGenericException format:@"Unexpected FormatType."];
}
}
UserAttribute 헬퍼 파일
enum UserAttribute: Hashable {
case email(String?)
}
// MARK: - Codable
extension UserAttribute: Codable {
private enum CodingKeys: String, CodingKey {
case email
}
func encode(to encoder: Encoder) throws {
var values = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .email(let email):
try values.encode(email, forKey: .email)
}
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let email = try values.decode(String.self, forKey: .email)
self = .email(email)
}
}
@implementation UserAttribute
- (id)initWithUserField:(NSString *)userField attributeType:(UserAttributeType)attributeType {
if (self = [super init]) {
self.userField = userField;
self.attributeType = attributeType;
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:self.userField forKey:@"userField"];
[encoder encodeInteger:self.attributeType forKey:@"attributeType"];
}
- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
self.userField = [decoder decodeObjectForKey:@"userField"];
NSInteger attributeRawValue = [decoder decodeIntegerForKey:@"attributeType"];
self.attributeType = (UserAttributeType) attributeRawValue;
}
return self;
}
@end
EventName Dictionary 헬퍼 파일
extension Dictionary where Key == String, Value == Any {
init(eventName: String, properties: [String: Any]? = nil) {
self.init()
self[PushNotificationKey.eventName.rawValue] = eventName
if let properties = properties {
for (key, value) in properties {
self[key] = value
}
}
}
}
@implementation NSDictionary (Helper)
- (id)initWithEventName:(NSString *)eventName properties:(NSDictionary *)properties {
self = [self init];
if (self) {
dict[@"event_name"] = eventName;
for(id key in properties) {
dict[key] = properties[key];
}
}
return self;
}
@end