[데브인턴십 3기] 헬로우봇 스킬스토어 웹 프론트엔드 개발 후기

성정민

업데이트:

A/B TEST

A/B 테스트는 디지털 환경에서 두개의 변형 A(대조군, 기존안)와 B(실험군, 수정안)를 사용하는 종합 대조 실험입니다. 주관적인 판단, 리소스 낭비를 최소화하고 정량적인 데이터 기반의 합리적이고 신뢰할 수 있는 지표로 더 나은 기획안을 채택할 수 있습니다. 실험을 하는 배경은 어떤 것이고 이루고자 하는 목적은 무엇인지 정하는 설계 단계가 필요합니다. A/B test 설계 단계부터 웹 프론트엔드에 테스트를 적용시키고 그 결과까지 실제 개발했던 과정을 통해서 설명하겠습니다.

A/B TEST Process

abtest-process

A/B TEST 설계

실험 설계 부분은 개발자가 필수적으로 알아야 할 부분은 아닐 수도 있습니다. 하지만 실험을 설계한 기획 목적을 알고 있어야 협업자와 능동적인 커뮤니케이션이 가능하고, 올바른 곳에서 이벤트를 수집할 수 있는 로직을 적용시키기 용이합니다. 또한 실험이 끝난 후 결과의 성공/실패에 따라 실험을 개선할 수 있는 새로운 후속 가설을 새울 수도 있습니다. 따라서 개발 이 외의 A/B TEST의 전반적인 흐름을 파악하실 수 있도록 개발자의 시선에서 A/B TEST가 서비스로 적용되기까지의 전과정을 정리해 보겠습니다. (헬로우봇은 해당 실험 문서를 담당자분께서 설계해서 전달해 주면 개발자가 해당 실험 로직에 그룹을 분배하고 이벤트를 올바른 곳에서 수집할 수 있도록 적용시킵니다.)

지면 구현 & 트래픽 분배

login-test

실험 배경

로그인 화면 개선을 통해 전환율을 높이자

  1. 현재 로그인 화면에 이메일 로그인이 메인으로 노출되고 있어, 사용자들이 가입/로그인 절차에 부담을 느낄 것이다.
  2. 간편 로그인을 전면에 배치하여 가입/로그인 절차에 대한 부담을 줄이자.
  3. 해당 수정 작업은 UI 변경이므로 클라이언트에서 A/B 테스트를 적용 시킨다.

A/B TEST 호스트 환경 선정 기준

A(대조군) 대비 B(실험군)에서 수정될 부분이 어느 부분인지에 따라 A/B test를 적용할 호스트 환경이 달라집니다. 서버 API Data라면 서버에서 테스트를 적용하고, 클라이언트 Data 혹은 UI의 변경이 적용되어야 한다면 클라이언트에서 적용합니다. 이 글은 웹 프론트엔드 환경에서의 A/B test 적용 후기에 대해서 설명하겠습니다.(헬로우봇 스킬스토어는 Angular로 구현되어 있습니다.)

실험 타깃

스킬스토어(헬로우봇 웹 버전) 로그인 페이지 진입 사용자

실험 핵심 가설

간편 로그인을 메인으로 노출하는 경우 로그인 화면 전환율이 20% 상승할 것이다.

실험 목표(실험이 성공하면 어떤 지표가 변하는가?)

  • Primary Metric: 가장 직접적으로 영향을 미치는 성공 여부를 가늠하는 지표
    • login_or_signup_success (가입/로그인 전환율)
  • Secondary Metric: 실험에 의해 간접적으로 영향을 받는 지표 (2-3개 정도)
    • signup_success (가입 전환율)
    • login_success (로그인 전환율)

A/B TEST 준비

설계 문서를 바탕으로 핵클을 통해 A/B 테스트를 준비합니다.

새 A/B 테스트를 생성합니다.

new-test

이벤트를 생성합니다.

create-event

목표 설정

goal

등록한 이벤트를 기반으로 얻고자 하는 지표를 설정합니다.

해당 SDK 정보를 확인합니다. 각 언어에 적용할수 있는 테스트 그룹 분배, 이벤트 수집 등의 적용 예시를 확인 할수 있습니다.

hackle-sdk

test-group

event-track

A/B TEST 개발

