useFunnel 을 따라해서 컴포넌트 만들어보기

버건디·2023년 11월 28일
0

리액트

목록 보기
54/58
post-thumbnail

토스 슬래시에서 진유림님이 설명해주신 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>
  );
}

영상처럼 컴포넌트를 클릭했을때, 각각 다른 퍼널 컴포넌트를 보여주도록 만들어보았다.

영상에서 구현한 방식은 이런식이다.

- useFunnel.tsx

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 바깥으로 선언을 해주었더니 해결되었다.

profile
https://brgndy.me/ 로 옮기는 중입니다 :)

0개의 댓글