Skip to Content
Packages@howmuchhome-web/funnel

@howmuchhome-web/funnel

React 및 use-funnel  기반의 다단계 플로우(Funnel) 관리를 위한 패키지입니다. 개념 및 설계 가이드는 Funnel 관리를 참고하세요.

설치

monorepo 내부 패키지로, 별도 설치 없이 사용합니다.

import { useFunnel, Funnel, FunnelOverlayProvider } from '@howmuchhome-web/funnel';

useFunnel

Funnel 상태와 네비게이션을 관리하는 훅입니다.

const funnel = useFunnel<StepContextMap>({ id: 'my-funnel', // Funnel 고유 식별자 (필수) initial: { // 초기 단계 설정 (필수) step: 'step1', context: { name: '' } }, funnelName: '회원가입', // 분석 도구에 표시될 이름 (기본값: id) });

StepContextMap 타입 정의

각 단계와 해당 단계에서 사용하는 컨텍스트 타입을 매핑합니다. 각 단계의 컨텍스트는 독립적으로 정의되며, 이전 단계의 데이터가 자동으로 carry되지 않습니다. push()/replace() 호출 시 직접 필요한 데이터를 전달해야 합니다.

type StepContextMap = { step1: { name: string }; step2: { name: string; age: number }; // step1 데이터를 쓰려면 명시적으로 포함 step3: { name: string; age: number; email: string }; };

반환값

속성타입설명
stepstring현재 단계
contextobject현재 컨텍스트
historyHistory네비게이션 메서드

history 메서드

메서드히스토리 스택설명
push(step, context?)추가 (+1)새 단계로 이동
replace(step, context?)변경 없음현재 단계를 교체 (뒤로가기 불가)
back()제거 (-1)이전 단계로 이동
exit(callback?)전체 제거Funnel의 모든 단계를 히스토리에서 제거
exitAndRedirect(href)전체 제거 후 이동Funnel 종료 후 지정 URL로 리다이렉트
reset()전체 제거 후 초기화초기 단계로 리셋

replacepush와 달리 히스토리 스택을 늘리지 않습니다. 이전 단계로 돌아오지 못하게 할 때 적합합니다.

push()replace()는 모두 Promise를 반환합니다. 이동 완료 후 추가 로직이 필요한 경우 await할 수 있습니다.

await history.replace('identityVerify', { defaultValues: formValue }); await history.push('code', { formValue, authId });

Funnel 컴포넌트

<Funnel funnel={funnel} route={routeMap} />

props

prop타입설명
funnelFunnelStateuseFunnel 반환값
routeRouteMap단계별 렌더링 설정
funnelNamestring (선택)Datadog 뷰 이름 prefix. 미설정 시 funnel.id 사용

Route 설정

페이지 타입

{ type: 'page'; viewName?: string | ((props: { funnelId: string; stepId: string; funnelName: string }) => string); render: (funnelStep: { context: Context; history: History }) => { element: ReactElement; splashScreen?: { splash: ReactElement; // 로딩 중 표시할 컴포넌트 duration: number; // 표시 시간 (ms) onFinish?: () => void; // 완료 후 콜백 }; }; }

splashScreen한 번만 표시됩니다. 같은 Funnel 인스턴스 내에서 해당 단계로 다시 돌아오면 스플래시 없이 바로 element가 렌더링됩니다.

viewName을 설정하지 않으면 ${funnelName} - ${stepId} 형태로 자동 생성됩니다. 자세한 규칙은 Datadog 연동을 참고하세요.

바텀시트 타입

{ type: 'bottom-sheet'; viewName?: string | ((props: { funnelId: string; stepId: string; funnelName: string }) => string); render: (funnelStep: { context: Context; history: History }) => { element: ReactElement; // ⚠️ bottom-sheet는 splashScreen을 지원하지 않습니다 }; }

바텀시트 외부 영역 클릭 시 history.back()이 자동으로 호출됩니다.

사이드 드로어 타입

오른쪽에서 슬라이드 인 되는 드로어입니다. 바텀시트와 동일하게 동작하며, 외부 클릭 시 history.back()이 호출됩니다.

{ type: 'side-drawer'; viewName?: string | ((props: { funnelId: string; stepId: string; funnelName: string }) => string); render: (funnelStep: { context: Context; history: History }) => { element: ReactElement; // ⚠️ side-drawer는 splashScreen을 지원하지 않습니다 }; }

FunnelOverlayProvider

exitAndRedirect 호출 시 화면이 순간적으로 비는 현상을 방지하는 Provider입니다.

동작 원리: exitAndRedirect가 실행되면 히스토리 스택을 정리하는 동안 현재 화면의 DOM을 스냅샷으로 복사해 fixed overlay로 고정합니다. 새 페이지 렌더링이 시작되면 overlay가 제거됩니다.

부모 Funnel이 없는 최상위 Funnel에서만 활성화됩니다. 중첩된 자식 Funnel에서는 동작하지 않습니다.

앱 최상위 layout에 한 번만 배치합니다. 개별 Funnel마다 감쌀 필요 없습니다.

// app/layout.tsx export default function RootLayout({ children }) { return ( <html> <body> <FunnelOverlayProvider> {children} </FunnelOverlayProvider> </body> </html> ); }

exitAndRedirect를 사용하지 않거나 전환 중 화면이 비어도 무방한 경우 생략할 수 있습니다.

Exit 동작 레퍼런스

1. 단일 Funnel 완전 제거

초기: (A → B → C) C에서 exitAndRedirect(D) 실행 결과: D

히스토리 스택에서 Funnel이 완전히 사라지고 D만 남습니다.

2. 이전 페이지로 복귀

초기: A → (B → C → D) D에서 exit() 실행 결과: A

Funnel이 완전히 제거되고 A로 되돌아갑니다.

초기: A → (B → C → D) D에서 exitAndRedirect(E) 실행 결과: A → E

Funnel이 제거되고 E로 리다이렉트됩니다.

3. 중첩 Funnel - 자식 종료

초기: (A → (B → C → D)) D에서 자식 Funnel exit() 실행 결과: A

자식 Funnel이 종료되면 최상위 부모로 돌아갑니다.

4. 중첩 Funnel - 선택적 종료

초기: A → (B → (C → D)) D에서 자식 Funnel exit() 실행 결과: A → B

자식 Funnel만 종료되고 부모 Funnel의 B 단계로 돌아갑니다.

초기: A → (B → (C → D)) D에서 부모 Funnel exit() 실행 결과: A

부모 Funnel까지 종료되어 A로 돌아갑니다.

5. Exit 후 부모 Funnel 계속

초기: A → (B → (C → D)) D에서 자식 exit(() => history.push(E)) 실행 결과: A → (B → E)

자식 Funnel이 종료된 후 부모 Funnel의 E 단계로 이동합니다.

6. 부모 초기 step의 자식 Funnel에서 부모 step replace

초기: external → (A → (C → D)) (부모가 자체 push 없이 초기 step A에서 자식 시작) D에서 자식 exit(() => parentHistory.replace('Result')) 실행 결과: external → Result

자식 funnel이 부모의 초기 step에 위치하고, callback에서 부모의 replace를 수행하는 패턴입니다.

자식 exit이 funnel 진입점까지만 히스토리를 정리하므로, callback의 replace가 정상 pathname에서 실행됩니다.

실제 사용 예시: /online-assembly/[assemblyId] 페이지의 entrance step에서 입장 서명을 마친 뒤 assembly step으로 replace.

E2E 테스트: apps/app/e2e/tests/funnel.spec.ts 케이스 8.

child exit 동작 분기

exitCore는 자식 funnel exit 시 mount 시점의 외부 히스토리 존재 여부(hasExternalAtMount = window.history.length > 1)에 따라 두 가지 동작으로 분기합니다.

외부 히스토리 있음 (hasExternalAtMount === true)

히스토리: landing → parent-A → parent-B/child-C → parent-B/child-D D에서 child exit() 실행 결과: landing → parent-B (child만 제거)

go(-historyIndex)로 정확히 child 히스토리만큼만 이동해 부모의 containing step에 머무릅니다. 케이스 5, 7, 8이 이에 해당합니다.

외부 히스토리 없음 (hasExternalAtMount === false)

히스토리: (parent-A 직접 진입) → parent-B/child-C → parent-B/child-D D에서 child exit() 실행 결과: parent-A (parent push까지 모두 정리)

go(-(historyIndex+1))로 funnel 초기 상태까지 모두 정리합니다. 케이스 4가 이에 해당합니다.

판정 시점과 신뢰성

hasExternalAtMountfunnel mount 시점window.history.length를 한 번 확인해서 ref에 저장합니다. 이후 navigation으로 length가 변해도 영향받지 않습니다.

  • length === 1: funnel 페이지가 세션의 첫 entry → 외부 없음 (100% 보장)
  • length > 1: 다른 entry가 더 있음 → 외부 있음으로 판정 (대부분 정확)

length는 forward 히스토리도 포함하지만, 자식 funnel mount 시점에는 보통 forward 히스토리가 없는 첫 진입 상태이므로 문제되지 않습니다. 또한 우리 분기 로직은 go(-N)만 호출하고 forward 영역은 건드리지 않아 forward 히스토리 유무와 무관하게 동작합니다.

의존성

패키지용도
@use-funnel/browser핵심 Funnel 로직
@howmuchhome-web/ui/v2BottomSheet, SplashWrapper UI 컴포넌트
@howmuchhome-web/loggerDatadog RUM 뷰 추적
@howmuchhome-web/utils-ts유틸리티 함수
next/navigationNext.js 라우팅

Native (Expo)

위 문서는 web(@howmuchhome-web/funnel)을 전제로 합니다. web 구현은 @use-funnel/browser + next/navigation + window.history/popstate 에 의존해 RN 에서 동작하지 않습니다. native 는 별도 진입점 @howmuchhome-web/funnel/native 를 씁니다.

import {Funnel, useFunnel, FunnelOverlayProvider} from '@howmuchhome-web/funnel/native';

native useFunnel 은 기본적으로 memory 라우터(React state 기반, useFunnelMemory)로 URL/브라우저 히스토리를 건드리지 않고 한 스크린 안에서 step 을 전환합니다(옵션으로 'expo-router' 스택 모드도 있음). 뒤로가기·이탈 가드도 다릅니다 — web 의 라우터 back 가로채기 대신 native 는 하드웨어 백버튼 + 화면 back 버튼을 BackGuardProvider/PreventLeaveConfirm(native) 로 한 채널에서 처리합니다. exit/exitAndRedirect 등 history 메서드의 동작 차이도 있으니 자세한 내용은 native 데이터 페칭·상태 관리 § Funnel를 참고하세요.

더 보기

  • Funnel 관리 — 개념, 설계 가이드, Datadog 연동
  • 컨벤션 — 파일 구조, 코드 패턴, 모범 사례
Last updated on