클라이언트 이벤트 추적은 크게 세 가지로 나뉩니다.

  • 코드 기반 추적
  • 시각적 추적
  • 자동 추적

이 글은 주로 자동 추적의 발상과 구현 과정을 소개합니다.

1. 코드 기반 추적

코드 기반 추적은 기록할 지점에서 인터페이스를 호출해 이벤트 데이터를 직접 업로드하는 방식입니다. 대부분의 외부 분석 서비스도 코드 기반 추적 api를 제공하므로 편리합니다.

하지만 문제가 생깁니다. 추적 코드가 비즈니스 로직 곳곳에 흩어지고 강하게 결합됩니다. 화면이나 업무 흐름이 바뀌면 예전 이벤트를 어떻게 처리할지, 새 이벤트가 필요한지 알기 어렵습니다. 이벤트 수가 늘어나면 흩어진 코드를 관리하는 일은 재앙이 됩니다. 매크로나 팩토리 클래스로 코드를 단순화해도 근본적인 문제는 달라지지 않습니다.

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    // 埋点
    APE(kEvent4001);
}

2. 시각적 추적

시각적 추적은 시각화 도구에서 수집 지점을 설정합니다. 클라이언트가 설정을 자동으로 해석해 이벤트를 보고하여 이른바 “흔적 없는 추적”을 구현합니다. 대표적인 오픈 소스 방식으로 Mixpanel이 있습니다.

3. 자동 추적

전체 이벤트 추적이라고도 하는 자동 추적은 실제로 계측이 필요 없다는 뜻이 아닙니다. 모든 이벤트를 수집해 보고하고 서버에서 유용한 데이터를 걸러 냅니다.

더 개선하려면 서버가 설정 파일을 보내 클라이언트에서 직접 이벤트를 필터링할 수 있습니다.

KPI를 위해 이 발상을 바탕으로 바퀴를 다시 발명해 WKTrackingData를 만들었습니다.

여기에는 WKTrackingData를 구현하며 만난 문제와 생각을 기록합니다.

구현 발상은 단순합니다. 코드 구성은 다음과 같습니다.

당시 WKTrackingData 구성: 관리자, 이벤트 경로와 AOP 모듈

여기서 WKTrackingDataManager가 모든 데이터 관리와 이벤트 추적 설정을 담당합니다.

당시 이벤트 추적 설정 사용 예시

WKTrackingDataViewPathHelper가 event_path를 생성합니다.

AOP 모듈이 모든 이벤트 추적을 담당합니다.

자세한 사용법은 GitHub에서 확인할 수 있습니다.

이벤트를 고유하게 식별하려면?

두 가지 문제가 있습니다. 이벤트를 어떻게 표현하고, 고유성을 어떻게 보장할까요?

이벤트 표현

이벤트는 event_path로 표현합니다. 이벤트를 일으킨 responder에서 리스폰더 체인을 따라가며 그 체인의 path를 만드는 것이 핵심 발상입니다.

생성된 리스폰더 path는 다음과 같습니다.

"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate
이벤트의 고유 식별

일부 비즈니스 상황에서는 같은 button이나 다른 컨트롤이라도 속성이 달라지면 여러 이벤트를 나타냅니다.

예를 들어 홈 화면의 button이 로그아웃 상태에서는 눌러서 로그인, 로그인했지만 본인 확인 전에는 본인 확인으로 이동이라고 표시될 수 있습니다.

같은 button의 뷰 트리는 바뀌지 않으므로 생성된 리스폰더 path도 같습니다.

비슷한 상황으로 UISwitch의 켜짐과 꺼짐, UISegmentedControl의 indexSelect, UIStepper, 그리고 UITableView와 UICollectionView의 cell 누르기가 있습니다.

이에 대응하기 위해 WKTrackingData는 event_path를 만들 때 컨트롤 자체의 여러 속성을 선택해 덧붙입니다. 결과 event_path는 다음과 같습니다.

"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate#currentTitle=Button#state=1#enabled=1#selected=0";
비즈니스 확장

다른 상황으로 홈 화면에 banner 캐러셀이 있다고 생각해 봅시다.

banner의 각 광고 위치에 들어갈 이미지와 이동 url은 서버에서 보내며 위치도 설정할 수 있습니다.

이때 banner의 각 index가 어느 페이지로 연결될지는 고정되어 있지 않습니다. 0번 위치의 이벤트는 event_path만으로 정할 수 없습니다.

이벤트를 고유하게 식별하려면 url과 같은 구체적인 비즈니스 매개변수를 덧붙여야 합니다.

WKTrackingData는 비즈니스 매개변수 확장도 제공하여 호출하는 쪽에서 사용자 정의 매개변수를 덧붙일 수 있습니다.

이벤트에 비즈니스 매개변수를 추가하는 당시 코드 예시

이벤트를 추적하고 싶지 않은 컨트롤은 wk_ignoreTracking으로 제외할 수 있습니다.

self.slider.wk_ignoreTracking = YES;

기타 문제

