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