지난 시간동안 toss/slash의 useFunnel의 기본적인 동작 원리를 분석해보았다. 이제 내가 useFunnel 라이브러리를 훑어보게 된 근본적인 이유를 프로젝트에 적용해서 해결해보고자 한다.
먼저, 토스에서 사용하고 있는 useFunnel의 유용한 기능들을 리스트업해보자.
- 합성 컴포넌트를 활용하여 퍼널 컴포넌트의 동작을 선언적으로 관리하여 직관적으로 코드를 파악할 수 있고, 읽기 쉽다.
- 쿼리스트링으로 퍼널의 스텝을 관리하여 뒤로가기, 스텝 페이지 불러오기가 용이하다.
- 제네릭을 통하여 컴포넌트의 타입이 명확히 정의되어 있다.
- 다음 스텝으로 넘어갈 때 실행되는 함수를 활용할 수 있다.
이 네 가지 유용한 점을 활용하고 싶은데, TIFY 프로젝트에 바로 적용하기에는 큰 산이 있었다. 바로 TIFY 프로젝트는 React로 개발을 진행 중이고, useFunnel의 구현은 Next API 함수로 이루어져 있다는 것이다.
그러므로 history
관리나 shallow Routing
을 구현하기 위해서는 React의 함수들을 적절히 이용하거나 새롭게 로직을 만들어 관리해야 한다.
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
요소를 리턴해준다.
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
이 바뀔 때마다 새로운 컴포넌트를 랜더링할 수 있도록 한다.
위에서 언급했듯이, slash와 다른 점은 Next.js 프로젝트가 아닌, React 프로젝트이기 때문에 useRouter
API를 사용하지 못한다는 점이다.
내가 구현하고자 하는 요구사항은 아래와 같다.
push
하여 뒤로가기 버튼을 눌렀을 때 이전 답변으로 돌아가고 답변한 내용 삭제shallow routing
을 할 것이러한 요구사항을 만족하기 위해, 내가 선택한 로직은 아래와 같다.
const setStep = (step: Steps[number], setStepOptions?: SetStepOptions) => {
navigate({ pathname: location.pathname, search: `?funnel-step=${step}` })
return
}
useRouter
와 같은 역할을 하는 것이 바로 useNavigate
이므로 push
를 통해 구현하는것이다.
먼저 withState
를 사용하지 않을 것이기 때문에 관련된 로직을 제거한다. funnel이 몇번째 스텝까지 와 있는지에 대한 정보를 sessionStorage
의 저장소를 통해서 관리할 수 있으나, TIFY 프로젝트는 사용자의 답변을 localStorage에 저장하고, 답변을 완료한다면 해당 localStorage를 비우고, 설문조사 페이지를 나가면 localStorage를 비우게끔 구현해놨다.
새로고침을 시도해도 현재 유저가 답변한 내용이 남아있을 수 있도록 구현하기 위해서이다.
물론, sessionStorage
를 이용한다면 세션이 닫혔을 때 자동으로 답변한 내역이 달아가기 때문에 그것으로 구현하는 것이 더욱 정당해 보여 리팩토링으로 진행할 예정이다.