그룹 분배 로직을 개발 환경에 적용 시킵니다.

  • 로그인 A/B TEST 일부 디렉토리 구조
.
└── src
    ├── app
    │   ├── core
    │   │   ├── reslovers
    │   │   │   └── abtest-login-resolver.service.ts
    │   │   └── services
    │   │       ├── hackle-service.ts
    │   │       └── analytics.service.ts
    │   └── modules
    │       └── login
    │           ├── join
    │           └── login
    │               ├── abtest-login-oauth-b
    │               ├── abtest-login-oauth-c
    │               ├── abtest-sign-in-email
    │               ├── login
    │               ├── login-wrapper
    │               ├── login-routing.module.ts
    │               └── login.module.ts
    └── asset
        └── images
            └── login

테스트 그룹 분배하기

핵클 SDK를 사용하여 생성한 특정 테스트에 대한 그룹 분배 variation 값을(A/B/C) 비동기로 받아 올수 있습니다.

// hackle-service.ts

export type ABTestGroup = 'A' | 'B' | 'C';
export interface HackleABTestVariation {
  key: number;
  variation: ABTestGroup;
}

@Injectable({
  providedIn: 'root'
})
export class HackleService {

  client?: HackleClient;
  // 핵클 A/B test 고유 실험 키 
  readonly loginTestKey: number = 41; // ABTEST: #41 로그인 페이지 테스트용. 로그인 페이지의 이벤트를 수집한다.

    @Inject(PLATFORM_ID) private platformId: Object,
  ) {
    if (isPlatformBrowser(this.platformId)) {
      this.client = Hackle.createInstance(environment.hackleBrowserKey);
    }
  }

  getTestGroup(testKey: number, userSeq?: number): Observable<ABTestGroup> {
    return new Observable<ABTestGroup>(observer => {
      this.client.onReady(() => {
        const user = { id: userSeq?.toString() }
        const variation = userSeq ? this.client.variation(testKey, user) : this.client.variation(testKey);

        if (variation === 'A') {
          observer.next('A');
        } else if (variation === 'B') {
          observer.next('B');
        } else if (variation === 'C') {
          observer.next('C');
        }
      });
    });
  }
}

hackle-service.ts 파일은 핵클 테스트 그룹 분배 로직과 테스트의 고유번호를 포함하고 있습니다. 기존에 A/B test를 작업을 위해 구현 되어 있는 클래스에 새로운 고유번호를 추가해줍니다.

// abtest-login-resolver.service.ts

@Injectable({
  providedIn: 'root'
})
export class AbtestLoginResolverService implements Resolve<ABTestGroup> {
  constructor(
    @Inject(PLATFORM_ID) private platformId: Object,
    private hackleService: HackleService,
  ) { }
  resolve(
    route: ActivatedRouteSnapshot, 
    state: RouterStateSnapshot): ABTestGroup | Observable<ABTestGroup> | Promise<ABTestGroup> {
      if (isPlatformServer(this.platformId)) {
        return;
      }
      return this.hackleService.getTestGroup(this.hackleService.loginTestKey).pipe(take(1));
  }
}

핵클 클래스에서 특정 고유키(스킬 스토어 로그인 테스트)에 대한 variation 값을 받기 위한 서비스를 생성해 줍니다. 스킬스토어에서는 SDK를 이용한 비동기 값을 통해 보여지는 컴포넌트를 에러 없이 렌더링 시켜주기 위해서 Route의 resolve interface를 활용하는 패턴으로 작성하고 있습니다. Reslove로 Route의 View가 렌더링 되기 이전에 View가 렌더링 되기 위해 반드시 필요한 데이터를 로딩할 수 있습니다. 스킬스토어는 SSR이 적용되어 있어 서버사이드에서 hackle SDK에 접근하지 않도록 isPlatformServer를 통해 SSR 에러를 예방하고 있습니다.

// login-routing.module.ts

