토스 슬래시에서 진유림님이 설명해주신 useFunnel 이 상당히 인상 깊었다.
그래서 회원가입 창을 만들어보면서 이 useFunnel을 최대한 직접 구현해보고 싶었다.
실제 토스에서 출시한 라이브러리 코드와는 많이 다르다.
라이브러리 코드를 직접 살펴보았지만 아직 나의 지식 한해서는 이해하기 좀 힘겨운 부분이 있었다.
하지만 직접 구현을 해본다는 점에 의의를 두고 기록하려 한다.
export default function RegisterFunnel({ userEmail }: RegisterFromPropsType) {
const [step, setStep] = useState<'welcome' | 'userName' | 'userImage' | 'finish'>('welcome');
return (
<FunnelCard>
{step === 'welcome' && <WelcomeFunnel setStep={setStep} />}
{step === 'userName' && <UserNameFunnel setStep={setStep} />}
{step === 'userImage' && <UserImageFunnel setStep={setStep} />}
{step === 'finish' && <FinishFunnel />}
</FunnelCard>
);
}
영상처럼 컴포넌트를 클릭했을때, 각각 다른 퍼널 컴포넌트를 보여주도록 만들어보았다.
영상에서 구현한 방식은 이런식이다.
import { Children, useState, ReactNode, isValidElement } from 'react';
interface StepProps {
name: string;
children: ReactNode;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function Step({ name, children }: StepProps) {
return <>{children}</>;
}
const useFunnel = (steps: readonly string[]) => {
const [step, setStep] = useState(steps[0]);
function Funnel({ children }: { children: ReactNode }) {
const validElements = Children.toArray(children).filter(
(child) => isValidElement(child) && child.props.name === step,
);
return <>{validElements}</>;
}
Funnel.Step = Step;
return { Funnel, step, setStep };
};
export default useFunnel;
여기서 토스 영상에서는 setStep에 다음 스텝으로 넘어 갈 값들을 직접 넣어주었다.
또한 router 의 shallow push 를 사용해서 쿼리파라미터를 업데이트 해주는 방식으로 퍼널 컴포넌트들을 이동시켜주었다.
하지만 나같은 경우엔 회원가입 페이지에 이 퍼널 컴포넌트들을 사용할 예정이었다.
또한 한 컴포넌트 내에서 사용하더라도 유효성 검사를 각각의 퍼널 컴포넌트에서 해준 후에 마지막 퍼널 컴포넌트에서 최종적인 상태 값을 서버로 보내주면 되지 않을까? 라는 생각이 들었다.
그렇기 때문에 이 step 값들을 인덱스로 관리해주고, nextStepHandler 와 previousStepHanlder로 따로 나누어주었다.
const nextStepHandler = () => {
setStepIndex((prevStepIndex) => prevStepIndex + 1);
setStep(steps[stepIndex + 1]);
};
const previousStepHandler = () => {
setStepIndex((prevStepIndex) => prevStepIndex - 1);
setStep(steps[stepIndex - 1]);
};
index 를 관리할 state 를 하나 더 두고, next, previous 스텝 핸들러를 만들어주었다.
const { Funnel, nextStepHandler, previousStepHandler } = useFunnel([
'welcome',
'userName',
'userImage',
'finish',
] as const);
return (
<FunnelCard>
<Funnel>
<Funnel.Step name="welcome">
<WelcomeFunnel nextStepHandler={nextStepHandler} />
</Funnel.Step>
<Funnel.Step name="userName">
<UserNameFunnel
previoustStepHandler={previousStepHandler}
nextStepHandler={nextStepHandler}
/>
</Funnel.Step>
<Funnel.Step name="userImage">
<UserImageFunnel
previoustStepHandler={previousStepHandler}
/>
</Funnel.Step>
<Funnel.Step name="finish">
<FinishFunnel />
</Funnel.Step>
</Funnel>
</FunnelCard>
그리고 이 핸들러를 각각 단계에 맞게 동적으로 받을수 있도록 해주었다.
정말 간단하지만 이해할수 있는 선에서 최대한 직접 구현해보려했다.
라이브러리랑 비교해보며 더 다듬어가야겠다.
위의 코드로 진행을 하면서 useForm 훅과 바인딩 해주었을때, 한글자만 입력하더라도 포커싱을 잃어버리는 문제가 발생했다.
원인은 step 과 관련한 컴포넌트들을 useFunnel 안에 선언해주었는데, 호출될때마다 새롭게 컴포넌트들을 렌더링하기 때문이었다.
저 Funnel 컴포넌트들 렌더링 해주는 부분을 useFunnel 바깥으로 선언을 해주었더니 해결되었다.