> For the complete documentation index, see [llms.txt](https://taewoongs-organization.gitbook.io/jtwjs-dev-wiki/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://taewoongs-organization.gitbook.io/jtwjs-dev-wiki/dev_note/system-design/a-b.md).

# A/B 테스팅

## A/B 테스팅

레이아웃이나 컨텐츠의 특정 변경이 방문자의 참여도(리텐션,구매율 등)을 어떻게 향상시키는지 평가

{% hint style="info" %} <mark style="color:orange;">**리텐션**</mark>이란 사용자가 특정 제품, 서비스, 플랫폼 등을 얼마나 오랫동안 계속해서 이용하는지를 나타내는 지표
{% endhint %}

* A/B 버전을 나눠 사용자의 반응을 측정하는것이 A/B 테스트
  * 트래픽 유도
  * 일관성 유지를 위해 선택 버전 식별자(쿠키) 유지
  * 나갔다 오는 경우, 새로고침에도 동일 컨텐츠(A/B)를 보여주기 위함

### A/B 테스팅을 하는 이유

* 무엇이 더 나은 성과를 가져다 주는지 파악
* 전환율, 이탈율, 신뢰도, 수익 창출같은 지표를 무엇이 어떻게 유도하는지 파악
* 보다 나은 결과를 가져다 주는 변화를 적용
* 데이터 기반의 객관적 의사결정을 하기 위한 수단

### A/B 테스팅 구현하기 (with Next.js)

* Next.js의 middleware 개념 이용
* 다른 A/B 테스트 프레임워크와 비교할때 개발자 워크플로에 적합
* Next.js의 성능 이점을 유지

{% hint style="info" %}
**Next.js Middleware란?**

* 사용자가 작성할 수 있는 임의의 함수
* Http request가 Next.js 서버에 도달하기 전에 실행
* Vercel과 같은 플랫폼의 클라우드 엣지(i.e CDN)에 배포
* [엣지](https://nextjs.org/docs/pages/api-reference/edge)에서 실행되기에 요청을 신속하게 재작성
  * 서버로 리라우팅
  * 직접 응답해서 서버를 단락
* 빠른 성능으로 인한 런타임 환경의 제한
* 빠른 봇 탐지, 피처 플래그, analytics, 라우팅, A/B 테스트 등에 사용
  {% endhint %}

### Next.js middleware를 이용한 A/B 테스팅

<figure><img src="/files/WdtGgJCBaXHCw42FQsps" alt=""><figcaption></figcaption></figure>

* 미들웨어 기능은 Edge(예: CDN)에서 실행
* 요청을 신속하게 재라우팅
* SSG와 함께 사용하여 A/B 테스트를 엣지에서 제공
* 처음 접근시 어떤 페이지를 보여줄지 선택
* 요청을 변경하여 프록시하고 쿠키를 설정
* SSG의 경우 Edge를 통해 제공
* SSR의 경우 서버에서 제공
  * SSR을 사용한다 하더라도 Edge에서 경로가 제공되면 사용자는 컨텐츠의 대한 지연시간이 단축되는 이점을 누릴 수 있음
* Edge에서 라우팅 시 컨텐츠 지연시간 단축

### 다른 A/B 테스팅과 접근방식 비교

#### 클라이언트 사이드 A/B testing

* Google Optimize, Adobe Target, VWO 등 사용
* 페이지에 스크립트 태그 추가
* 쉽게 구현
* 스크립트가 서버에 연결, 버킷 확인, DOM 동적 수정
  * 이로인한 지연 깜빡임 상황 발생
  * Web vitals, SEO에 영향

#### 서버사이드 A/B testing

* 개발자가 원하는 방식으로 구현 가능
* A/B 테스트 서비스에 요청하여 버킷 결정, 이벤트 보고
* 모든 요청이 오리진 서버에 도달해야 함

  * 지연시간 길어질 수 있음
  * 두 사용자가 동일 URL에서 다른 응답을 볼 수 있음
  * CDN 캐싱의 효율이 떨어짐

#### 미들웨어를 이요한 A/B testing

* 개발자가 원하는 방식으로 구현 가능
* 엣지에서 버킷화
  * 사용자에게 짧은 지연시간 제공
* 초기 요청에 대한 버킷 처리
  * 미들웨어가 초기 http 요청에서 바로 사용자 버킷을 결정 -> 추가적인 요청 필요 X
  * 클라이언트 방식 대비 로드시간의 감소
* Static Site Generation (SSG) 활용
  * page variant가 SSG로 빌드되어 Edge에서 제공시 CDN 성능 이점을 활용해 A/B 테스트를 전적으로 Edge에서만 실행할 수 있음 ->  빠른 응답 가능

## A/B 테스팅 구현

### 미들웨어 추가

* ex: /marketing/\* 경로에 대한 미들웨어 적용
* 쿠키가 존재한다면 버켓 유지, 아니라면 버켓 선정
* variant로 프록시하여 응답 rewrite
* 미들웨어는 올바른 버킷을 선택하고 사용자에게 올바른 바리언트로 투명하게 라우팅

```typescript
import { NextRequest, NextResponse } from 'next/server'

import COOKIE_NAME = 'bucket-marketing'

// 랜덤 버켓 선택
// Optional: 써드파티 서비스를 이용하여 버킷을 받을 수도 있다.
const MARKETING_BUCKETS = ['original', 'a', 'b'] as const
const getBucket = () => MARKETING_BUCKET[Math.floor(Math.random() * 
MARKETING_BUCKET.length)]

export function middleware(req: NextRequest) {
  if (req.nextUrl.pathname.startWith('/marketing')) {
    const cookies = req.cookies;
    // Get the bucket cookie
    const bucket = cookies.get(COOKIE_NAME)?.value || getBucket()
    
    // 적절한 variant로 프록시
    const res = NextResponse.rewrite(new URL(`/marketing/${bucket}`, req.url))
    
    // 쿠키가 없다면 버킷을 추가
    if (!cookies.get(COOKIE_NAME)) {
      res.cookies.set(COOKIE_NAME, bucket)
    }
    
    return res
  }
}
```

### Variant 구성

테스트 하려는 지면이 `props/marketing.tsx` 라고 가정\
`page/marketing` 디렉토리 생성 후 해당 지면 이동

* Variant 구성 (A,B 또는 A,B,C)
* `/pages/marketing/original.tsx`
  * variant original (원래구성)
* `/pages/marketing/a.tsx`
  * variant a
* `/pages/marketing/b.tsx`
  * variant b
* 키 값에 따라 화면이 달라지며, 쿠키 존재시 동일 버켓으로 rewirte

{% hint style="info" %}
getStaticProps를 통해 SSG를 사용한다면 최상의 성능을 위해 각각의 변형은 CDN Edge 내에서 제공될것
{% endhint %}

### [Configcat](https://configcat.com/)을 사용한 구현

* middleware는 동일한 역할
* configcat의 variant 값들을 불러오는 lib/config.json 은 빌드타임에 구성

```typescript
import { type NextRequest, NextResponse } from 'next/server';
import { type FlagMatcher, getValue, type Flags } from '@lib/configcat';

const FLAGS: FlagsMatcher = {
  '/about': {
    cookie: 'flag-newAboutPage',
    name: process.env.FEATURE_FLAG_ABOUT_PAGE as Flags,
    rewrite: (enabled) => (enabled ? '/about/b' : '/about'),
  },
  '/marketing': {
    cookie: 'flag-newMarketingPage',
    name: process.env.FEATURE_FLAG_MARKETING_PAGE as Flags,
    rewrite: (enabled) => (enabled ? '/marketing/b' : '/marketing')
  }
}

export const config = {
  matcher: ['/about', '/marketing'],
}

export async function middleware(req: NextRequest) {
  const url = req.nextUrl;
  const flag = FLAGS[url.pathname]
  
  if (!flag) return
  
  const value = req.cookies.get(flag.cookie)?.value || (getValue(flag.name) ? '1' : '0')
  
  // Create a rewrite to the page matching the flag
  url.pahtname = flag.rewrite(value === '1')
  const res = NextResponse.rewrite(url)
  
  // Add the cookie to the response if it's not present
  if (!req.cookies.has(flag.cookie)) {
    res.cookies.set(flag.cookie, value)
  }
  
  return res
}
```

### ConfigCat 구성

* 회원가입 후 대시보드에서 4개의 FeatureFlag 설정
  * isMyFirstFeatureEnabled (on)
  * clientSideFeatureEnabled (on)
  * newAboutPage
    * 50% 비율로 on/off 결정
  * newMarketingPage
    * 50% 비율로 on/off 결정

## 개발 피쳐에 대한 검증 및 롤백전략 수립

> Amplitude를 이용한 A/B 테스트 결과 수집 및 분석

### Amplitude?

* 서비스의 사용자 행동을 분석할 수 있는 도구
* 이벤트 기준의 정보, 사용자의 퍼널/리텐션 등을 다양한 방법으로 볼 수 있는 도구

### 구성

* 회원가입 후 수집할 이벤트로 `visited`와 `button_clicked`를 추가
* Amplitude의 API Key를 받아 ".env.local"의 `NEXT_PUBLIC_AMPLITUDE_API_KEY` 항목에 적용

### 설치

* amplitude-js SDK 설치: [npm](https://www.npmjs.com/package/amplitude-js)
* @types/amplitude-js로 설치된 type 오류 처리

### 사용을 위한 커스텀 훅

* amplitude initialize를 위한 `useAmplitudeInit()`
* API Key를 환경변수로부터 받아서 init() 수행

```typescript
import { useEffect } from 'react';
import amplitude from 'amplitude-js'

const AMPLITUDE_API_KEY = process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY ?? ''

const useAmplitudeInit = () => {
  useEffect(() => {
    amplitude.getInstance().init(AMPLITUDE_API_KEY)
  })
}

export default useAmplitudeInit
```

* `_app.tsx` 에서 initialize 동작 수행

```typescript
export default function MyApp({ Component, pageProps }: AppProps) {
  const Layout = getLayout<LayoutProps>(Component)
  // Amplitude init
  useAmplitudeInit()
}
```

* event를 Amplitude로 보내기 위한 hook 구성
* About page에서 다음 이벤트를 발송
  * `visited`
  * `button_clicked`

```typescript
import { useCallback } from 'react';
import amplitude from 'amplitude-js';

import Cookies from 'js-cookie'

const useAmplitudeAboutEvents = () => {
  const logVisited = useCallback(() => {
    const value = COokies.get('flag-newAboutPage')
    const identity = new amplitude.Identify();
    identify.set('flag-newAboutPage', value === '1')
    
    amplitude.getInstance().identify(identify);
    amplitude.getInstance().logEvent('visited')
  }, [])
  
  const logButtonCLicked = useCallback(() => {
    amplitude.getInstance().logEvent('button_clicked)
  }, [])
  
  return { logVisited, logButtonClicked }
}

export default useAmplitudeAboutEvents
```

* About 페이지에서 visited 이벤트를 처리하기 위한 logVisited() 및 버튼 클릭 이벤트  logButtonClicked() 호출
* about/index.tsxm about/b.tsx 모두 적용

### Amplitude로의 테스트 시각화

> Amplitude를 이용하여 test 결과를 시각화하여 표시
