Skip to Content
FunnelFunnel 관리

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은 다음 우선순위로 결정됩니다.

  1. route에 viewName 함수가 있으면 → 함수 호출 결과
  2. route에 viewName 문자열이 있으면 → 해당 문자열
  3. 없으면 → ${funnelName} - ${stepId} (기본값)

funnelNameFunnel 컴포넌트의 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 }); };

더 보기

  • 컨벤션 — 파일 구조, 코드 패턴, 모범 사례
  • 예시 — 실제 패턴 기반 E2E 예시
  • API 레퍼런스useFunnel, Funnel, FunnelOverlayProvider 상세 스펙
Last updated on