REINPUT에서 useFunnel 적용하기

xoxristine·2024년 5월 21일
0

🧐 기존 방식 문제점

기존 방식은 Zustand로 전역 상태를 관리하고, 하단 버튼을 누르면 단계별로 page routing이 되는 방식이었다.
이 방식으로 개발했을 때 문제점은 아래와 같다고 느꼈다.

  1. 5단계로 이루어진 signUp 과정을 모두 각각 routing 하고 있어서 개발할 때 추적하기 어려움
  2. 뒤로가기 누르면 이전에 입력한 값 저장 안됨
1. signUp과 관련된 pages2. 뒤로가기 누르면 상태 저장 안됨

따라서 위 두가지 문제를 해결하기 위해 useFunnel 커스텀 훅을 만들어 사용해보았다.

📍 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);
};

useFunnel 반환값

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에 진입한 컴포넌트 저장하고 클로저 특성으로 다른 함수에 의해 제어 가능
  • query parameter 사용하지 않고 조건부 컴포넌트 렌더링 방식을 선택한 이유?
  1. 차후에 애니메이션을 적용할 때 header는 고정하고 내부 컴포넌트 UI만 바뀌는게 페이지 전체가 바뀌는 것보다 자연스러울 것 같아서
  2. toss 어플 처럼 중간에 다른 어플에서 인증해야해서 이탈했다가 다시 들어와도 정보가 저장되어 있어야할 상황이 없어서

📍 useFunnel 사용부

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로 내려주는 방식을 채택했다.
toPrevSteptoNextStep은 각각 Header의 뒤로가기 버튼, 컴포넌트 내부의 하단 BottomBtn 버튼을 클릭했을 때 호출될 수 있도록 했다.

하나의 파일 내에서 모든 step을 관리할 수 있기 때문에 Funnel을 사용하기 전보다 state 변경 추적이 수월해졌고, 컴파운드 컴포넌트 사용을 통해 Funnel의 세부 구현을 감출 수 있다는 점이 내가 작성한 useFunnel 커스텀 훅 적용의 가장 큰 장점으로 느껴졌다.

📍 더 생각해 봐야 할 점

라이브러리를 뜯어 보던 중 shallow routing을 사용하면 그 routing을 제어하는 상위 컴포넌트의 state는 유지된다는 점을 알게 되었다.

따라서 query parameter로 step 제어하기 vs useFunnel 내부에서 state로 step 제어하기 에 대해서 내가 택한 방식이 어떤 이점을 더 크게 가질지..
애니메이션을 적용해보면서 조건부 컴포넌트 렌더링의 이점을 가져갈 수 있을지 추가로 테스트하고 정리할 예정이다.

profile
🔥🦊

0개의 댓글