Skip to Content
FunnelFunnel 예시

Funnel 예시

실제 코드 패턴을 기반으로 한 본인인증 Funnel 예시입니다. 컨벤션 적용 방법을 단계별로 확인할 수 있습니다.

구조

IdentityVerificationFunnel/ IdentityVerificationFunnel.tsx funnel.ts steps/ OnboardingStep.tsx InputInfoStep.tsx VerifyCodeStep.tsx

타입 정의 (funnel.ts)

export type IdentityVerificationFunnel = { onboarding: {}; inputInfo: { defaultValues: { name: string; phone: string } | null; // 뒤로 왔을 때 복원용 }; verifyCode: { name: string; phone: string; authId: string; }; };

진입(onboarding)과 최종 단계(verifyCode 완료 후 exit)는 컨텍스트가 없거나 최소화합니다. defaultValues는 뒤로가기 시 폼 값을 복원하기 위해 포함합니다.

메인 컴포넌트 (IdentityVerificationFunnel.tsx)

'use client'; import {Funnel, useFunnel} from '@howmuchhome-web/funnel'; import {IdentityVerificationFunnel} from './funnel'; import {InputInfoStep} from './steps/InputInfoStep'; import {OnboardingStep} from './steps/OnboardingStep'; import {VerifyCodeStep} from './steps/VerifyCodeStep'; type Props = { onComplete: (identityVerificationId: string) => void; }; export function IdentityVerificationFunnel({onComplete}: Props) { const funnel = useFunnel<IdentityVerificationFunnel>({ id: 'identity-verification', initial: {step: 'onboarding', context: {}}, }); return ( <Funnel funnel={funnel} funnelName="본인인증" route={{ onboarding: { type: 'page', viewName: '본인인증 안내', render: ({history}) => ({ element: ( <OnboardingStep onNext={() => history.push('inputInfo', {defaultValues: null})} /> ), }), }, inputInfo: { type: 'page', viewName: '정보 입력', render: ({context, history}) => ({ element: ( <InputInfoStep defaultValues={context.defaultValues} onNext={async ({name, phone, authId}) => { // 현재 단계에 입력값 저장 (뒤로 왔을 때 폼 복원) await history.replace('inputInfo', { defaultValues: {name, phone}, }); // 다음 단계로 이동 await history.push('verifyCode', {name, phone, authId}); }} /> ), }), }, verifyCode: { type: 'page', viewName: '인증번호 입력', render: ({context, history}) => ({ element: ( <VerifyCodeStep name={context.name} phone={context.phone} authId={context.authId} onComplete={(identityVerificationId) => { history.exit(() => onComplete(identityVerificationId)); }} /> ), }), }, }} /> ); }

스텝 컴포넌트 예시

스텝 컴포넌트는 Funnel을 알지 못합니다. props로 데이터와 콜백만 받습니다.

// steps/InputInfoStep.tsx type Props = { defaultValues: {name: string; phone: string} | null; onNext: (value: {name: string; phone: string; authId: string}) => Promise<void>; }; export function InputInfoStep({defaultValues, onNext}: Props) { const [name, setName] = useState(defaultValues?.name ?? ''); const [phone, setPhone] = useState(defaultValues?.phone ?? ''); const errorAlert = useAlert(); const handleSubmit = async () => { try { const {authId} = await requestAuthCode({name, phone}); // API 호출 await onNext({name, phone, authId}); // 완료 후 이동 } catch { errorAlert.open(); // 현재 단계 유지 } }; return ( <div> <input value={name} onChange={(e) => setName(e.target.value)} /> <input value={phone} onChange={(e) => setPhone(e.target.value)} /> <button onClick={handleSubmit}>다음</button> </div> ); }
// steps/VerifyCodeStep.tsx type Props = { name: string; phone: string; authId: string; onComplete: (identityVerificationId: string) => void; }; export function VerifyCodeStep({name, phone, authId, onComplete}: Props) { const [code, setCode] = useState(''); const errorAlert = useAlert(); const handleVerify = async () => { try { const {identityVerificationId} = await verifyCode({authId, code}); // API 호출 onComplete(identityVerificationId); // 완료 시 부모에게 전달 } catch { errorAlert.open(); // 현재 단계 유지 } }; return ( <div> <input value={code} onChange={(e) => setCode(e.target.value)} /> <button onClick={handleVerify}>인증</button> </div> ); }

exit 활용

verifyCode: { type: 'page', render: ({context, history}) => ({ element: ( <VerifyCodeStep onComplete={(identityVerificationId) => { // exit: 히스토리 스택에서 Funnel 전체 제거 // → 완료 후 뒤로가기 시 인증 단계로 재진입 불가 (Android 백버튼 포함) history.exit(() => onComplete(identityVerificationId)); }} /> ), }), },

부모에서 사용

이 Funnel을 최상위에서 사용할 때와 서브 Funnel로 사용할 때의 차이입니다.

// 최상위: 완료 후 다른 페이지로 이동 function IdentityVerificationPage() { return ( <IdentityVerificationFunnel onComplete={(id) => router.push(`/result?id=${id}`)} /> ); } // 서브 Funnel: 부모 Funnel의 다음 단계로 이동 parentRoute = { verifyIdentity: { type: 'page', render: ({history}) => ({ element: ( <IdentityVerificationFunnel onComplete={(id) => history.replace('nextStep', {identityVerificationId: id}) } /> ), }), }, };

뒤로 돌아갔을 때 입력값 복원

다음 단계로 이동하기 전에 현재 단계 상태를 replace로 저장해두면, 사용자가 뒤로 돌아왔을 때 이전에 입력한 값이 그대로 유지됩니다.

위 예시의 inputInfo 단계가 이 패턴을 사용합니다.

inputInfo: { type: 'page', render: ({context, history}) => ({ element: ( <InputInfoStep defaultValues={context.defaultValues} // 복원된 입력값 onNext={async ({name, phone, authId}) => { // 현재 단계에 입력값 저장 → 뒤로 왔을 때 defaultValues로 복원됨 await history.replace('inputInfo', {defaultValues: {name, phone}}); // 다음 단계로 이동 await history.push('verifyCode', {name, phone, authId}); }} /> ), }), },

replace는 히스토리 스택을 늘리지 않으므로 현재 단계의 컨텍스트만 업데이트됩니다. 사용자가 verifyCode에서 뒤로가기를 누르면 inputInfo로 돌아오고, context.defaultValues에 저장된 값으로 폼이 채워집니다.

Last updated on