A/B 실험 노출이 클릭 전에 발화되는 이유
A/B 실험 SDK 렌더 타임 vs 클릭 타임
난이도: ★★☆☆☆
핵심 요약
NextLink href={}는 렌더 시점에 값이 결정되어야 한다.- 클릭 시점에 분기하려면 NextLink를 걷어내고
router.push()로 구조 자체를 바꿔야 한다. - 같은 분기 함수를 두 곳에 인라인으로 쓰면 side effect(노출 이벤트)가 2번 발화되므로 변수 하나로 추출해 1회 호출, 2곳 주입한다.
- 단, 변수 추출은 “한 번의 렌더 내” 중복만 막는다. 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 href | router.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 NextLink —
href를 선언적으로 받아 prefetch + 내비게이션을 처리. href는 렌더 시점에 확정된다
→ 확장 개념 (여기서 더 나아가면)
- A/B 실험 SDK 노출 이벤트 설계 — “노출”을 어떤 시점으로 정의하느냐가 실험 결과 해석에 영향을 준다 (페이지 진입 vs 버튼 클릭 vs 전환)
- React useMemo — 렌더마다 재계산되는 값 중 비용이 있는 연산을 메모이제이션. side effect가 있는 함수라면 useMemo 안에 넣어선 안 된다
↔ 같은 원리가 적용되는 곳
- 비동기 반복문에서 React 상태가 꼬이는 이유 — side effect 있는 함수를 잘못된 위치에서 호출하면 예상치 못한 중복 실행이 발생한다는 같은 원리
- React useEffect 의존성 배열 — “언제 실행되는가”를 명시적으로 제어해야 하는 같은 맥락