-
Notifications
You must be signed in to change notification settings - Fork 3
Protocol 기반의 Analytics System을 만들어보자
- 이벤트를 로깅하는것은 매우 쉬워야합니다 (UIKit view controller 기준 "한줄") (SwiftUI view/view model 기준 "한줄")
- 이벤트를 어딘가에 보낼수 있어야합니다 (BE 서버, CoreData ... )
- 테스트 가능해야합니다
- 이벤트를 새로 추가/제거/수정 하는것이 쉬워야합니다.
싱글톤
- 가장 편한 방법이 될수도 있지만, 최대한 피하고, Dependency Injection을 이용하자
스트링
- 물론 결국 저장되고 보내지는 형태는 string이야 하겠지만, 이벤트 자체의 identifier가 string이 된다면, 나중에 유지보수가 매우 힘들어 질 것이다.
-
AnalyticsEvent
- analytics system이 제공하는 모든 이벤트들.
-
enum으로 정의프로토콜로 정의되어
-
AnalyticsManager
- 이벤트를 로깅하기 위한 최상단 API, 실제로 로깅을하지는 않고,
-
AnalyticsEngine
을 이용하여 보낼것이다.
-
AnalyticsEngine
- 직접적으로 로깅 전송/저장을 담당한다.
- 프로토콜로 정의
처음에 생각했던것은 enum이었습니다....(참고)
enum AnalyticsEvent {
case loginScreenViewed
case loginAttempted
case loginFailed(reason: LoginFailureReason)
case loginSucceeded
case messageListViewed
case messageSelected(index: Int)
case messageDeleted(index: Int, read: Bool)
}
그리고 해당 event를 매핑할떄는 computed properties를 이용합니다.
extension AnalyticsEvent {
var name: String {
switch self {
case .loginScreenViewed, .loginAttempted,
.loginSucceeded, .messageListViewed:
return String(describing: self)
case .loginFailed:
return "loginFailed"
case .messageSelected:
return "messageSelected"
case .messageDeleted:
return "messageDeleted"
}
}
}
extension AnalyticsEvent {
var metadata: [String : String] {
switch self {
case .loginScreenViewed, .loginAttempted,
.loginSucceeded, .messageListViewed:
return [:]
case .loginFailed(let reason):
return ["reason" : String(describing: reason)]
case .messageSelected(let index):
return ["index" : "\(index)"]
case .messageDeleted(let index, let read):
return ["index" : "\(index)", "read": "\(read)"]
}
}
}
하지만 이렇게할때 두가지 문제 점이 있었습니다
- AnalyticsEvent 하나 추가 할때 마다 name과 meta data를 switch self하면서 지정해줘야한다
- 현재 앱에서 존재하는 모든 이벤트가 하나에 모여있다
그래서 protocol과 struct를 이용하는것으로 바꿨습니다. (참고1 & 참고2) 참고 했던 사이트의 말을 빌려보자면...
But where the enum required two large computed properties, the struct has a simple init and its data is localized to a small func or let. (And as of today, it’s much easier to make the struct version Equatable/Hashable.) -Matt Diephouse
protocol AnalyticsEvent {
var name: String { get }
var metadata: [String: String] { get }
}
struct PlayerEvent: AnalyticsEvent {
var name: String
var metadata: [String: String]
private init(name: String, metadata: [String: String] = [:]) {
self.name = name
self.metadata = metadata
}
static func trackPlayed(_ trackID: Int) -> PlayerEvent {
return PlayerEvent(
name: "trackPlayed",
metadata: ["trackID": "\(trackID)"]
)
}
.
.
.
이때 struct 내에서 type method를 쓰면 매우 편리합니다!
https://docs.swift.org/swift-book/LanguageGuide/Methods.html (기억을 한번 되살려보면...)
이렇게 할때의 장점은 크게 두가지입니다.
이벤트의 간편한 확장성
AnalyticEvent이
protocol로 구현함으로써, 새로운 이벤트를 추가하는것은 매우 간편해집니다.
이벤트 type checking
AnalyticEvent
protocol을 채택하는 custom type의 이벤트를 구현함으로써, 원하는 이벤트에 대한 자동완성 결과를 볼수 있습니다.
먼저 프로토콜부터 구현을 하자면
protocol AnalyticsEngine: class {
func sendAnalyticsEvent(named name: String, metadata: [String : String])
}
엔진의 다양한 구현
AnalyticsEngine
protocol을 채택하는 엔진을 다양하게 구현할수 있습니다.
그리고 상황에 맞게 필요한 엔진을 갈아 끼우는것도 매우 쉽습니다.
또한, AnalyticsManager는 다수의 엔진을 가질 수도 있습니다.
실제로 저희 앱에서는 back end server를 위한 engine
과 core data를 위한 engine
두개를 구현하고 주입했습니다.
마지막으로 event와 engine을 이어줄 manager를 구현해봅니다.
이친구가 engine을 가지고 이벤트를 받으면 해당 engine한테 넘겨주는 역할을 하게됩니다.
class AnalyticsManager {
private let engine: AnalyticsEngine
'
'
'
func log(_ event: AnalyticsEvent) {
engine.sendAnalyticsEvent(named: event.name, metadata: event.metadata)
}
}
먼저 최상위 파일인 App에 AnalyticsManager를 만들어줍니다.
struct MiniVibeApp: App {
let manager = AnalyticsManager(engine: MockAnalyticsEngine())
var body: some Scene {
WindowGroup {
CustomTabView(manager: manager)
}
}
}
그리고 해당 manager를 inject 해줍니다.
struct CustomTabView: View {
private let manager: AnalyticsManager
init(manager: AnalyticsManager) {
self.manager = manager
}
'
'
'
마지막으로 로깅을 위해서는 manager를 불러서 해주면된다.
struct TodayView: View {
private let manager: AnalyticsManager
init(manager: AnalyticsManager) {
self.manager = manager
}
'
'
'
.onAppear(perform: {
manager.log(ScreenEvent.screenViewed(.today))
})
이렇게 순수하게 로깅을 위한 코드는
manager.log(ScreenEvent.screenViewed(.today))
한줄이 됩니다.