[TIFY 개발일지 #5] toss/slash의 useFunnel 원리를 이용해 취향 설문조사 페이지 구현하기 🎁

김유진·2023년 11월 15일
3

slash

목록 보기
3/3

지난 시간동안 toss/slash의 useFunnel의 기본적인 동작 원리를 분석해보았다. 이제 내가 useFunnel 라이브러리를 훑어보게 된 근본적인 이유를 프로젝트에 적용해서 해결해보고자 한다.

먼저, 토스에서 사용하고 있는 useFunnel의 유용한 기능들을 리스트업해보자.

  1. 합성 컴포넌트를 활용하여 퍼널 컴포넌트의 동작을 선언적으로 관리하여 직관적으로 코드를 파악할 수 있고, 읽기 쉽다.
  2. 쿼리스트링으로 퍼널의 스텝을 관리하여 뒤로가기, 스텝 페이지 불러오기가 용이하다.
  3. 제네릭을 통하여 컴포넌트의 타입이 명확히 정의되어 있다.
  4. 다음 스텝으로 넘어갈 때 실행되는 함수를 활용할 수 있다.

이 네 가지 유용한 점을 활용하고 싶은데, TIFY 프로젝트에 바로 적용하기에는 큰 산이 있었다. 바로 TIFY 프로젝트는 React로 개발을 진행 중이고, useFunnel의 구현은 Next API 함수로 이루어져 있다는 것이다.

그러므로 history 관리나 shallow Routing을 구현하기 위해서는 React의 함수들을 적절히 이용하거나 새롭게 로직을 만들어 관리해야 한다.

1. 기본적인 틀 생성

export interface FunnelProps<Steps extends NonEmptyArray<string>> {
  steps: Steps
  step: Steps[number]
  children:
    | Array<ReactElement<StepProps<Steps>>>
    | ReactElement<StepProps<Steps>>
}
export 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}</>
}

Funnel의 최상단을 감쌀 수 있는 Funnel 컴포넌트이다. 토스 slash 라이브러리의 구현과 다르지 않다. 원리를 간단히 설명하자면, string 타입의 수정 불가능한 배열을 Steps 라는 제네릭 타입으로 받고, step의 이름인 name 프로퍼티를 가지는 자식컴포넌트들을 Funnel의 자식 요소로 구분하여 리턴한다.

그럼 합성 컴포넌트로 사용될 Step 컴포넌트는 어떻게 생겼을까?

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

export const Step = <T extends NonEmptyArray<string>>({
  children,
}: StepProps<T>) => {
  return <>{children}</>
}

간단하다. name이라는 프로퍼티를 가지며, children 요소를 리턴해준다.

2. queryString과 일치하는 Funnel 찾기

const FunnelComponent = useMemo(
    () =>
      Object.assign(
        function RouteFunnel(props: RouteFunnelProps<Steps>) {
          const step = searchParams.get('funnel-step') ?? options?.initialStep
		  if(step === null) {
          	console.log('step이 존재하지 않습니다.')
          }
          return <Funnel<Steps> steps={steps} step={step} {...props} />
        },
        {
          Step,
        },
      ),
    [step],
  )

Object.assign 을 이용하여 합성 컴포넌트를 제작한다.
현재 funnel-step이라는 params 가 가지는 값을 가져오고, 만약 없다면 첫번째 스텝을 가져온다. 일치하지 않는 step이 있을 경우에는 에러 콘솔을 띄울 수 있게 한다.

또한, queryParams의 step을 정의하여 step이 바뀔 때마다 새로운 컴포넌트를 랜더링할 수 있도록 한다.

3. setStep으로 다음 스텝 정의하기

위에서 언급했듯이, slash와 다른 점은 Next.js 프로젝트가 아닌, React 프로젝트이기 때문에 useRouter API를 사용하지 못한다는 점이다.
내가 구현하고자 하는 요구사항은 아래와 같다.

  • 이전 페이지로 돌아가면 전의 답변을 이어 할 수 있어야 하므로, history에 push 하여 뒤로가기 버튼을 눌렀을 때 이전 답변으로 돌아가고 답변한 내용 삭제
  • 새로고침 (깜빡임) 존재하지 않게 shallow routing 을 할 것

이러한 요구사항을 만족하기 위해, 내가 선택한 로직은 아래와 같다.

const setStep = (step: Steps[number], setStepOptions?: SetStepOptions) => {
  navigate({ pathname: location.pathname, search: `?funnel-step=${step}` })
  return
}

useRouter와 같은 역할을 하는 것이 바로 useNavigate이므로 push를 통해 구현하는것이다.

4. 사용하지 않는 로직 제거

먼저 withState를 사용하지 않을 것이기 때문에 관련된 로직을 제거한다. funnel이 몇번째 스텝까지 와 있는지에 대한 정보를 sessionStorage의 저장소를 통해서 관리할 수 있으나, TIFY 프로젝트는 사용자의 답변을 localStorage에 저장하고, 답변을 완료한다면 해당 localStorage를 비우고, 설문조사 페이지를 나가면 localStorage를 비우게끔 구현해놨다.

새로고침을 시도해도 현재 유저가 답변한 내용이 남아있을 수 있도록 구현하기 위해서이다.
물론, sessionStorage를 이용한다면 세션이 닫혔을 때 자동으로 답변한 내역이 달아가기 때문에 그것으로 구현하는 것이 더욱 정당해 보여 리팩토링으로 진행할 예정이다.

0개의 댓글