📕 목차
1. Introduction
2. Architecture Design & Implementation
3. Firebase Service Implementation
1. Introduction
📌 문제 상황
작업한 내용은 위 PR에서 확인 가능합니다.
개발 중인 앱에서 사용자의 행동 데이터를 수집하기 위해 Google Analytics 4를 사용하기로 결정했다.
가장 큰 이유는 무료...ㅎ 라는 점이었고, 그럼에도 다양한 기능들을 제공해주기 때문이다.
심지어 Firebase 버전의 딥링크인 동적 링크로 유입 경로도 추적(페이스북, 인스타 같은 특정 광고 매체 제외)해준다니, 이 정도만 해도 디자인팀에서 원하던 사용자 행동 분석 데이터로는 충분할 것이라 예상한다.
심지어 적용도 엄청 쉽다.
pod 'Firebase/Analytics'
Podfile에 GA만 추가해주고, 문서에 나온 그대로 똑같이 코드를 추가해주면, 자동 수집 이벤트 항목들은 트리거가 된다고 한다.
역시 구글이야, 믿고 있었어!
다른 사람들은 편하고 쉽게 잘만 적용하던데, 우린 알아먹을 수 없는 이상한 클래스 명으로 나온다.
심지어 화면 이름은 죄다 (not set)으로 보여져서 분석을 할 수가 없다.
그럼 GA4가 우리 서비스를 차별하는 걸까?
당연히 그럴리가..
FCM에선 Swift UI를 사용할 거면 직접 이벤트 로깅을 해야한다고 알려주고 있었다.
대체 왜 Swift UI만 차별할까 싶었는데, UIKit와 달리 MVVM 패턴을 적극 권장하는 특성 상 어쩔 수 없었던 게 아닐까 싶었다.
MVC 패턴처럼 UIViewController와 View가 1:1로 대응되는 환경에선, 하나의 View 이벤트와 화면 조회수를 추적하기 위해 UIViewController를 추적하면 그만이다.
하지만 MVVM 패턴은? View를 구성하기 위한 여러 View가 있을 수 있고, ViewModel이 있을 수도 있다.
이런 상황에서 대체 어떤 View를 선택해서 조회수를 측정하라는 건지 함부로 판단할 수가 없으니, Firebase에선 깔끔하게 "니가 해"라고 대응하는 것 같다.
한 가지 아쉬운 점은 SwiftUI를 MVC 패턴처럼 설계한 후 구성해도 추적이 안 되는지 확인해보고 싶은데, 내가 iOS 개발자는 아니니 팀원들이 구현해놓은 코드를 기반으로만 진행하고 테스트는 안 했다.
📌 단순한 해결책
Analytics.logEvent(AnalyticsEventSelectContent, parameters: [
AnalyticsParameterItemID: "id-\(title!)",
AnalyticsParameterItemName: title!,
AnalyticsParameterContentType: "cont",
])
Firebase에선 SwiftUI 사용자를 위한 수동 이벤트 로깅 방법을 위와 같이 처리하면 된다고 알려준다.
혹시나 더 상세한 사용방법을 알고 싶다면, 위 repo에서 확인할 수 있다.
그래서 MainTabView의 TabView에 대충 아무 값이나 넣고 시뮬레이터를 실행했더니, 드디어 사람이 알아볼 수 있는 언어로 조회수를 확인하고 있었다.
navigation도 정상적으로 추적해줄까?
MainView에서 NavigationLink로 연결되어 있는 View 아무거나 하나 붙잡고, 똑같이 View에다가 logEvent를 추가해봤다.
조회수 체크는 물론이고, 이전 screen까지 잘 체크해서 경로 탐색 분석까지 성공하는 걸 보여준다.
하지만 문제는 모든 View에 저 작업을 하나하나 지정해주어야 한다는 게 영 마음에 들지 않았다.
중복 코드, 휴먼 에러 등 여러 문제도 무시할 수 없는 논제지만,
Analytics을 위해 View LifeCycle 여러 단계에 event를 보내야 하는 경우가 있는 반면, 그저 데이터 그 자체만 중요한 경우도 있다.
그러나 대부분의 Analytics는 PM이나 UX Designer의 관심사지 Product code spec에 포함되지 않아야 한다고 생각한다.
만약 분석을 위한 스니펫이 Product code에 포함된다면, 앱의 기능이나 아키텍처, 혹은 UI를 수정할 때 문제가 매우 까다로워진다.
따라서 수동 이벤트 로깅을 적절히 처리하면서, Usecase를 만족하는 설계를 도입해보자.
📌 Use case
- 사용자 행동 분석
- 앱 내 사용자 흐름 추적
- 가장 자주 방문하는 화면 파악
- 사용자 참여도 측정
- 전환율 분석
- 회원가입 프로세스 분석
- 가장 자주 사용하는 로그인 타입 분석
- 사용자 세그멘테이션
- 사용자 속성별 행동 패턴 분석
- 지역별, 디바이스별 사용 패턴 파악
- 신규 사용자와 재방문 사용자의 행동 비교
- 페이지 뷰
- 조회수와 체류 시간 측정
- 페이지 이동 경로 측정
데이터 분석에 있어선 예전에 "하루 5분 UX"라는 책을 읽은 적이 있었다.
데이터 측정을 위한 인프라를 마련해두고 실제 트래픽을 마련하면, 이 그래프를 분석할 수 있는 좋은 기회가 되지 않을까.
2. Architecture Design & Implementation
📌 분석 도구들의 외부 종속성
위에서도 언급했듯 Analytics를 위한 코드를 위 방식처럼 반영했을 때 발생할 수 있는 문제점은 무엇일까?
1️⃣ 디버깅 어려움
product code 스펙에 해당하지 않은 스니펫이 침투했을 때 가장 직접적으로 느낄 수 있는 문제는 역시나 가독성 문제라고 생각한다.
백엔드 애플리케이션에서 swagger 문서를 위한 스니펫이 제품 코드에 침투하는 것과 같은 맥락이다.
클래스 혹은 함수 하나를 이해하기 위해 너무 많은 정보에 노출된다.
심지어 개발자를 위한 게 아닌 PM이나 Designer를 위해 추가한 코드에 의해!
2️⃣ 정책 변경에 따른 유연성 저하
GA4가 무료인데다 접근성이 편리해서 사용한다지만, 만능은 아니다.
처음 도입했을 때 데이터 분석은 커녕 UI를 이해하는 데만 한참 걸렸고, GA4는 이벤트 기반 전환만 볼 수 있기 때문에 대부분 전환 항목에 대해선 구글 태그 관리자를 직접 추가해서 커스텀해야 한다.
분석 기술도 배워야 하는데, 클라이언트 측에서 Analytics를 위한 스니펫을 도입하는 것도 쉬운지는 잘 모르겠다. (처음엔 쉬웠는데 갈 수록 복잡..)
이런 상황에서 만약 UX 디자이너가 "우리가 원하는 데이터를 확보하기 위해 다른 분석툴을 도입하고 싶어요"라는 안건을 들고 왔다고 치자.
분석 도구를 아예 바꾸는 경우일 수도 있고, Mixpanel Analytics같은 다른 도구를 추가로 도입하자고 할 수도 있다.
예를 들어, 이런 경우에 해당한다.
"GA4를 사용해 일부 이벤트를 추적하고, Mixpanel를 이용해 다른 이벤트를 추적해주세요. 아, 그리고 또 다른 이벤트를 추적하기 위해 Flurry Analytics에 통합해주세요"
왜 그래야 하는 지 의문을 갖는 건 개발자가 할 일이 아니다.
어쨌든 프로젝트 정책 상 그렇게 해야 한다고 결정했으면, 작업 예상 시간을 측정해봐야 하는데 이건 뭐 답이 없어진다.
하지만 한 가지 방법이 있다면, 설계 원칙에서 귀에 딱지가 앉도록 들어오던 특정 구현체에 의존하지 않게 만들고
외부 종속성과 코드 간의 결합을 느슨하게 만들면 되지 않을까?
📌 Incremental Architecture Improvement
기존 방법의 문제는 결국 View가 구체 클래스에 의존하고 있기 때문에 발생한다.
V1, V2, V3가 구체적인 스펙에 맞추어 코드를 작성했으므로, 만약 새로운 분석 도구를 도입하자고 했을 때 변경에 상당히 취약해진다.
이때, V1, V2, V3가 구체 클래스에 의존하지 않게 하는 가장 좋은 방법은 어댑터 패턴을 적용해보는 것이다.
구체 클래스 A를 사용하려면 반드시 AM을 거쳐야 한다고 강제했다고 치자.
V1, V2, V3는 더 이상 Analytics의 존재를 알지 못하기 때문에, 새로운 분석 도구로 갈아치워도 AM만 변경 사항에 대응하면 된다.
물론 위 방법이 완벽하게 V1, V2, V3의 변경 사항을 없앴다고 보긴 힘들다.
화살표 방향을 대충 그려서 저 모양이지만, 원래는 V1, V2, V3는 Analytics에 대해 추이 종속성을 가지게 되므로 변경에 대한 영향을 받게 될 수도 있다.
하지만 적어도 처음 방식보단 수정할 코드가 많이 줄어들 것이다.
여기서 한 발짝 더 나가보자.
GA4를 그대로 유지하면서, 다른 분석 도구를 "추가"로 도입하자는 정책이 수립되면 어떻게 될까?
일단 AM의 책임이 너무 많아진다.
A1, A2, A3가 요구하는 스펙대로 데이터를 가공해야 하고, 거기에 필요한 데이터를 수집하기 위해 V1, V2, V3에서 추가적인 인자를 전달해야 할 수도 있다.
단일 분석 도구 변경에 대해선 그나마 대응할만 했지만, 여전히 추가적인 분석 도구가 도입될 때는 확장성이 썩 좋질 않다.
이 문제를 해결하기 위해선, 처음에 했던 방법과 똑같은 접근법을 사용하면 된다.
AM이 더 이상 A1, A2, A3의 존재를 모르도록 만들어라!
A1, A2, A3 각각에 대한 어댑터 패턴의 Service를 만들어준다.
하지만 여전히 AM은 AS1, AS2, AS3라는 구체 클래스에 의존하게 되므로 문제는 사라지지 않는다.
그러나 AS1, AS2, AS3가 가져야 하는 스펙은 그리 차이가 나지 않는다.
따라서 AnalyticsService라는 protocol을 각 AS가 구현하도록 만들면, AM은 AS만을 가지게 되어 의존성을 완전히 제거할 수 있게 된다.
결과적으로 V는 구체 클래스 A에 의존하지 않게 되었으며,
AM 또한 각 AS의 존재를 모르므로 SOLID 원칙을 아주 골고루 지킨 아키텍처를 만들 수 있었다.
📌 Diagram
- Adapter 패턴을 사용하여 각 SDK 특정 구현체 의존성을 제거하여 의존성을 역전시킴으로써, 확장성과 유연성을 극대화했다.
- 각 컴포넌트에 명확한 역할을 정의하여 SRP 원칙 또한 준수하고 있다.
- AnalyticsManager를 Singleton으로 만들지 않아도 되지만, 하지 않을 이유가 없다. 사용하려는 클래스마다 의존성을 주입하는 과정은 번거롭기 때문에 이렇게 처리했다.
- Observer 패턴을 사용해 Application에서 event가 발생하면, 특정 이벤트를 구독 중인 AnalyticsService들이 로깅을 하도록 만들었다. (옵저버 패턴 공부할 적, 로깅할 때 유용하다고 배웠던 것처럼)
참고로 그림은 해당 블로그를 참고했다.
아니, 근데 억울한 게 아이디어 훔친 거 아닌데 훔친 것처럼 됨...
블로그 쓰다가 발견해서 뭐 증명도 못 하구 🥲
📌 AnalyticsService
자, 그럼 이제 AnalyticsService protocol은 어떤 명세를 표현해야 할까.
- initialize(): AppDelegate에서 SDK를 초기화해야 한다.
- firebase로 치면 FirebaseApp.configure() 동작을 포함할 것이다.
- track(): 구독할 event를 tracking한다.
- setUser(): 이벤트를 발생시킨 사용자 세션 정보 등록
protocol AnalyticsService {
/**
SDK를 초기화한다.
> 이 메서드는 AppDelegate application:didFinishLaunchingWithOptions: 메서드 내에서 반드시 호출되어야 한다.
*/
func initialize(application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]?)
/**
앱 내에서 이벤트를 추적한다.
*/
func track(_ event: AnalyticsEvent, additionalParams: [AnalyticsConstants.Parameter: Any]?)
/**
사용자 정보를 설정한다.
- Parameters:
- userId: 사용자 식별자 정보 (고유값)
- properties: 사용자 추가 정보 (옵션)
*/
func setUser(_ userId: String, _ properties: [String: String]?)
/**
구독할 이벤트의 목록을 제공하는 변수.
- Note: 이 변수는 구독할 이벤트의 배열을 반환합니다. 구독할 이벤트를 설정하려면 이 변수를 구현하는 클래스 또는 구조체에서 배열을 반환하도록 해야 합니다.
*/
var subscribedEvents: [AnalyticsEvent.Type] { get }
}
위 명세를 그대로 protocol로 옮겼다.
subscribeScreens와 subscribeTriggerEvents 속성은 각 SDK가 구독 중인 Event 정보를 표현한다.
로그를 전송하기 위한 메서드는 비동기 처리를 했다.
📌 AnalyticsEvent
Analytics 설계를 열심히 해놓고, 로깅 정보를 문자열로 입력받도록 API를 내놓는 순간 온갖 휴먼 에러가 발생할 가능성이 높다.
특히 분석 도구를 사용하는 부서에서 특정 로그의 클래스 이름을 수정해달라던가, 비슷한 요청을 했을 때 하나하나 추적하면서 수정하려는 수고를 들이고 싶은 생각은 추호도 없다.
이런 경우는 대게 열거 타입을 사용하는 것을 선호하는 편이다.
다만 java만 줄창 쓰다가 swift로 쓰려니까 죽을 맛...제대로 쓴 건지도 잘 모르겠다.
enum AnalyticsEvent {
case screenView(ScreenViewEvent)
case eventLog(TriggerEvent)
var name: String {
switch self {
case .screenView(let event):
return event.name
case .eventLog(let event):
return event.name
}
}
var parameters: [String: Any]? {
switch self {
case .screenView(let event):
return event.parameters
case .eventLog(let event):
return event.parameters
}
}
}
처음에는 위 방식처럼 ScreenEvent와 TriggerEvent로 나눴다. (이름이 뒤죽박죽인 건 swift가 익숙칠 않아서 이것저것 테스트해보다가 수정을 안 해서,,)
이유는 위에서 조금 분석 코드 조금 써봤을 때, Screen의 경우 AnalyticsParameterScreenName 파라미터 같은 걸 쓰고 있고, logEvent에서도 AnalyticsEventScreenView 같은 이상한 애들이 자꾸 등장해서 이걸 구분해줘야 하나? 싶었다.
그런데 이렇게 하면 너무 Firebase 플랫폼에 종속되는 형태가 되어버려서, 마음에 들지 않아 조금 더 찾아봤다.
그런데 알고보니 이거 그냥 상수였음 ㅋ. 바보 인증 프리패스.
screen 로그에선 screen_name 쓰고, event 로그에선 page_name 쓰는 건가 했는데 web/app 차이였을 뿐이었다.
그렇다면 app에선 내가 정의한 파라미터 상수를 쓰게 하고, 각 Service 구현체에서 매핑해주면 될 것 같았다.
protocol AnalyticsEvent {
var eventName: AnalyticsConstants.EventName { get }
var eventType: AnalyticsConstants.EventType { get }
var parameters: [AnalyticsConstants.Parameter: Any]? { get }
}
AnalyticsEvent는 protocol로 명세만 정의하고, 구체적인 로그 정보는 각 도메인 별로 상수를 정의해주는 쪽이 관리가 편할 것 같았기에 위와 같이 수정했다.
enum AnalyticsConstants {
enum EventName {
case screenView
var rawValue: String {
switch self {
case .screenView: return "screen_view"
}
}
}
enum EventType {
case screenView
case userAction
var rawValue: String {
switch self {
case .screenView: return "screen_view"
case .userAction: return "user_action"
}
}
}
enum Parameter {
case screenId
case screenName
case screenClass
var rawValue: String {
switch self {
case .screenId: return "screen_id"
case .screenName: return "screen_name"
case .screenClass: return "screen_class"
}
}
}
}
이벤트 이름과 타입, 그리고 파라미터의 키로 사용할 항목은 열거 타입을 사용해 제한하였다.
참고로 여기서 상수값들은 Application에서 보낸 event들을 각 구현체에서 처리하기 쉽게 만들기 위함이지, 특정 SDK에 종속되어 사용하기 위함이 아니다.
위의 protocol을 사용하면 각 도메인 별로 상수를 정의하는 게 매우 쉬워지는데, 로그인 스크린 뷰 이벤트 상수를 정의할 땐 다음과 같이 정의하면 된다.
enum AuthenticationEvents: AnalyticsEvent {
case login
var eventName: AnalyticsConstants.EventName {
switch self {
case .login: return AnalyticsConstants.EventName.screenView
}
}
var eventType: AnalyticsConstants.EventType {
switch self {
case .login: return AnalyticsConstants.EventType.screenView
}
}
var parameters: [AnalyticsConstants.Parameter: Any]? {
switch self {
case .login:
return [
.screenId: "login_screen_view_event",
.screenName: "로그인 화면",
.screenClass: "LoginView"
]
}
}
}
📌 AnalyticsManager
final class AnalyticsManager {
static let shared = AnalyticsManager()
private var services: [AnalyticsService] = []
private let queue = DispatchQueue(label: "kr.co.pennyway.analytics", attributes: .concurrent)
private init() {}
/// AnalyticsService 프로토콜을 구현한 서비스를 추가한다.
func addService(_ service: AnalyticsService) {
queue.async(flags: .barrier) {
self.services.append(service)
}
}
/// `addService` 메서드로 추가한 서비스들을 `AppDelegate`에서 초기화한다.
/// 이 메서드는 반드시 AppDelegate의 `application(_:didFinishLaunchingWithOptions:)`에서 호출되어야 한다.
func initialize(application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
services.forEach { $0.initialize(application: application, launchOptions: launchOptions) }
}
/// 사용자 정보를 설정한다.
///
/// - Parameters:
/// - userId: 사용자 식별자 정보 (고유값)
/// - properties: 사용자 추가 정보. key (옵션)
func setUser(_ userId: String, _ properties: [String: String]? = [:]) {
for service in services {
service.setUser(userId, properties)
}
}
/**
구독된 이벤트를 추적한다.
이 메서드는 비동기로 실행되며, Thread-Safe하다.
- Parameters:
- event: 추적할 이벤트
- additionalParams: 이벤트에 추가할 파라미터
*/
func trackEvent(_ event: AnalyticsEvent, additionalParams: [AnalyticsConstants.Parameter: Any]?) {
queue.async {
self.services.forEach{ service in
service.trackEventIfSubscribed(event, additionalParams: additionalParams)
}
}
}
}
Manager를 정의하는 건 어렵지 않다.
Application에서 이벤트를 발행했을 때, 구독된 서비스의 메서드를 차례로 호출해주면 끝이기 때문.
이벤트를 추적할 때 비동기 처리를 해준 이유는 로깅도 어찌됐건 네트워크 요청인데, 이 작업을 동기적으로 수행해주는 건 자칫 성능 저하를 유발할 수 있다고 판단했기 때문이다.
async/await을 사용할 수도 있지만, 안 그래도 swift가 익숙칠 않은데 콜백 함수가 너무 많이 나와서 이해가 안 가길래 DispatchQueue 자료구조를 사용해서 처리했다.
📌 AnalyticsScreenTracker
얼추 필요한 인터페이스와 클래스는 모두 정의했다.
하지만 이대로는 사용할 때마다 다음과 같이 매니저를 호출해주어야 한다.
// 로그인 이벤트 (추가 파라미터 포함)
AnalyticsManager.shared.trackEvent(AuthenticationEvents.login, additionalParams: ["method": "email"])
// 회원가입 이벤트
AnalyticsManager.shared.trackEvent(AuthenticationEvents.signup)
하지만 기존의 이건..뭔가 안 이쁘잖아.
import SwiftUI
// MARK: - AnalyticsEventTracker
struct AnalyticsEventTracker: ViewModifier {
let event: AnalyticsEvent
let additionalParams: [AnalyticsConstants.Parameter: Any]?
func body(content: Content) -> some View {
content.onAppear {
AnalyticsManager.shared.trackEvent(event, additionalParams: additionalParams)
}
}
}
// MARK: - AnalyticeUserTracker
struct AnalyticeUserTracker: ViewModifier {
let userId: String
let properties: [String: String]?
func body(content: Content) -> some View {
content.onAppear {
AnalyticsManager.shared.setUser(userId, properties)
}
}
}
extension View {
func analyzeEvent(_ event: AnalyticsEvent, additionalParams: [AnalyticsConstants.Parameter: Any]? = nil) -> some View {
modifier(AnalyticsEventTracker(event: event, additionalParams: additionalParams))
}
func analyzeUser(_ userId: String, properties: [String: String]? = [:]) -> some View {
modifier(AnalyticeUserTracker(userId: userId, properties: properties))
}
}
그래서 View를 확장해주기로 했다.
event 발행과 이벤트를 발생시킨 user 정보를 설정하기 위한 Tracker를 별도로 정의해주고,
View를 확장해서 메서드를 추가해주었다.
위 작업을 처리해주면 보다 간결하게 AnalyticsManager 호출 로직을 처리해줄 수 있다.
analyzeUser 부분도 더 간략하게 처리할 수 있을 거 같은데, 이 이상은 진짜 모르겠어서 패스
3. Firebase Service Implementation
📌 Firebase Service
FirebaseService의 메서드를 구현하는 일만 남았다.
initialize는 설명할 게 없지만, track은 고민할 거리가 남아있다.
애플리케이션에서 파라미터를 특정 구현체에 종속되지 않으면서, 상수로 정의하여 관리의 편의성을 높여두었으나
GA4에서 인식하려면 문서에 나온대로 쿼리 파라미터 필드를 수정해주어야 한다.
func track(_ event: any AnalyticsEvent, additionalParams: [AnalyticsConstants.Parameter: Any]?) {
let firebaseParams = convertParameters(event, additionalParams)
if event.eventName == .screenView {
Analytics.logEvent(AnalyticsEventScreenView, parameters: firebaseParams)
} else {
Analytics.logEvent(event.eventName.rawValue, parameters: firebaseParams)
}
Log.info("Firebase: Tracking event \(event.eventName.rawValue) with parameters \(String(describing: additionalParams))")
}
우선 파라미터를 [String: Any] 타입의 배열로 매핑하면서, 동시에 firebase에서 요구하는 쿼리로 매핑해주었다.
그 다음 eventName을 기준으로 logEvent의 값을 수정해주어 처리하도록 했다.
private func convertParameters(_ event: AnalyticsEvent, _ additionalParams: [AnalyticsConstants.Parameter: Any]?) -> [String: Any] {
var params: [String: Any] = [:]
// 이벤트 파라미터 변환
event.parameters?.forEach { key, value in
switch key {
case .screenId:
params["firebase_screen_id"] = value
case .screenName:
params[AnalyticsParameterScreenName] = value
case .screenClass:
params[AnalyticsParameterScreenClass] = value
default:
params[key.rawValue] = value // 그 외의 파라미터가 있다면 커스텀 파라미터로 취급
}
}
additionalParams?.forEach { params[$0.key.rawValue] = $0.value }
return params
}
convertParameters() 메서드는 위와 같은데, 단도직입적으로 말해서 진짜 더러운 코드다!
이걸 더 깔끔하게 처리하려면, 각 파라미터마다 대응하는 firebase 상수를 열거 타입으로 정의하는 게 더 보기 좋을 것이다.
하지만 현재 테스트를 위해 극히 일부의 상수만 정의해둔 상태고, 나중에 개선해도 될 문제라고 생각해서 일단 이대로 코드를 구현했다.
전체 코드는 다음과 같다.
class FirebaseAnalyticsService: AnalyticsService {
var subscribedEvents: [AnalyticsEvent.Type] {
[AuthenticationEvents.self, SpendingEvents.self]
}
func initialize(application _: UIApplication, launchOptions _: [UIApplication.LaunchOptionsKey: Any]?) {
Log.info("Firebase: Initialized")
}
func track(_ event: any AnalyticsEvent, additionalParams: [AnalyticsConstants.Parameter: Any]?) {
let firebaseParams = convertParameters(event, additionalParams)
if event.eventName == .screenView {
Analytics.logEvent(AnalyticsEventScreenView, parameters: firebaseParams)
} else {
Analytics.logEvent(event.eventName.rawValue, parameters: firebaseParams)
}
Log.info("Firebase: Tracking event \(event.eventName.rawValue) with parameters \(String(describing: additionalParams))")
}
/// - Parameters:
/// - userId:
/// The user ID to ascribe to the user of this app on this device, which must be non-empty and no more than 256 characters long. Setting userID to nil removes the user ID.
/// - value:
/// The value of the user property. Values can be up to 36 characters long. Setting the value to nil removes the user property.
/// - name:
/// The name of the user property to set. Should contain 1 to 24 alphanumeric characters or underscores and must start with an alphabetic character. The “firebase_”, “google_”, and “ga_” prefixes are reserved and should not be used for user property names.
func setUser(_ userId: String, _ properties: [String: String]?) {
Analytics.setUserID(userId)
properties?.forEach { Analytics.setUserProperty($0.key, forName: $0.value) }
Log.info("Firebase: Setting user \(userId) with properties \(String(describing: properties))")
}
private func convertParameters(_ event: AnalyticsEvent, _ additionalParams: [AnalyticsConstants.Parameter: Any]?) -> [String: Any] {
var params: [String: Any] = [:]
// 이벤트 파라미터 변환
event.parameters?.forEach { key, value in
switch key {
case .screenId:
params["firebase_screen_id"] = value
case .screenName:
params[AnalyticsParameterScreenName] = value
case .screenClass:
params[AnalyticsParameterScreenClass] = value
default:
params[key.rawValue] = value // 그 외의 파라미터가 있다면 커스텀 파라미터로 취급
}
}
additionalParams?.forEach { params[$0.key.rawValue] = $0.value }
return params
}
}
📌 AppDelegate
이론 상 완벽하게 동작하는 코드를 구현했다.
마지막으로 서비스를 등록하고 초기화만 해주면 되지 않을까..!
하지만 나는 잊고 있었다.
인생은 뭐든 한 번에 풀리는 법이 없다는 것을..
별 짓을 다 해봤는데 FirebaseApp이 정상적으로 초기화되지 않아 로그가 보내지지 않았다.
그래서 그냥 포기하고 AppDelegate에서 직접 초기화해주었다.
안 되는 걸 나더러 어쩌라구..
정확하진 않지만 초기화 시점이 너무 늦어져서 문제가 되는 건 아닐까 싶은데,
이건 뭐 iOS팀에서 알아서 해주겠지.
📌 테스트
드디어 성공!
하지만 아직 할 일이 많다.
맞춤형 이벤트가 정상적으로 보내지는 지도 확인해봐야 하고, 여전히 user 정보를 세팅해줘도 제대로 전송이 되지 않는 문제.
그리고 근본적으로 swift에 대한 내 이해도 결핍으로 인해 코드가 상당히 더러운 게 너무 마음에 안 든다.
여튼 초기 목적은 1차적으로 완수했으므로 이번 포스팅은 여기까지.