여러 단계에 걸쳐 상태를 효과적으로 수집하기 위한 useFunnel 구현 및 적용 - bookkbookk/bookkbookk GitHub Wiki

문제 상황

  1. 프로젝트 여러 곳에서 반복적으로 유저에게 많은 정보를 입력 받아야 하는 비슷한 경우가 반복적으로 발생 (새로운 책 추가, 새로운 북클럽 생성, 북클럽 모임 생성 등)
  2. 정보 입력 시 유저의 피로감을 감소시킬 수 있는 UI 구조와 유지보수하기 쉬운 구조 설계 고민
    • 한번에 모든 정보를 입력받지 않고 여러 단계를 나눠서 단계 별로 일부 정보를 입력받고 다음 단계로 이동하는 UX/UI 방식을 고려
    • 단계를 이동하는 흐름과 유저가 입력한 상태에 대한 관심사를 응집하여 관리할 수 있는 방법을 고민

해결 과정

  • 매주 진행하는 기술학습 스터디에서 리액트 아티클 리뷰를 주제로 이러한 문제를 해결한 방법을 소개하는 토스 SLASH 유튜브 채널의 <퍼널: 쏟아지는 페이지 한 방에 관리하기>를 선정하여 발표를 진행 (🔗 Blog Link)
  • 아티클 내용을 기반으로 현재 프로젝트에 적합하게 custom hook을 직접 구현하여 프로젝트 전반에 적용

핵심 내용

1. 커스텀 훅 useFunnel 🔗 Code Link

  • useFunnel은 퍼널의 단계를 관리하고, 각 단계의 상태를 업데이트할 수 있는 훅
  • 초기 단계를 설정할 수 있으며, 현재 활성화된 단계의 인덱스와 해당 단계를 변경할 수 있는 함수를 반환
  • useMemo를 사용하여 성능 최적화를 위해 FunnelComponent를 메모이제이션

2. 퍼널 컴포넌트 구현 (Funnel 및 Step) 🔗 Code Link

  • Funnel 컴포넌트

    • 주어진 단계에 해당하는 컴포넌트를 렌더링하며, 현재 활성화된 단계만을 표시
    • Funnel 컴포넌트는 주어진 단계에 해당하는 컴포넌트를 찾아 렌더링하고, 만약 해당 단계가 없으면 에러 발생
  • Step 컴포넌트

    • Step 컴포넌트는 각 단계에 해당하는 UI를 정의

핵심 트러블 슈팅

  • 실제 토스 slash 라이브러리에서 제공하는 useFunnel은 NextJS 환경 기반으로 URL path를 변경하여 현재 step을 변경하여 페이지 전체가 라우팅되는 방식

초기 구현

  • 위의 방식과 동일하게 URL Query Parameter로 현재 단계를 변경하도록 react-router에서 제공하는 useParams를 활용하여 구현
  • 그러나 URL path가 변경되면 전체 페이지가 다시 렌더링 되어 Root 컴포넌트의 loader로 등록해둔 토큰 재발급이 매번 요청됨

재구현

  • 현재 프로젝트에서는 각 단계마다 전체 페이지가 이동하는 것이 아니라 페이지의 일부만 단계에 따라 렌더링하면 되는 상황이므로 URL path가 아닌 hook 내부에서 state로 step 상태 관리하도록 변경
  • step이 변경되었을 때 이에 맞는 Step 컴포넌트를 렌더링하기 위해 useMemo의 dependencies에 step 추가

문제 해결

  • 더 높은 수준의 추상화를 통해 재사용성 향상
  • '상태를 수집하는 단계 구성'이라는 관심사에 따라 응집도를 향상시켜 코드의 가독성 향상
📌 사용 예시
export default function NewBookClubFunnel() {
  const { profile, member, congratulation } = NEW_BOOK_CLUB_FUNNEL;
  const funnelSteps = [profile, member, congratulation] as const;
  const navigate = useNavigate();

  const [Funnel, activeStepIndex, setStep] = useFunnel(funnelSteps, {
    initialStep: profile,
  });

  return (
    <BoxContent sx={{ position: "relative" }}>
      <Stepper activeStep={activeStepIndex} funnel={funnelSteps} />
      <Funnel>
        <Funnel.Step name={profile}>
          <BookClubProfile
            onPrev={() => navigate(-1)}
            onNext={() => setStep(member)}
          />
        </Funnel.Step>
        <Funnel.Step name={member}>
          <BookClubMember
            onPrev={() => setStep(profile)}
            onNext={() => setStep(congratulation)}
          />
        </Funnel.Step>
        <Funnel.Step name={congratulation}>
          <BookClubCongratulation />
        </Funnel.Step>
      </Funnel>
    </BoxContent>
  );
}
⚠️ **GitHub.com Fallback** ⚠️