서로 다른 이벤트를 추적하기 위해 WKTrackingData는 관점 지향 발상에 따라 runtime으로 메서드를 직접 바꿉니다.

하지만 UIAlertView, UIActionSheet, UITableView, UICollectionView를 집계하려면 각각의 delegate 메서드를 바꿔야 합니다.

  • 만약 delegate class가 해당 메서드를 이미 구현했다면 바로 교체하면 됩니다.

  • 만약 delegate class가 해당 메서드를 구현하지 않았는데도 이벤트를 추적하려면 대응하는 delegate method 구현을 수동으로 추가해야 합니다.

- (void)wk_swizzleInstanceSelector:(SEL)origSel_ fromClass:(Class)fromClass replaceSelector:(SEL)replaceSel_ originNotImp:(SEL)notImpSel_ {
    Method originalMethod = class_getInstanceMethod([self class], origSel_);
    if (originalMethod) {
        [self wk_swizzleInstanceSelector:origSel_ fromClass:fromClass replaceSelector:replaceSel_];
    } else {
        Method notImpMethod = class_getInstanceMethod(fromClass, notImpSel_);
        // 如果delegateClass没有实现 origSel_ 方法
        // 则给delegateClass的 origSel_ 添加 orginReplaceMethod 的实现
        BOOL didAddNotImpMethod =
        class_addMethod([self class],
                        origSel_,
                        method_getImplementation(notImpMethod),
                        method_getTypeEncoding(notImpMethod));
        if (didAddNotImpMethod) {
            NSLog(@"%@ did add not imp method %@" , NSStringFromClass([self class]) , NSStringFromSelector(notImpSel_));
        }
    }
}

구체적인 구현은 여기 있습니다

하지만 실제로는 UITableView와 UICollectionView의 delegate 객체가 해당 메서드를 구현하지 않았을 때 tableView:didSelectRowAtIndexPath:에 implementation을 수동으로 추가해도 호출되지 않았습니다.

먼저 정상적인 tableView:didSelectRowAtIndexPath: 호출 스택을 살펴봅시다.

테이블 행을 선택할 때의 당시 디버거 호출 스택

여기서 UITableView는 다음 네 메서드를 순서대로 호출해 cell 누르기에 응답합니다.

  • 1. [UITableView _userSelectRowAtPendingSelectionIndexPath:]
  • 2. [UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:]
  • 3. [UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:]
  • 4. [TableViewController tableView:didSelectRowAtIndexPath:]

호출 순서를 보면 UITableView는 delegate class의 tableView:didSelectRowAtIndexPath:를 실제로 호출하기 전에 먼저 자신의 _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect: 메서드를 실행합니다.

직접 _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:에 중단점을 놓고 어셈블리를 살펴봤습니다.

delegate 메서드 분기를 보여 주는 당시 어셈블리 화면

만약 delegate class가 tableView:didSelectRowAtIndexPath:를 구현했다면 모든 것이 정상적으로 진행되어 tableView:didSelectRowAtIndexPath: 호출을 마칩니다.

하지만 delegate class가 tableView:didSelectRowAtIndexPath:를 구현하지 않았다면, tableView:didSelectRowAtIndexPath:에 implementation을 수동으로 추가해도 tableView:didSelectRowAtIndexPath:에 응답하려 하지 않습니다.

어셈블리 호출을 따라가면 다른 명령으로 곧바로 jump합니다.

WKTrackingData는 여기서 다른 방법을 사용해 [UITableView _userSelectRowAtPendingSelectionIndexPath:]을 직접 교체합니다(UICollectionView도 비슷합니다). 이를 통해 UITableView와 UICollectionView의 delegate 객체가 해당 cell 이벤트 메서드를 구현하지 않아도 이벤트를 추적할 수 있습니다.

4. 마무리

한 이벤트에 여러 event_path가 대응하는 경우

출시된 버전이 여러 개이고 화면마다 작은 차이가 있는 상황을 생각해 봅시다. 같은 이벤트에 여러 event_path가 있을 수 있습니다.

실제로 이 매핑에는 서버 담당자의 협력이 필요합니다. 백엔드에서 설정할 수 있게 만들 수 있습니다.

이벤트 분석 개선

앞의 비즈니스 확장에서는 비즈니스 매개변수를 추가하는 방법을 설명했습니다. WKTrackingData는 이를 카테고리로 구현합니다.

실제로 사용해 보니 전체 이벤트 추적은 많은 트래픽을 낭비했습니다. 필요 없는 이벤트가 너무 많았습니다 [(ŏ_ŏ)].

더 개선하려면 서버가 보고받고 싶은 모든 이벤트가 담긴 설정 파일을 보냅니다. 설정에 맞는 event_path만 업로드합니다.

이때 비즈니스 매개변수는 WKTrackingData의 구현 없이 kvc로 직접 가져올 수도 있습니다.

예를 들면 다음과 같습니다.

key_path = "viewController.banner.url"

여기까지 읽었다면 WKTrackingData를 열어 star를 눌러 주세요. 질문은 GitHub에 issue로 남기거나 아래에 댓글을 달아 주세요!