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

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


참고:

관심 있을 만한 포스트

Vinext — Vite 위에서 Next.js를 1주일 만에 다시 만든 이야기

Cloudflare가 AI와 함께 단 일주일, $1,100의 API 비용으로 Next.js 호환 프레임워크를 Vite 위에 구축한 과정.

VinextNext.js

Cloudflare Workers Static Assets 운용 가이드 — 라우팅과 캐시를 안정적으로 잡는 방법

Workers Static Assets를 기준으로 SPA/SSR 혼합 서비스에서 라우팅, 캐시, Worker 실행 순서를 설계하는 실전 패턴을 정리한다.

Cloudflare WorkersStatic Assets

SVG 아이콘 — 코드 배포 없이 프로덕트 팀이 직접 관리하는 법

CSS mask-image와 S3를 조합해 개발자 개입 없이 아이콘을 교체하는 패턴을 소개한다.

SVGCSS

VS Code 1.116 — 에이전트 디버깅, 포그라운드 터미널, 내장 Copilot

2026년 4월 VS Code 1.116이 에이전트 경험, 터미널, Chat UX, 내장 브라우저를 개선한 핵심 변경사항을 정리한다.

VS Code1.116

Naver FE News 2026년 4월 — 49MB 웹 페이지부터 Temporal Stage 4까지

Naver FE News 2026년 4월호에서 프론트엔드 개발자가 주목할 6가지 소식을 선별해 정리한다.

Naver FE NewsTemporal

VS Code 에이전트 — 실전 개발에서 쓸 수 있게 만드는 세 가지 축

VS Code 1.110이 도입한 컨텍스트 관리, 에이전트 제어, 확장성 기능이 AI 에이전트를 실무에 투입 가능하게 만든 방식을 분석한다.

VS CodeAI Agent

node:vfs — Node.js에 가상 파일 시스템이 필요한 이유

Matteo Collina가 제안한 node:vfs 모듈이 해결하려는 4가지 문제와 아키텍처를 분석한다.

Node.jsVFS

Astro OAuth — Google 로그인부터 캘린더 API 연동까지

Astro에서 OAuth 2.0으로 Google 로그인을 구현하고 Calendar API까지 연동하는 전체 과정을 코드와 함께 정리한다.

AstroOAuth