← Dev Log

토스의 useFunnel 훅을 직접 구현해보기

·4 min read

오늘 한 일

  • Gloddy 개발
  • GDG 회의

하루 요약

  • 14:00 ~ 19:30 카공
  • 20:00 ~ 01:00 집공

Gloddy 개발 - useFunnel 직접 구현

  • useFunnel을 직접 구현해보았다. (시도해보았다)

왜? 그냥 토스꺼 쓰면 되는거 아냐?

토스에서 만든 useFunnel은 내부적으로 next/router를 사용하고 있다. 그래서 Next 13 버전에서는 사용할 수 없다.. 관련 이슈

그래서 직접 만들어보았다.

토스의 Slash 라이브러리의 useFunnel 훅을 참고하여 작성했다.

소스 코드
/* eslint-disable react/jsx-no-useless-fragment */
'use client'

import { useRouter, useSearchParams } from 'next/navigation'
import { Children, isValidElement, useEffect, useState } from 'react'

type NonEmptyArray<T> = [T, ...T[]]

interface FunnelProps {
  children: React.ReactNode
}

interface StepProps<Steps extends NonEmptyArray<string>> {
  name: Steps[number]
  children: React.ReactNode
}

export function useFunnel<Steps extends NonEmptyArray<string>>(
  steps: Steps,
  options?: { initialStep?: Steps[number]; stepQueryKey?: string }
) {
  const initialStep = options?.initialStep ?? steps[0]
  const queryKey = options?.stepQueryKey ?? 'step'
  const [step, setStep] = useState<Steps[number]>(initialStep)
  const router = useRouter()
  const searchParams = useSearchParams()

  const nextStep = () => {
    const currentIndex = steps.indexOf(step)
    if (currentIndex < steps.length - 1) {
      setStep(steps[currentIndex + 1])
      window.history.pushState(
        null,
        '',
        `${window.location.pathname}?${queryKey}=${steps[currentIndex + 1]}`
      )
    }
  }

  const prevStep = () => {
    const currentIndex = steps.indexOf(step)
    if (currentIndex > 0) {
      setStep(steps[currentIndex - 1])
    }

    router.back()
  }

  const Funnel = ({ children }: FunnelProps) => {
    const childrenArray = Children.toArray(children)
      .filter(isValidElement)
      .filter((child) => {
        return (child.props as StepProps<Steps>).name !== undefined
      })

    childrenArray.forEach((child) => {
      if (!steps.includes((child.props as StepProps<Steps>).name)) {
        throw new Error('스텝 이름이 잘못되었습니다.')
      }
    })

    return <>{children}</>
  }

  const Step = ({ name, children }: StepProps<Steps>) => {
    return step === name ? <>{children}</> : null
  }

  Funnel.Step = Step

  window.addEventListener('popstate', () => {
    const currentStep = searchParams.get(queryKey) as Steps[number]
    console.log('pop', currentStep)
    if (currentStep) {
      setStep(currentStep)
    }
  })

  window.addEventListener('pushstate', () => {
    const currentStep = searchParams.get(queryKey) as Steps[number]
    console.log('push', currentStep)
  })

  useEffect(() => {
    const currentStep = searchParams.get(queryKey) as Steps[number]
    if (!currentStep) {
      window.history.replaceState(
        null,
        '',
        `${window.location.pathname}?${queryKey}=${initialStep}`
      )
    }
  }, [initialStep, queryKey, searchParams])

  return { currentStep: step, Funnel, nextStep, prevStep } as const
}

최대한 내 방식대로 해봤다. useFunnel 훅을 사용하면 다음과 같이 사용할 수 있다.

export default function FeedbackWrapper() {
  const { Funnel, prevStep, nextStep } = useFunnel(['praise', 'mate'])
  const { handleSubmit } = useFeedbackContext()

  const onSubmit = (data: FeedbackRequestType) => {
    console.log(data)
  }

  return (
    <Funnel>
      <Funnel.Step name="praise">
        <PraiseComponent onPrevClick={prevStep} onNextClick={nextStep} />
      </Funnel.Step>
      <Funnel.Step name="mate">
        <MateComponent onPrevClick={prevStep} onNextClick={handleSubmit(onSubmit)} />
      </Funnel.Step>
    </Funnel>
  )
}

아직 구현되지 않은 부분

  • 브라우저에서 뒤로가기를 눌렀을 때, step이 바뀌지 않는다. popState로 처리해서 뒤로 갈 때는 되는거같은데 앞으로 갈 때 또 안된다..

  • 쿼리스트링으로 현재 스텝을 표시해서, 새로고침을 했을 때도 현재 스텝을 유지할 수 있도록 해야한다.

  • 그 외 최적화

Gloddy 개발 - react-hook-form 에러

react-hook-formFormProvider를 커스텀해서 사용하고 있었다.

근데 이런 에러가 떴다.

useFormContext로 formState를 가져오니까 생긴 문제였다.

원인은 정확히 알 수 없어서 일단 임시방편으로 useFormStatecontrol을 받아와 해결했다.

오늘은 늦었으니 다음에 다시 정확한 원인을 찾아보자..!


내일 할 일

  • Gloddy 개발