Toss Slash useFunnel 뜯어보기 (1)

xoxristine·2024년 5월 14일
0

아직 수정 중인 게시물 입니다..⚙️

📍 라이브러리 훅 소개

const [퍼널, setStep] = useFunnel(['스텝1', '스텝2', '스텝3'] as const)
const [퍼널, setStep] = useFunnel(['이름입력', '주민번호입력', '완료'] as const)

return (
  <퍼널>
    <퍼널.Step name="이름입력">
      <이름입력스텝 /> // ?funnel-step=이름입력 스텝일 때 렌더
    </퍼널.Step>

    <퍼널.Step name="주민번호입력">
      <주민번호입력스텝 /> // ?funnel-step=주민번호입력 스텝일 때 렌더
    </퍼널.Step>

    <퍼널.Step name="완료">
      <퍼널완료스텝 /> // ?funnel-step=완료 스텝일 때 렌더
    </퍼널.Step>
  </퍼널>
)
const KyoboLifeFunnel = () => {
  const [Funnel, state, setState] = useFunnel(['아파트여부', '지역선택', '완료'] as const).withState<{
    propertyType?: '빌라' | '아파트';
    address?: string;
  }>({});

  const 상담신청 = useLoanApplicationCallback();

  return (
    <Funnel>
      <Funnel.Step name="아파트여부">
        <아파트여부스텝 지역선택으로가기={() => setState(prev => ({...prev, step: '지역선택', isApartment: true}))} />
      </Funnel.Step>
      <Funnel.Step name="지역선택">
        <지역선택스텝 지역선택완료={(지역정보) => setState(prev => ({...prev, step: '완료', region: 지역정보}))} />
      </Funnel.Step>
      <Funnel.Step name="완료">
        <완료스텝 신청={() => 상담신청(state)} />
      </Funnel.Step>
    </Funnel>
  );
};

📍 Funnel.tsx

Funnel.tsx 전체 코드는 여기 참고!

👕 FunnelProps - interface

export interface FunnelProps<Steps extends NonEmptyArray<string>> {
  steps: Steps;
  step: Steps[number];
  children: Array<ReactElement<StepProps<Steps>>> | ReactElement<StepProps<Steps>>;
}

먼저 FunnelProps에서 상속받는 NonEmptyArray<string>> 타입부터 살펴보자.

export type NonEmptyArray<T> = readonly [T, ...T[]];

제네릭 타입으로 만든 배열 타입을 readonly로 반환한다.
따라서 FunnelProps는 아래와 같이 정리할 수 있다.

steps: 문자열 타입의 배열 (readonly)
step: steps의 배열 요소(문자열), number 인덱스로 접근 가능
children: ReactElement<StepProps<Steps>> 또는 배열

👚 Funnel Component

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);

  assert(targetStep != null, `${step} 스텝 컴포넌트를 찾지 못했습니다.`);

  return <>{targetStep}</>;
};

위에서 선언한 FunnelProps 타입의 props를 받아서 children 중 일치하는 step을 가진 컴포넌트를 반환하는 함수이다.

  1. React api인 isValidElement로 React element인지 먼저 확인하고
  2. 자식 컴포넌트의 props를 꺼내서 name이 존재하는지, steps 내에 존재하는지 확인한다.
  3. 검증된 children 중에서 step과 동일한 이름을 가진 children을 반환하게 된다.

잠깐! Partial 에 대해 아시는지~? Partial: 특정 타입의 부분 집합을 만족하는 타입을 정의

👕 StepProps - interface

export interface StepProps<Steps extends NonEmptyArray<string>> {
  name: Steps[number];
  onEnter?: () => void;
  children: ReactNode;
}

Step 함수의 props 타입을 정의해둔 부분으로, Steps 제네릭 타입을 받는다.

name: Steps 배열에 number 인덱스로 접근한 string 배열 요소
onEnter: () => void 타입의 함수
children: ReactNode

👚 Step Component

export const Step = <Steps extends NonEmptyArray<string>>({ onEnter, children }: StepProps<Steps>) => {
  useEffect(() => {
    onEnter?.();
  }, [onEnter]);

  return <>{children}</>;
};

컴포넌트 마운트 될 때, onEnter가 변경될 때 useEffect 실행해서 onEnter가 존재한다면 실행시킨다. & children 반환한다.

📍 배운 점 및 후기

  1. 제네릭을 잘 사용한다는게 이런걸까..
  2. Partial과 같은 유틸리티 타입 사용
  3. readonly와 const, as const 차이
  • const: 변수에 사용
  • readonly: 속성에 사용, const와 마찬가지로 object / array 안의 값들이 수정 안되는거 아님
  • as const: object property 값 / array의 아이템들 수정 불가능
    하지만 constant variable / readonly property 아니면 같은 타입으로 재할당 가능!
profile
🔥🦊

0개의 댓글