const routes: Routes = [
  {
    path: '',
    component: LoginWrapperComponent,
    resolve: {
      abtestGroup: AbtestLoginResolverService,
    },
    data: {
      meta: {
        title: '로그인',
      },
      layoutOptions: {
        hasHeader: true,
        headerType: 'onlyLogo',
        hasFooter: false,
      },
    },
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class LoginRoutingModule { }
// login-wrapper.component.ts

@Component({
  selector: 'hb-login-wrapper',
  templateUrl: './login-wrapper.component.html'
})
export class LoginWrapperComponent implements OnDestroy {

  private unsubscribe$ = new Subject<void>();
  abTestGroup: ABTestGroup;

  constructor(
    private route: ActivatedRoute,
  ) { 
    this.route.data.pipe(
      takeUntil(this.unsubscribe$),
    ).subscribe(data => {
      this.abTestGroup = data.abtestGroup;
    });
  }

  ngOnDestroy(): void {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }

}
// login-wrapper.component.html

<ng-container *ngIf="abTestGroup === 'C'; else abTestLoignOauthB">
  <hb-abtest-login-oauth-c></hb-abtest-login-oauth-c>
 </ng-container>

<ng-template #abTestLoignOauthB> 
   <ng-container *ngIf="abTestGroup === 'B'; else login"> 
     <hb-abtest-login-oauth-b></hb-abtest-login-oauth-b> 
 </ng-container>
</ng-template>

<ng-template #login>
  <hb-login></hb-login>
</ng-template>

LoginWrapperComponent 로 라우팅 되기 이전에 resolve를 json 형태로 설정하며, 설정된 key값(abtestGroup)을 사용해서 component가 값을 가져가게 됩니다. abTestGroup으로 할당된 variation 값에 해당하는 컴포넌트를 렌더링 시킵니다.

수정안 컴포넌트 개발

기본적으로 해당 실험을 시작하기 이전에는 그룹 분배 값이 A로 설정됩니다. 수정안에 대한 컴포넌트를 개발하기 위해서 크롬 개발자 모드를 통해 핵클 이벤트의 userId 값을 확인하고 개발할 그룹에 테스트 기기 등록을 해준 후 수정안 컴포넌트를 개발합니다.

user-id

test-id

이벤트 수집

// abtest-login-oauth-b.component

// ABTEST: #41 로그인 페이지 테스트용. 로그인 페이지의 이벤트를 수집한다.
  private setAbtestHackleButtonTrack(type:string) {
    if(!isPlatformBrowser(this.platformId)){
      return;
    }
    if(type === "join") {
      this.analyticsService.setAbtestHackleLoginTrack('click_signup_button_type_email');
    }
    else {
      this.analyticsService.setAbtestHackleLoginTrack('click_login_button_type_'+type)
    }
    this.analyticsService.setAbtestHackleLoginTrack('click_login_or_signup_button')
  }
// analytics.service 일부

// 로그인 or 회원가입 API 성공 반환 시
  triggerSignInEvent(user: User, isSignup = false) {
    if(isSignup === false) {
      // ABTEST: #41 로그인 페이지 테스트용. 로그인 페이지의 이벤트를 수집한다.
      this.setAbtestHackleLoginTrack('login_success');
      this.setAbtestHackleLoginTrack("login_or_signup_success");
    }
  }

// ABTEST: #41 로그인 페이지 테스트용. 로그인 페이지의 이벤트를 수집한다.
  setAbtestHackleLoginTrack(type : string){
    this.hackleService.client.onReady(()=> {
      this.hackleService.client.track(type)
    })
  }

A/B/C 컴포넌트에 실험을 설계한 의도에 맞게 이벤트를 올바른 곳에서 수집해 줍니다.

event-graph

핵클의 데이터 인사이트 이벤트 탭을 통해 해당 이벤트가 잘 수집되고 있는지 개발 환경에서도 확인할 수 있습니다.

A/B Test 코드 적용 시 고려할 점

  • 결국은 A/B test는 전환율이 높은 안으로 Fix 되기 때문에 그룹 분배 로직을 구현할 때는 걷어내기 쉽게 코드를 짜는 것이 좋습니다. 기존안을 최대한 유지시킨 형태로 코드를 구현합니다.
  • A/B test 관련된 코드의 경우 파일명/함수명/변수명 등에서 abTest 코드임을 나타낼 수 있게 작성합니다.
  • A/B test와 관련된 코드는 ABTEST: #testKey로 시작하는 주석으로 표시하여 유지/보수가 용이하도록 합니다.
  • 특정 안에서만 이벤트가 수집되어야 함으로 재사용 컴포넌트를 활용 시 주의해야 합니다. 성능에 큰 영향을 주지 않은 코드는 컴포넌트를 분리시켜 작업하는 것이 올바른 이벤트 수집과 코드 유지/보수에 용이합니다.
  • Hackle SDK을 활용해 비동기로 값을 받아오기 때문에 SSR을 적용 시킨 환경에서는 SSR 에러를 발생시키지 않도록 구현합니다.

QA, 배포 후 실험 런칭

test-ready

test-start

운영 환경에 배포 후 핵클에 설정한 환경에 맞게 자동으로 트레픽을 분배 시키기 위해서 운영 탭에서 실험을 활성화시켜주어야 합니다. 설계 문서를 바탕으로 트래픽을 분배 후 실험을 런칭합니다.

실험 결과

test-result

test-data

test-graph

결과는 C안이 가장 높은 전환율을 보였고 실험의 성공 여부를 가늠할 수 있는 가입/로그인 전환율이 기존안 대비 36.92% 상승하였고, B안 대비 10.92% 높은 전환율로 기존에 정했던 가설을 넘어서는 수정안이 나오고 있습니다.(아직 진행 중인 실험)

AB TEST 후기

두 번의 A/B test를 진행하면서 느꼈던 점은 확실히 주관적인 판단으로는 사용자에게 더 나은 UX를 제공하기 어렵다는 것이었습니다. 예상했던 결과와 크게 벗어나는 결과를 얻은 케이스도 있을 뿐 아니라, ‘단순 디자인의 변화로 실제 더 많은 로그인/가입 전환율을 높일 수 있을까?’라는 의문도 실험을 통해서 해소할 수 있었습니다. AB TEST의 장점은 정량적인 지표를 기반으로 더 나은 안을 채택하고 원하고자 하는 바를 검증할 수 있는 것 같습니다. 이런 A/B TEST를 지속적으로 서비스에 반영 시키면 더욱더 좋은 UX를 제공할 수 있을 뿐만 아니라 개선안에 대한 데이터도 축적되어서 더욱 고도화된 실험을 할 수 있을 것 같다는 생각이 들었습니다.

인턴, 포스팅 회고

두 달이라는 시간동안 처음으로 앵귤러를 통해 개발을 해보았고 a/b test도 적용해 보았는데 사실 원온원이 없었으면 거의 불가능 할만한 작업이었습니다. 헬로우봇 스킬스토어에서 웹 프론트엔드 개발을 하고 계신 세림님과 주기적인 원온원을 통해 스프린트를 무사히 쳐낼수 있었습니다. 실제 서비스 코드였기 때문에 코드 양이 상당히 많게 느껴졌고 언어도 생소해서 로직이 잘 읽히지 않은 것도 많았습니다. 그래서 처음 개발을 할때는 어느 코드를 어떻게 활용해야 할지 감이 잡히지 않았습니다. 원온원을 통해서 기존에 만들어져 있는 코드를 파악하는 데 도움을 얻을 수 있었고 잘 구축되어 있는 a/b test 관련 로직과 패턴을 이해하고 활용할 수 있었습니다. 이 포스팅은 헬로우봇 그룹장이신 지영님이 설계해 주신 a/b test 설계문서와 세림님께서 스킬스토어에 구축해 놓으신 a/b test 설계 패턴을 적용시키며 개발하면서 이해한 전체적인 흐름을 정리한 글입니다. 스스로 고민하고 개발한 다른 주제에 대한 글을 쓸수도 있었겠지만 개인적인 경험과 회고의 위주의 글보단 독자들에게 좀 더 유용한 정보제공을 목적으로 작성하였습니다 :)

띵스플로우 팀은 자기의 일을 좋아하고 잘하는 사람들 입니다. 사용자와 서비스를 중심으로 빠르게 실행하고 학습하며, 다양한 직무의 사람들이 협업을 통해 시너지를 내고 있습니다. 다양한 콘텐츠 혁신을 이루고 있는 띵스플로우 팀에 함께할 분을 찾습니다! 언제든 people@thingsflow.com로 이메일을 주시기 바랍니다!

태그: ,

카테고리:

업데이트:

댓글남기기