A/B 실험 노출이 클릭 전에 발화되는 이유

A/B 실험 SDK 렌더 타임 vs 클릭 타임

reactrenderingab-testing

난이도: ★★☆☆☆


핵심 요약

  1. NextLink href={}는 렌더 시점에 값이 결정되어야 한다.
  2. 클릭 시점에 분기하려면 NextLink를 걷어내고 router.push()로 구조 자체를 바꿔야 한다.
  3. 같은 분기 함수를 두 곳에 인라인으로 쓰면 side effect(노출 이벤트)가 2번 발화되므로 변수 하나로 추출해 1회 호출, 2곳 주입한다.
  4. 단, 변수 추출은 “한 번의 렌더 내” 중복만 막는다. re-render가 발생하면 분기 함수가 다시 호출되므로, SDK dedup을 보장하지 않는다면 useRef로 마운트 시 1회만 호출해야 한다.

왜 헷갈렸나

“CTA 클릭 시 실험을 할당해야 하는 거 아닌가?” — 언제 노출 이벤트가 발화되는지 의식하지 않았음. NextLink href={}가 선언적 구조라 변수를 렌더 시점에 결정해야 한다는 것도 놓쳤음.


메커니즘

[클릭 시점 할당 — router.push 방식]

컴포넌트 렌더
  └─ <Button onClick={() => {
         const dest = getVariation() === "B" ? pathB : pathA;  // 클릭 시 결정
         router.push(dest);
       }}>

→ 노출 이벤트: 버튼 클릭 시

[렌더 타임 할당 — NextLink 방식]

컴포넌트 렌더
  └─ const dest = getVariation() === "B" ? pathB : pathA;  // 렌더 시 결정
  └─ <NextLink href={dest}>
         <Button>...</Button>
     </NextLink>

→ 노출 이벤트: 페이지 진입(렌더) 시

두 방식 비교:

NextLink hrefrouter.push
노출 이벤트 시점페이지 렌더버튼 클릭
prefetch자동 (Next.js 최적화)없음
구조 변경 필요 여부없음Button + onClick으로 교체 필요

해결 패턴

// 같은 분기 결과가 두 곳에 필요 — 변수로 추출
const dest = getVariation('exp-key') === "B" ? pathB : pathA;
// getVariation은 호출마다 노출 이벤트 side effect 발화
// 인라인으로 두 곳에 쓰면 이벤트 2회 → 변수로 추출해 1회 호출

// 적용
<NextLink href={dest}>           {/* hero CTA */}
  <Button>가입하기</Button>
</NextLink>

<FixedButton href={dest} />      {/* fixed CTA — 동일 변수 주입 */}

re-render로 인한 중복 발화가 우려된다면 useRef로 마운트 시 1회만 호출:

// useRef 패턴 — 마운트 1회 보장
const destRef = useRef<string | null>(null);
if (!destRef.current) {
  destRef.current = getVariation('exp-key') === "B" ? pathB : pathA;
}
const dest = destRef.current;

// ❌ useMemo는 쓰면 안 됨 — React가 메모이제이션을 버릴 수 있어
// side effect 있는 함수를 useMemo 안에 넣는 건 안티패턴
const dest = useMemo(() => getVariation('exp-key') === "B" ? pathB : pathA, []);

다음에 이 상황을 만나면

트리거: A/B 실험 분기 결과를 CTA 경로로 써야 할 때

  • NextLink를 쓰고 있는가? → 렌더 타임 할당, 변수로 추출
  • 클릭 시 할당이 필요한가? → router.push로 구조 변경 검토
  • 같은 분기 결과를 두 곳 이상에서 쓰는가? → 반드시 변수 추출 (이벤트 중복 발화 방지)
  • re-render가 많이 발생하는 컴포넌트인가? → useRef 패턴으로 마운트 1회 보장
  • SDK가 세션 레벨 dedup을 보장하는지 확인했는가? → 불확실하면 useRef 적용

커넥팅 닷

← 선행 개념 (이걸 알아야 이해된다)

  • React 렌더링 사이클 — 컴포넌트 함수 본문은 렌더마다 실행된다. 변수 선언부도 렌더 타임에 평가된다
  • Next.js NextLinkhref를 선언적으로 받아 prefetch + 내비게이션을 처리. href는 렌더 시점에 확정된다

→ 확장 개념 (여기서 더 나아가면)

  • A/B 실험 SDK 노출 이벤트 설계 — “노출”을 어떤 시점으로 정의하느냐가 실험 결과 해석에 영향을 준다 (페이지 진입 vs 버튼 클릭 vs 전환)
  • React useMemo — 렌더마다 재계산되는 값 중 비용이 있는 연산을 메모이제이션. side effect가 있는 함수라면 useMemo 안에 넣어선 안 된다

↔ 같은 원리가 적용되는 곳

  • 비동기 반복문에서 React 상태가 꼬이는 이유 — side effect 있는 함수를 잘못된 위치에서 호출하면 예상치 못한 중복 실행이 발생한다는 같은 원리
  • React useEffect 의존성 배열 — “언제 실행되는가”를 명시적으로 제어해야 하는 같은 맥락