Funnel 관리
Funnel은 회원가입, 본인인증, 부동산 인증 등 다단계 사용자 플로우를 관리하기 위한 시스템입니다. 복잡한 사용자 여정을 단계별로 나누고, 브라우저 히스토리와 동기화된 네비게이션을 제공합니다.
핵심 개념
Funnel이란?
Funnel은 단일 진입점과 후속 플로우를 가진 페이지들의 모임입니다. 중간에 직접 진입할 수 없는 연속된 단계들을 하나의 Funnel로 묶습니다.
예시: 본인인증 Funnel
온보딩 → 약관동의 → 본인인증 → 인증번호 입력각 단계는 특별한 경우가 아니면 (ex. 초기 상태에 따라 스킵할 수 있는 경우 등) 이전 단계에서만 진입 가능하며, 단계 간 이동 시 컨텍스트(데이터)가 누적됩니다.
왜 Funnel을 사용하는가?
일반적인 페이지 라우팅으로 다단계 플로우를 구현하면 다음 문제가 발생합니다:
- 완료 후 뒤로가기로 중간 단계에 재진입 가능
- 단계 간 데이터 전달을 별도로 관리해야 함
- 히스토리 스택이 지저분해짐
Funnel은 이를 해결합니다:
- 히스토리 스택 정리:
exit()호출 시 Funnel의 모든 단계를 히스토리에서 제거 - 컨텍스트 누적: 각 단계에서 다음 단계로 데이터를 타입 안전하게 전달
- 선언적 플로우 정의: 라우트 맵으로 전체 플로우를 한눈에 파악
Funnel 설계 가이드
Funnel을 나누는 기준
하나의 Funnel로 묶어야 하는 경우
- 단계들이 순서를 가지며, 외부에서 임의 단계로 직접 진입할 수 없는 플로우
- 초기 상태에 따라 일부 앞 단계를 건너뛸 수는 있지만, 그 이후의 흐름은 순차적인 경우
- 사용자가 플로우 도중 임의로 단계를 건너뛸 수 없는 경우
별도 Funnel로 분리해야 하는 경우
- 다른 곳에서도 재사용 가능한 독립적인 플로우
- 중간 단계로 직접 진입이 가능한 경우
- 다른 컨텍스트에서 독립적으로 사용되는 경우
중첩 Funnel
Funnel 안에 다른 Funnel을 중첩할 수 있습니다. 최대 3단계 중첩을 권장합니다.
listing-create (최상위) — exit 가능
└─ listing-search-estate (중간) — exit 불가, 부모로 복귀
└─ select-address (최하위) — 재사용 가능한 독립 플로우Exit 전략
Exit가 필요한 이유
히스토리: A → (B → C → D)
D 완료 후 뒤로가기 → C로 이동 (의도하지 않은 재진입)
exit() 사용 시:
D 완료 → 히스토리에서 B, C, D 제거 → A로 이동이 문제는 특히 Android에서 중요합니다. Android는 시스템 레벨의 뒤로가기 버튼(Back Handler)을 제공하기 때문에, 사용자가 Funnel을 완료한 후에도 시스템 뒤로가기로 중간 단계에 재진입할 수 있습니다. exit()를 호출하지 않으면 완료된 플로우의 단계들이 히스토리에 남아 의도치 않은 동작이 발생합니다.
iOS는 브라우저 히스토리를 직접 조작하는 뒤로가기 수단이 제공되지 않아 상대적으로 덜 민감하지만, Android 사용자를 위해 exit()는 항상 올바르게 처리해야 합니다.
언제 어떤 Exit를 사용하는가
| 상황 | 사용 방법 |
|---|---|
| 다른 URL로 이동 | history.exitAndRedirect('/home') |
| 부모 Funnel의 다음 단계로 이동 | history.exit(() => parentHistory.push('next')) |
| 서브 Funnel 종료 후 콜백 | history.exit(() => onComplete(result)) |
주의사항
- 과도한 중첩 방지: 3단계 이상의 중첩은 피하세요
- 각 Funnel의 역할과 범위를 명확히 정의하세요
- 재사용 가능한 플로우는 별도 Funnel로 분리하세요
- 비슷한 상황에서는 동일한 exit 패턴을 사용하세요
- Funnel은 반드시
'use client'환경에서 사용해야 합니다
Datadog 연동
Funnel은 각 단계 진입 시 Logger.startView(viewName)을 자동으로 호출합니다.
별도 코드 없이 Datadog RUM에서 단계별 전환율, 체류 시간, 오류를 추적할 수 있습니다.
viewName 결정 규칙
단계별 viewName은 다음 우선순위로 결정됩니다.
- route에
viewName함수가 있으면 → 함수 호출 결과 - route에
viewName문자열이 있으면 → 해당 문자열 - 없으면 →
${funnelName} - ${stepId}(기본값)
funnelName은 Funnel 컴포넌트의 funnelName prop이며, 미설정 시 funnel.id가 사용됩니다.
// funnelName 미설정 시 기본 viewName 예시
// id: 'signup-funnel', step: 'inputName'
// → viewName: "signup-funnel - inputName"
// funnelName 설정 시
// funnelName: '회원가입', step: 'inputName'
// → viewName: "회원가입 - inputName"viewName 설정 방법
<Funnel
funnel={funnel}
funnelName="회원가입" // 모든 단계의 funnelName으로 사용
route={{
// 정적 viewName
inputName: {
type: 'page',
viewName: '회원가입 - 이름 입력',
render: ({ history }) => ({ element: <InputName /> })
},
// 동적 viewName
inputAge: {
type: 'page',
viewName: ({ funnelId, stepId, funnelName }) =>
`${funnelName} - ${stepId}`,
render: ({ history }) => ({ element: <InputAge /> })
},
// viewName 미설정 → "회원가입 - inputEmail" 자동 생성
inputEmail: {
type: 'page',
render: ({ history }) => ({ element: <InputEmail /> })
}
}}
/>중복 호출 방지
동일한 viewName으로 연속 진입 시 startView를 중복 호출하지 않습니다.
예를 들어 replace로 같은 단계를 교체하거나 리렌더링이 발생해도 Datadog 이벤트는 한 번만 전송됩니다.
커스텀 이벤트 추가
자동 뷰 추적 외에 버튼 클릭 등 커스텀 이벤트가 필요하면 Logger.addAction을 직접 사용합니다.
const handleNext = () => {
Logger.addAction('button_click', { step: 'inputName', action: 'next' });
history.push('inputAge', { ...context, name });
};