Runtime

Cloudflare Workers로 A/B 테스트 — 엣지에서 실험 분기하기

클라이언트 사이드 A/B 테스트가 렌더링을 막는 문제를 Cloudflare Workers로 해결하는 방법을 정리한다.

10 min read
Cloudflare WorkersA/B TestingEdge ComputingPerformance
Cloudflare Workers로 A/B 테스트 — 엣지에서 실험 분기하기

클라이언트 사이드 A/B 테스트의 문제

핵심: A/B 테스트 스크립트가 렌더링을 차단하면, 사용자는 빈 화면을 본다.

전통적인 A/B 테스트 도구는 대부분 클라이언트 사이드 JavaScript로 동작한다. 페이지가 로드될 때 스크립트가 실행되고, 사용자를 실험 그룹에 배정한 뒤, DOM을 조작해서 변형(variant)을 보여주는 방식이다. 문제는 이 스크립트가 <head>에 렌더링 차단(render-blocking) 방식으로 삽입된다는 점이다. 느린 네트워크에서는 이 스크립트 하나 때문에 수백 밀리초의 빈 화면이 발생한다.

비유하면 이렇다. 놀이공원 입구에서 팔찌 색깔을 정해야 하는데, 팔찌 담당자가 늦게 와서 모든 입장객이 대기하는 상황이다. 팔찌 색깔 정하는 일을 입구가 아니라 **티켓 발권 시스템(서버/엣지)**에서 미리 해두면, 입장객은 기다릴 필요가 없다.


엣지에서 A/B 테스트한다는 건 뭔가?

방식실험 배정 시점렌더링 차단깜빡임(FOUC)
클라이언트 사이드브라우저 로드 후있음있음
서버 사이드서버 응답 전없음없음
엣지 (Workers)CDN 레벨없음없음

Cloudflare Workers는 요청이 오리진 서버에 도달하기 전에 CDN 엣지에서 실행된다. 여기서 실험 그룹을 배정하고, 해당 그룹에 맞는 콘텐츠를 라우팅하면 된다. 클라이언트에 추가 스크립트가 필요 없으므로, 렌더링을 전혀 막지 않는다.

[💡 잠깐! 이 용어는?] 엣지 컴퓨팅(Edge Computing): 사용자와 가까운 CDN 노드에서 로직을 실행하는 방식. 오리진 서버까지 왕복하지 않아도 되므로 응답이 빠르다.


3단계 프레임워크

Philip Walton이 제안한 엣지 A/B 테스트의 핵심 흐름은 딱 세 단계다.

1단계: 사용자를 랜덤 그룹에 배정한다

Worker가 요청을 가로채서 xid(experiment ID) 쿠키를 확인한다. 쿠키가 없으면 새로 생성하고, 해시 함수로 0~99 사이의 숫자를 만들어 실험 그룹을 결정한다.

worker.js — 실험 그룹 배정
function getOrCreateExperimentId(request) {
  const cookieHeader = request.headers.get('Cookie') || ''
  const match = cookieHeader.match(/(?:^|;\s*)xid=([^;]+)/)
 
  if (match) {
    return match[1]
  }
 
  const bytes = new Uint8Array(16)
  crypto.getRandomValues(bytes)
  return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')
}
 
function getExperimentGroup(xid, numGroups) {
  let hash = 0
  for (let i = 0; i < xid.length; i++) {
    hash = (hash * 31 + xid.charCodeAt(i)) & 0xffffffff
  }
  return Math.abs(hash) % numGroups
}

중요한 건 같은 xid는 항상 같은 그룹에 속한다는 점이다. 사용자가 새로고침해도, 다른 페이지로 이동해도 동일한 실험 경험을 유지한다.

2단계: 그룹에 맞는 콘텐츠를 서빙한다

디렉토리 구조로 변형을 관리한다. 컨트롤 그룹은 원본 경로를, 실험 그룹은 변형 서브디렉토리의 콘텐츠를 받는다.

디렉토리 구조
/
├── index.html              ← 컨트롤 그룹 (원본)
├── /variant-1/
│   └── index.html          ← 실험 그룹 1
└── /variant-2/
    └── index.html          ← 실험 그룹 2

Worker는 그룹 번호에 따라 요청 URL을 재작성(rewrite)한다.

worker.js — 요청 라우팅
async function handleRequest(request) {
  const url = new URL(request.url)
  const xid = getOrCreateExperimentId(request)
  const group = getExperimentGroup(xid, 3)
 
  if (group === 1) {
    url.pathname = `/variant-1${url.pathname}`
  } else if (group === 2) {
    url.pathname = `/variant-2${url.pathname}`
  }
 
  const response = await fetch(url.toString(), request)
  const newResponse = new Response(response.body, response)
 
  newResponse.headers.append(
    'Set-Cookie',
    `xid=${xid}; Path=/; Max-Age=31536000; SameSite=Lax`
  )
 
  return newResponse
}
 
