-
Notifications
You must be signed in to change notification settings - Fork 1
Feat(client): useFunnel 구현 #261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { useCallback, useMemo } from 'react'; | ||
| import { useSearchParams } from 'react-router-dom'; | ||
|
|
||
| interface UseFunnelOptions<TStep extends string> { | ||
| steps: readonly TStep[]; | ||
| initialStep: TStep; | ||
| queryKey?: string; | ||
| } | ||
|
|
||
| interface MoveStepOptions { | ||
| replace?: boolean; | ||
| } | ||
|
|
||
| export function useFunnel<TStep extends string>({ | ||
| steps, | ||
| initialStep, | ||
| queryKey = 'step', | ||
| }: UseFunnelOptions<TStep>) { | ||
| const [searchParams, setSearchParams] = useSearchParams(); | ||
|
|
||
| const currentStep = useMemo(() => { | ||
| const value = searchParams.get(queryKey); | ||
| return value && steps.includes(value as TStep) | ||
| ? (value as TStep) | ||
| : initialStep; | ||
| }, [searchParams, queryKey, steps, initialStep]); | ||
|
|
||
| const currentIndex = steps.indexOf(currentStep); | ||
|
|
||
| const setStep = useCallback( | ||
| (nextStep: TStep, { replace = true }: MoveStepOptions = {}) => { | ||
| if (!steps.includes(nextStep)) return; | ||
| const nextParams = new URLSearchParams(searchParams); | ||
| nextParams.set(queryKey, nextStep); | ||
| setSearchParams(nextParams, { replace }); | ||
| }, | ||
| [steps, searchParams, queryKey, setSearchParams] | ||
| ); | ||
|
|
||
| const goNext = useCallback( | ||
| (options?: MoveStepOptions) => { | ||
| const nextStep = steps[currentIndex + 1]; | ||
| if (!nextStep) return null; | ||
| setStep(nextStep, options); | ||
| return nextStep; | ||
| }, | ||
| [steps, currentIndex, setStep] | ||
| ); | ||
|
|
||
| const goPrev = useCallback( | ||
| (options?: MoveStepOptions) => { | ||
| const prevStep = steps[currentIndex - 1]; | ||
| if (!prevStep) return null; | ||
| setStep(prevStep, options); | ||
| return prevStep; | ||
| }, | ||
| [steps, currentIndex, setStep] | ||
| ); | ||
|
|
||
| return { | ||
| currentStep, | ||
| currentIndex, | ||
| isFirstStep: currentIndex <= 0, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isFirstStep의 currentIndex가 ===0이 아니라 <= 0로 한 이유가 뭔가요?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 로직은 음수의 index값이 나왔을 때에도 첫 번째 step으로 보도록 하는 방어적 로직에 가까워요. |
||
| isLastStep: currentIndex === steps.length - 1, | ||
| setStep, | ||
| goNext, | ||
| goPrev, | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
initialStep이steps배열에 포함되지 않을 경우 조용히 오동작런타임에
initialStep이steps에 포함되어 있는지 검증하지 않습니다. 잘못 구성된 경우currentIndex === -1이 되어goNext()가steps[0]으로 이동하는 등 예측 불가한 동작이 발생합니다.🛡️ 개발 환경용 런타임 가드 예시
export function useFunnel<TStep extends string>({ steps, initialStep, queryKey = 'step', }: UseFunnelOptions<TStep>) { + if (process.env.NODE_ENV !== 'production' && !steps.includes(initialStep)) { + throw new Error( + `[useFunnel] initialStep "${initialStep}" is not in the steps array.` + ); + } + const [searchParams, setSearchParams] = useSearchParams();🤖 Prompt for AI Agents