기존 방식은 Zustand로 전역 상태를 관리하고, 하단 버튼을 누르면 단계별로 page routing이 되는 방식이었다.
이 방식으로 개발했을 때 문제점은 아래와 같다고 느꼈다.
1. signUp과 관련된 pages | 2. 뒤로가기 누르면 상태 저장 안됨 |
---|---|
따라서 위 두가지 문제를 해결하기 위해 useFunnel 커스텀 훅을 만들어 사용해보았다.
export const useFunnel = <Steps extends NonEmptyArray<string>>(
steps: Steps,
): readonly [FunnelComponent<Steps>, () => void, () => void] => {
const [step, setStep] = useState(steps[0]);
const router = useRouter();
const currentStepIdx = steps.indexOf(step);
const toPrevStep = () => {
if (currentStepIdx <= 0) {
router.back();
return;
}
setStep(steps[currentStepIdx - 1]);
};
const toNextStep = () => {
if (currentStepIdx >= steps.length - 1) return;
setStep(steps[currentStepIdx + 1]);
};
const FunnelComponent = useMemo(
() =>
Object.assign(
function RouteFunnel(props: RouteFunnelProps<Steps>) {
return <Funnel<Steps> steps={steps} step={step} {...props} />;
},
{
Step,
},
),
[step, steps],
);
return Object.assign([FunnelComponent, toPrevStep, toNextStep] as const);
};
FunnelComponent
: 해당 step에 일치하는 Funnel 컴포넌트 & Step 반환
toPrevStep
: 이전 step으로 이동하게 해주는 함수
인덱스가 0일 때 뒤로가기를 누르면 router.back()을 호출하여 이전 페이지로 라우팅 될 수 있도록 했다.
toNextStep
: 이후 step으로 이동하게 해주는 함수
를 리턴하는데 FunnelComponent가 어떻게 해당 step과 일치하는 Funnel 컴포넌트를 반환하는지에 대해서는 아래와 같다.
const Funnel = <Steps extends NonEmptyArray<string>>({
steps,
step,
children,
}: FunnelProps<Steps>) => {
const validChildren = Children.toArray(children)
.filter(isValidElement)
.filter((i) =>
steps.includes((i.props as Partial<StepProps<Steps>>).name ?? ''),
) as Array<ReactElement<StepProps<Steps>>>;
const targetStep = validChildren.find((child) => child.props.name === step);
return <>{targetStep}</>;
};
parameter로 받은 step과 child.props.name이 일치하는지 확인하고, targetStep을 반환하게 된다.
라이브러리 | REINPUT |
---|---|
Next.js의 useRouter 사용 | 조건부 컴포넌트 렌더링 |
다음 스텝으로 넘어갈 때 query parameter가 바뀜 | 다음 스텝으로 넘어갈 때 step을 인덱스로 접근하여 내부에서 state 바뀜 |
query parameter로 해당 step 페이지 반환 | useFunnel 내부에서만 관리하는 state를 통해 해당 컴포넌트 반환 |
sessionStorage에 진입했던 Funnel 페이지 저장 | useFunnel 내부 state에 진입한 컴포넌트 저장하고 클로저 특성으로 다른 함수에 의해 제어 가능 |
- 차후에 애니메이션을 적용할 때 header는 고정하고 내부 컴포넌트 UI만 바뀌는게 페이지 전체가 바뀌는 것보다 자연스러울 것 같아서
- toss 어플 처럼 중간에 다른 어플에서 인증해야해서 이탈했다가 다시 들어와도 정보가 저장되어 있어야할 상황이 없어서
const SignUp: NextPage<Props> = () => {
const [signupInfo, setSignupInfo] = useState<SignupPostRequest>({ /* 생략 */ });
const [Funnel, toPrevStep, toNextStep] = useFunnel(STEP_NAMES);
const steps = [AccountSetup, NameSetup, JobSetup, SubjectSetup];
const renderSteps: Function = (): ReactElement[] => {
return steps.map((Step, i) => (
<Funnel.Step key={STEP_NAMES[i]} name={STEP_NAMES[i]}>
<Header toPrevStep={toPrevStep} rightText={i > 0 ? `${i}/3` : ''} />
<Step {...{ signupInfo, setSignupInfo, toNextStep }} />
</Funnel.Step>
));
};
return (
<Funnel>
{renderSteps()}
<Funnel.Step name="add-home">
<AddHome
{...{
signupInfo,
deferredPrompt,
setDeferredPrompt,
}}
/>
</Funnel.Step>
</Funnel>
);
};
export default SignUp;
공통으로 사용하는 state는 /signup 페이지에서 관리하고 state와 setState 함수를 컴포넌트 props로 내려주는 방식을 채택했다.
toPrevStep
과 toNextStep
은 각각 Header의 뒤로가기 버튼, 컴포넌트 내부의 하단 BottomBtn 버튼을 클릭했을 때 호출될 수 있도록 했다.
하나의 파일 내에서 모든 step을 관리할 수 있기 때문에 Funnel을 사용하기 전보다 state 변경 추적이 수월해졌고, 컴파운드 컴포넌트 사용을 통해 Funnel의 세부 구현을 감출 수 있다는 점이 내가 작성한 useFunnel 커스텀 훅 적용의 가장 큰 장점으로 느껴졌다.
라이브러리를 뜯어 보던 중 shallow routing을 사용하면 그 routing을 제어하는 상위 컴포넌트의 state는 유지된다는 점을 알게 되었다.
따라서 query parameter로 step 제어하기 vs useFunnel 내부에서 state로 step 제어하기
에 대해서 내가 택한 방식이 어떤 이점을 더 크게 가질지..
애니메이션을 적용해보면서 조건부 컴포넌트 렌더링의 이점을 가져갈 수 있을지 추가로 테스트하고 정리할 예정이다.