addEventListener('fetch', (event) => {
  event.respondWith(handleRequest(event.request))
})

3단계: 분석 데이터와 연동한다

xid 쿠키 값을 Google Analytics나 다른 분석 도구에 커스텀 디멘션으로 전송하면, 그룹별 지표를 비교할 수 있다. Worker에서 응답 HTML에 분석 스크립트의 설정값을 주입하는 방식도 가능하다.

[💡 잠깐! 이 용어는?] 커스텀 디멘션(Custom Dimension): Google Analytics 등에서 기본 제공하지 않는 사용자 정의 분류 기준. 실험 그룹 ID를 보내면 그룹별 데이터를 분리해서 볼 수 있다.


캐시 적중률의 함정

엣지에서 A/B 테스트를 할 때 놓치기 쉬운 부분이 캐시 적중률(cache hit rate) 차이다.

그룹트래픽 비율캐시 적중률
컨트롤 (원본)~90%11.07%
Variant 1~5%1~2%
Variant 2~5%1~2%

컨트롤 그룹은 트래픽의 대부분을 차지하므로 캐시가 따뜻하게 유지된다. 반면 변형 그룹은 트래픽이 적어서 캐시 미스가 자주 발생한다. 이 차이가 성능 지표에 그대로 반영되면, "변형이 더 느리다"는 잘못된 결론을 내릴 수 있다.

해결 방법은 Render Time 지표를 쓰는 것이다.

Render Time = FCP - TTFB

FCP(First Contentful Paint)에서 TTFB(Time to First Byte)를 빼면, 네트워크/캐시 차이를 제거하고 순수하게 브라우저가 렌더링에 걸린 시간만 비교할 수 있다.

[💡 잠깐! 이 용어는?] FCP(First Contentful Paint): 브라우저가 텍스트나 이미지 등 첫 번째 콘텐츠를 화면에 그린 시점. TTFB(Time to First Byte): 요청을 보낸 뒤 서버로부터 첫 바이트가 도착한 시점. 네트워크 + 서버 처리 시간을 반영한다.


실험 결과: 렌더링 차단 CSS의 영향

Philip Walton은 실제로 렌더링을 차단하는 CSS를 변형 그룹에 주입해서 영향을 측정했다. 결과는 명확했다.

지표컨트롤변형 (blocking CSS)차이
Render Time (p75)~320ms~560ms+240ms
Render Time (p95)~680ms~1,020ms+340ms

p75에서 약 240ms, p95에서 약 340ms의 차이가 발생했다. 클라이언트 사이드 A/B 테스트 스크립트가 렌더링을 차단하면 이 정도 성능 손실이 발생한다는 걸 수치로 확인한 셈이다.


모범 사례

엣지 A/B 테스트를 실전에 적용할 때 기억할 포인트를 정리한다.

항목권장 사항
에셋 공유CSS/JS/이미지는 변형 간 공유하고, HTML만 분기한다. 중복 에셋은 캐시 효율을 떨어뜨린다.
동시 실험 수2~3개 이하로 제한한다. 실험이 많을수록 캐시가 분산되고 분석이 복잡해진다.
쿠키 수명실험 기간보다 길게 설정한다. 중간에 쿠키가 만료되면 사용자가 다른 그룹으로 이동할 수 있다.
성능 지표TTFB가 아닌 **Render Time(FCP - TTFB)**을 기준으로 비교한다.
트래픽 배분변형 그룹에도 충분한 트래픽을 배분해야 통계적 유의성을 확보할 수 있다.

정리

  • 클라이언트 사이드 A/B 테스트는 렌더링을 차단하고, 느린 네트워크에서 빈 화면을 유발한다.
  • Cloudflare Workers를 사용하면 엣지에서 실험 그룹을 배정하고 콘텐츠를 라우팅할 수 있다. 클라이언트에 추가 스크립트가 필요 없다.
  • 캐시 적중률 차이를 보정하려면 Render Time(FCP - TTFB) 지표를 사용한다.
  • 변형 간 에셋을 공유하고, 동시 실험 수를 제한하는 것이 캐시 효율과 분석 정확도를 높이는 핵심이다.

결국 A/B 테스트의 목적은 사용자 경험 개선인데, 테스트 도구 자체가 경험을 해치면 본말이 전도된 거다. 엣지에서 처리하면 이 모순을 깔끔하게 해결할 수 있다.


참고: