useFunnel
๋จ๊ณ๋ณ UI ํ๋ฆ(ํผ๋)์ ์ ์ธ์ ์ผ๋ก ๊ด๋ฆฌํ๋ Hook์ ๋๋ค.
์ธ์ ์ฌ์ฉํ๋์?
- ๐ ํ์๊ฐ์ ํ๋ก์ฐ
- ๐ณ ๊ฒฐ์ ํ๋ก์ธ์ค
- ๐ฏ ์จ๋ณด๋ฉ ํํ ๋ฆฌ์ผ
- ๐ ์ค๋ฌธ์กฐ์ฌ, ํผ ์์ ๋
์ฃผ์ ํน์ง
1. ์ ์ธ์ UI
Funnel/Step ์ปดํฌ๋ํธ๋ก ์ง๊ด์ ์ธ ๊ตฌ์กฐ
2. ์ปจํ ์คํธ ๊ด๋ฆฌ
์คํ ๊ฐ ๋ฐ์ดํฐ ์๋ ๋์
3. ๋ธ๋ผ์ฐ์ ํ์คํ ๋ฆฌ ์ง์
๋ค๋ก๊ฐ๊ธฐ ์ ์ด์ ์คํ + ๋ฐ์ดํฐ ๋ณต์
4. ํ๋ ์์ํฌ ์ด๋ํฐ
Memory, Browser, Next.js, React Router ์ง์
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
import { useFunnel } from '@frontend-toolkit-js/hooks';
const STEPS = ['email', 'password', 'profile', 'done'] as const;
function SignUpFunnel() {
const funnel = useFunnel(STEPS, {
initialStep: 'email',
});
return (
<funnel.Funnel>
<funnel.Step name="email">
<div>
<h2>์ด๋ฉ์ผ ์
๋ ฅ</h2>
<button onClick={() => funnel.history.push('password')}>
๋ค์
</button>
</div>
</funnel.Step>
<funnel.Step name="password">
<div>
<h2>๋น๋ฐ๋ฒํธ ์ค์ </h2>
<button onClick={() => funnel.history.back()}>์ด์ </button>
<button onClick={() => funnel.history.push('profile')}>
๋ค์
</button>
</div>
</funnel.Step>
<funnel.Step name="profile">
<div>
<h2>ํ๋กํ ์ค์ </h2>
<button onClick={() => funnel.history.back()}>์ด์ </button>
<button onClick={() => funnel.history.push('done')}>
์๋ฃ
</button>
</div>
</funnel.Step>
<funnel.Step name="done">
<div>
<h2>๊ฐ์
์๋ฃ!</h2>
</div>
</funnel.Step>
</funnel.Funnel>
);
}์ปจํ ์คํธ๋ก ๋ฐ์ดํฐ ์ ๋ฌ
์คํ
๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ ค๋ฉด history.push์ ๋ ๋ฒ์งธ ์ธ์๋ฅผ ์ฌ์ฉํฉ๋๋ค.
interface SignUpContext {
email?: string;
password?: string;
name?: string;
}
function SignUpFunnel() {
const funnel = useFunnel<(typeof STEPS)[number], SignUpContext>(STEPS, {
initialStep: 'email',
initialContext: {},
});
return (
<funnel.Funnel>
<funnel.Step name="email">
<EmailStep
onNext={(email) => funnel.history.push('password', { email })}
/>
</funnel.Step>
<funnel.Step name="password">
<PasswordStep
onNext={(password) => funnel.history.push('profile', { password })}
onBack={() => funnel.history.back()}
/>
</funnel.Step>
<funnel.Step name="profile">
<ProfileStep
onNext={(name) => funnel.history.push('done', { name })}
onBack={() => funnel.history.back()}
/>
</funnel.Step>
<funnel.Step name="done">
{/* ๋์ ๋ ์ปจํ
์คํธ ์ฌ์ฉ */}
<p>ํ์ํฉ๋๋ค, {funnel.context.name}๋!</p>
<p>์ด๋ฉ์ผ: {funnel.context.email}</p>
</funnel.Step>
</funnel.Funnel>
);
}๋ฐ์ดํฐ๋ ์๋์ผ๋ก ๋์ ๋ฉ๋๋ค:
// email step
funnel.history.push('password', { email: 'user@example.com' });
// context: { email: 'user@example.com' }
// password step
funnel.history.push('profile', { password: '1234' });
// context: { email: 'user@example.com', password: '1234' }
// profile step
funnel.history.push('done', { name: 'ํ๊ธธ๋' });
// context: { email: 'user@example.com', password: '1234', name: 'ํ๊ธธ๋' }์ด๋ํฐ
์ํ ์ ์ฅ ๋ฐฉ์์ ์ด๋ํฐ๋ก ์ ํํ ์ ์์ต๋๋ค.
Memory ์ด๋ํฐ (๊ธฐ๋ณธ๊ฐ)
๋ฉ๋ชจ๋ฆฌ์๋ง ์ํ๋ฅผ ์ ์ฅํฉ๋๋ค. ์๋ก๊ณ ์นจ ์ ์ด๊ธฐํ๋ฉ๋๋ค.
const funnel = useFunnel(STEPS, {
initialStep: 'email',
// adapter ๋ฏธ์ง์ ์ memory ์ด๋ํฐ ์ฌ์ฉ
});Browser ์ด๋ํฐ
URL ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ์ ๋๊ธฐํํฉ๋๋ค. ๋ธ๋ผ์ฐ์ ๋ค๋ก๊ฐ๊ธฐ/์์ผ๋ก๊ฐ๊ธฐ๋ฅผ ์ง์ํฉ๋๋ค.
import { useFunnel, createBrowserAdapter } from '@frontend-toolkit-js/hooks';
const funnel = useFunnel(STEPS, {
initialStep: 'email',
adapter: createBrowserAdapter(),
});
// URL: ?step=password&context={"email":"user@example.com"}Next.js App Router
import { useFunnel } from '@frontend-toolkit-js/hooks';
import { useNextAppAdapter } from '@frontend-toolkit-js/hooks/useFunnel/adapters/next-app';
function SignUpFunnel() {
const adapter = useNextAppAdapter();
const funnel = useFunnel(STEPS, {
initialStep: 'email',
adapter,
});
return <funnel.Funnel>...</funnel.Funnel>;
}Note:
useSearchParams๋ฅผ ์ฌ์ฉํ๋ฏ๋กSuspense๋ก ๊ฐ์ธ์ผ ํฉ๋๋ค.
Next.js Pages Router
import { useFunnel } from '@frontend-toolkit-js/hooks';
import { useNextPagesAdapter } from '@frontend-toolkit-js/hooks/useFunnel/adapters/next-pages';
function SignUpFunnel() {
const adapter = useNextPagesAdapter({ shallow: true });
const funnel = useFunnel(STEPS, {
initialStep: 'email',
adapter,
});
return <funnel.Funnel>...</funnel.Funnel>;
}React Router v6+
import { useFunnel } from '@frontend-toolkit-js/hooks';
import { useReactRouterAdapter } from '@frontend-toolkit-js/hooks/useFunnel/adapters/react-router';
function SignUpFunnel() {
const adapter = useReactRouterAdapter();
const funnel = useFunnel(STEPS, {
initialStep: 'email',
adapter,
});
return <funnel.Funnel>...</funnel.Funnel>;
}API Reference
useFunnel(steps, options)
Parameters
| ์ด๋ฆ | ํ์ | ์ค๋ช |
|---|---|---|
steps | readonly string[] | ์คํ
์ด๋ฆ ๋ฐฐ์ด (as const ๊ถ์ฅ) |
options.initialStep | string | ์ด๊ธฐ ์คํ (ํ์) |
options.initialContext | object | ์ด๊ธฐ ์ปจํ ์คํธ ๋ฐ์ดํฐ |
options.onStepChange | (step, context) => void | ์คํ ๋ณ๊ฒฝ ์ฝ๋ฐฑ |
options.adapter | FunnelAdapter | ์ํ ๊ด๋ฆฌ ์ด๋ํฐ |
Returns
| ์ด๋ฆ | ํ์ | ์ค๋ช |
|---|---|---|
currentStep | string | ํ์ฌ ์คํ |
context | object | ๋์ ๋ ์ปจํ ์คํธ ๋ฐ์ดํฐ |
history | FunnelHistory | ํ์คํ ๋ฆฌ ๊ด๋ฆฌ ๊ฐ์ฒด |
Funnel | Component | ํผ๋ ์ปจํ ์ด๋ |
Step | Component | ์คํ ์ปดํฌ๋ํธ |
history ๊ฐ์ฒด
| ๋ฉ์๋/์์ฑ | ํ์ | ์ค๋ช |
|---|---|---|
push | (step, data?) => void | ์ ์คํ ์ผ๋ก ์ด๋ (ํ์คํ ๋ฆฌ ์ถ๊ฐ) |
replace | (step, data?) => void | ํ์ฌ ์คํ ๊ต์ฒด (ํ์คํ ๋ฆฌ ์ ์ง) |
back | () => void | ์ด์ ์คํ ์ผ๋ก ์ด๋ |
canGoBack | boolean | ๋ค๋ก๊ฐ๊ธฐ ๊ฐ๋ฅ ์ฌ๋ถ |
Funnel / Step ์ปดํฌ๋ํธ
<funnel.Funnel>
<funnel.Step name="step1">
{/* step1 ๋ด์ฉ */}
</funnel.Step>
<funnel.Step name="step2">
{/* step2 ๋ด์ฉ */}
</funnel.Step>
</funnel.Funnel>Funnel: ํ์ฌ ์คํ ์ ํด๋นํ๋Step๋ง ๋ ๋๋งStep:nameprop์ผ๋ก ์คํ ์๋ณ
์ค๋ฌด ํจํด
์งํ๋ฅ ํ์
const steps = ['a', 'b', 'c'] as const;
const funnel = useFunnel(steps, { initialStep: 'a' });
const currentIndex = steps.indexOf(funnel.currentStep);
const progress = ((currentIndex + 1) / steps.length) * 100;
return (
<div>
<div style={{ width: `${progress}%`, height: 4, background: 'blue' }} />
<funnel.Funnel>...</funnel.Funnel>
</div>
);์กฐ๊ฑด๋ถ ์คํ
<funnel.Step name="payment">
{needsVerification ? (
<button onClick={() => funnel.history.push('verify')}>
๋ณธ์ธ์ธ์ฆ ํ์
</button>
) : (
<button onClick={() => funnel.history.push('complete')}>
๊ฒฐ์ ํ๊ธฐ
</button>
)}
</funnel.Step>์ด๊ธฐํ (์ฒ์๋ถํฐ ๋ค์)
<button onClick={() => funnel.history.replace('email', {})}>
์ฒ์๋ถํฐ ๋ค์
</button>replace๋ฅผ ์ฌ์ฉํ๋ฉด ํ์คํ ๋ฆฌ์ ์์ด์ง ์๊ณ , ๋น ๊ฐ์ฒด {}๋ฅผ ์ ๋ฌํ๋ฉด ์ปจํ
์คํธ๊ฐ ์ด๊ธฐํ๋ฉ๋๋ค.
์ฃผ์์ฌํญ
์ด๋ํฐ๋ณ ์๊ตฌ์ฌํญ
| ์ด๋ํฐ | ์๊ตฌ์ฌํญ |
|---|---|
| Next.js App Router | Suspense๋ก ๊ฐ์ธ๊ธฐ ํ์ |
| Next.js Pages Router | Router ์ปจํ ์คํธ ๋ด์์ ์ฌ์ฉ |
| React Router | <BrowserRouter> ๋ฑ Router ์ปจํ
์คํธ ๋ด์์ ์ฌ์ฉ |
๋ธ๋ผ์ฐ์ ๋ค๋ก๊ฐ๊ธฐ ์ context ์ ์ง
history.push ํธ์ถ ์ ๋ด๋ถ์ ์ผ๋ก replaceโpush ํจํด์ ์ฌ์ฉํฉ๋๋ค.
1. replace: ํ์ฌ ํ์คํ ๋ฆฌ ์ํธ๋ฆฌ์ context ์ ์ฅ
2. push: ์ ์คํ
์ผ๋ก ์ด๋์ด๋ฅผ ํตํด ๋ธ๋ผ์ฐ์ ๋ค๋ก๊ฐ๊ธฐ ์์๋ ์ด์ ์คํ ์ context๊ฐ ์ ์ง๋ฉ๋๋ค.
TypeScript
์คํ ๊ณผ ์ปจํ ์คํธ ํ์ ์ด ์๋์ผ๋ก ์ถ๋ก ๋ฉ๋๋ค.
const STEPS = ['email', 'password', 'done'] as const;
interface MyContext {
email?: string;
password?: string;
}
const funnel = useFunnel<(typeof STEPS)[number], MyContext>(STEPS, {
initialStep: 'email',
initialContext: {},
});
funnel.currentStep; // 'email' | 'password' | 'done'
funnel.context; // MyContext
// ์๋ชป๋ ์คํ
์ด๋ฆ โ ํ์
์๋ฌ
funnel.history.push('invalid'); // โ Error๊ด๋ จ ๋ฌธ์
- useQueryParams - URL ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ๊ด๋ฆฌ