이번 앱잼을 진행하며 저는 두개의 페이지를 맡았습니다. 하나는 게스트인 사람이 호스트 신청을 위해 작성해야 하는 다양한 질문에 대한 입력폼을 다루는 페이지이고, 다른 하나는 호스트가 자신만의 모임을 개설하기 위해 작성해야 하는 다양한 질문에 대한 입력폼을 다루는 페이지입니다.

스크린샷 2024-07-19 오후 11.17.05.png

스크린샷 2024-07-19 오후 11.18.02.png

하나는 총 3단계, 다른 하나는 총 4단계의 Step을 가지고 있고, 한 페이지에 꽤 많은 데이터를 다루고 있어 꽤나 복잡해보입니다.

이번 앱잼을 진행하기 전 사전 과제로 funnel구조를 사용하여 회원가입 로직 구현해보기 과제를 진행했는데 이때 퍼널구조 자료들을 찾아보다가 TOSS | SLASH 23 - 퍼널 : 쏟아지는 페이지 한 방에 관리하기 영상을 보게 되었습니다. 이미 토스에서 이러한 사용자 입력폼을 위한 퍼널구조의 대부분을 구현해놨고, 라이브러리화 시켜놓은것을 보고 많은 도움을 받았습니다.

어떤 방식의 퍼널구조를 사용해야 하는가?

퍼널구조에도 다양한 방식이 있습니다. 하나의 공통된 URL로 모든 퍼널구조의 페이지들을 다루는 방법이 있고, path parameter를 추가해서 각각 다른 URL을 가진 페이지로 만드는 방식이 있는데 이번 프로젝트에서는 다음 페이지로 넘어갈때마다 path parameter를 갈아끼워 다른 URL이 되도록 하는 방식을 채택했습니다. 이와 같이 퍼널구조를 구현하며 고려했던 부분들은 아래와 같습니다.

  1. 뒤로 가기에 대응하기 각 퍼널구조는 세단계 혹은 네단계의 과정을 거쳐 진행되는데 이때 이전 페이지에 입력한 내용을 수정하고 싶거나 적은 내용을 검토하기 위해서는 뒤로 가기에 대응할 필요가 있었습니다. 이 모든 과정을 하나의 URL로 관리한다면 뒤로 가기 클릭시 퍼널구조 페이지로 들어오기 전으로 돌아가버리기 때문에 작성했던 내용도 모두 사라지고 새로 작성해야 한다는 단점이 있습니다. 이러한 단점을 보완하기 위해 URL에 각 step의 name값을 path parameter로 갈아끼워주는 방식으로 구현했습니다.
  2. 각 Step에서의 입력값 handle 및 validation 하나의 step에서는 적게는 1개 많게는 7개의 데이터 값을 다뤄야 합니다. 이때 각 step별로, 각 입력값 별로 state를 나누는것은 매우 비효율적이라고 생각했습니다. 따라서 이러한 문제를 해결하기 위해 최종 POST요청시 보낼 데이터와 같은 형식으로 미리 데이터 객체 초기값을 만들어놓고, 각 Step에서 이를 가져와 입력값을 변경해주었습니다. 추가적으로 하나의 Step 페이지 컴포넌트에서 이미 너무 많은 tsx코드를 담고있기 때문에 입력값 변경에 대한 handle함수나 입력값들의 유효성을 검사하는 로직들은 분리하는것이 좋겠다 판단했습니다. 이를 위해 커스텀 훅을 적극 활용했습니다.

useFunnel 훅 만들기

import { ReactElement, ReactNode, useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';

export interface StepProps {
  name: string;
  children: ReactNode;
}
export interface FunnelProps {
  children: Array<ReactElement<StepProps>>;
}

export const useFunnel = (defaultStep: string, basePath: string) => {
  const [step, setStep] = useState(defaultStep);
  const navigate = useNavigate();
  const { step: urlStep } = useParams<{ step: string }>();

  useEffect(() => {
    if (urlStep) {
      setStep(urlStep);
    }
  }, [urlStep]);

  const Step = (props: StepProps): ReactElement => {
    return <>{props.children}</>;
  };

  const Funnel = ({ children }: FunnelProps) => {
    const targetStep = children.find((childStep) => childStep.props.name === step);

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

  const nextStep = (next: string) => {
    setStep(next);
    navigate(`/${basePath}/${next}`);
  };

  return { Funnel, Step, setStep, nextStep, currrendStep: step } as const;
};

위 코드는 funnel구조를 다루기 위한 커스텀 훅 코드입니다. Step : 각 step에 대한 페이지 컴포넌트를 담을 컴포넌트

Funnel : 여러 Step들을 담고, step의 name값을 받아 어떤 Step을 보여줄지 확인

nextStep : step값을 다음 step값으로 변경해주고, 다음 페이지로 라우팅 해줄 함수

퍼널구조 전체를 담을 페이지 생성

import { LogoHeader } from '@components';
import ClassPost from '@pages/class/components/ClassPost/ClassPost';
import { useFunnel } from 'src/hooks/useFunnel';
import { classPostPageLayout } from './ClassPostPage.style';
import { Provider } from 'jotai';
import { useParams } from 'react-router-dom';

const steps = ['step1', 'step2', 'step3', 'finish'];

const ClassPostPage = () => {
  const { step } = useParams<{ step: string }>();
  const { Funnel, Step, nextStep } = useFunnel(step || steps[0], 'class/post');
  return (
    <Provider>
      <LogoHeader />
      <div css={classPostPageLayout}>
        <ClassPost steps={steps} nextClickHandler={nextStep} Funnel={Funnel} Step={Step} />
      </div>
    </Provider>
  );
};

export default ClassPostPage;