https://www.youtube.com/watch?v=NwLWX2RNVcw

 

프로젝트에서 온보딩 화면을 구현하게 되었는데, 같은 팀원이 퍼널 구조를 도입해보자며 보내준 동영상이다. 

 

퍼널(Funnel) 이란

사용자가 목표 지점까지 도달하는 일련의 과정이라 할 수 있다. 

회원가입 시 사용자 정보를 묻는 과정, 설문조사의 질문 페이지 등을 예시로 들 수 있다. 

 MBTI 검사 역시 일종의 퍼널이라 할 수 있다. 일련의 단계들을 거쳐 최종 결과를 도출해내는 구조이다. 

이 과정 속에서 여러가지 컴포넌트들과 데이터가 필요하고, 이 중 반복되는 요소들도 존재할 것이다. 

토스에서는 이러한 퍼널을 관리하기 위하여 useFunnel  이라는 훅을 제작하였다고 한다. 

 

useFunnel 구현해보기

 

const steps = [
  { name: 'Step1', component: OnBoardingStep1, nextStep: 'Step2' },
  { name: 'Step2', component: OnBoardingStep2, nextStep: 'Step3' },
  { name: 'Step3', component: OnBoardingStep3, nextStep: 'Step4' },
  { name: 'Step4', component: OnBoardingStep4, nextStep: 'Step6' },
  { name: 'Step6', component: OnBoardingStep6, nextStep: '/main' },
];

퍼널에 사용될 컴포넌트들의 정보를 담은 steps 배열이다. 

현재 스텝의 이름과 다음 스텝의 이름의 비교를 통하여 다음 화면을 렌더링할 것이다. 

 

//useFunnel.jsx
import { useState } from 'react';

export function useFunnel( defaultStep ){
  const [currentStep, setCurrentStep] = useState(defaultStep);
  
  const Step = ({ children }) => {
    return <>{children}</>
  };

  const Funnel = ({ children }) => {
    const targetStep = children.find(childStep => childStep.props.name === currentStep );
    return <>{targetStep}</>
  };

  return [Funnel, Step, currentStep, setCurrentStep];
}

currentStep: step의 이름값을 가지는 state. defaultStep 은 첫 step의 name 인 Step1 을 할당해주었다. 

setCurrentStep: step 의 상태 변화 함수

Step: step을 렌더링하는 컴포넌트

Funnel: 해당하는 단계의 step 을 렌더링하는 컴포넌트

 

const [Funnel, Step, currentStep, setCurrentStep] = useFunnel('Step1');

적용하려는 페이지에서 다음과 같이 useFunnel 훅을 호출해주었다. 

<Funnel>
  {steps.map((step, idx) => (
    <Step key={idx} name={step.name}>
      <step.component onNext={handleNext}/>
    </Step>
  ))}
</Funnel>

Step 컴포넌트는 steps 배열의 컴포넌트를 렌더링하고 있다. 

Funnel 컴포넌트는 Step 컴포넌트에서 렌더링하고 있는 step의 name 이 현재 보여줄 step의 name과 일치하면 이를 렌더링해준다. 

 

const handleNext = () => {
  const nextStepIndex = steps.findIndex((step) => step.name === currentStep) + 1;

  if (nextStepIndex < steps.length) {
    setCurrentStep(steps[nextStepIndex].name);
  } else {
    navigate(steps[nextStepIndex - 1].nextStep);
  }
};

currentStep을 변경해주는 handleNext 함수이다. 

다음 step의 name을 찾아서 상태를 갱신해주었다. 갱신된 상태에 따라 Funnel 컴포넌트에서 타겟으로 하는 Step 컴포넌트가 변경되기 때문에 새로운 step 화면이 렌더링될 수 있다. 

마지막 step의 경우는 다른 페이지로 이동할 수 있도록 useNavigate 훅을 이용해주었다. 

 

전체 코드이다. 

function OnBoarding() {
  const navigate = useNavigate();
  const steps = [
    { name: 'Step1', component: OnBoardingStep1, nextStep: 'Step2' },
    { name: 'Step2', component: OnBoardingStep2, nextStep: 'Step3' },
    { name: 'Step3', component: OnBoardingStep3, nextStep: 'Step4' },
    { name: 'Step4', component: OnBoardingStep4, nextStep: 'Step6' },
    { name: 'Step6', component: OnBoardingStep6, nextStep: '/main' },
  ];
  const [Funnel, Step, currentStep, setCurrentStep] = useFunnel('Step1');

  const handleNext = () => {
    const nextStepIndex = steps.findIndex((step) => step.name === currentStep) + 1;

    if (nextStepIndex < steps.length) {
      setCurrentStep(steps[nextStepIndex].name);
    } else {
      navigate(steps[nextStepIndex - 1].nextStep);
    }
  };

  return (
    <S.OnBoardingPageWrapper>
      <Funnel>
        {steps.map((step, idx) => (
          <Step key={idx} name={step.name}>
            <S.ProgressWrapper>
              <StepProgress steps={steps} cur={step.name} />
            </S.ProgressWrapper>
            <step.component onNext={handleNext}/>
          </Step>
        ))}
      </Funnel>
    </S.OnBoardingPageWrapper>
  );
}
export default OnBoarding;

 

 

 

다음과 같이 Funnel 구조를 사용할 경우

1. 상위 컴포넌트에서 흐름을 관리하고, 하위 컴포넌트에서 세부 UI를 관리할 수 있다. 

상태 변화의 흐름을 한눈에 보기 쉬우며 각각의 컴포넌트 관리도 더 편하게 할 수 있다. 

2. 상태 변화를 위한 함수를 상위에서 한번에 관리할 수 있다. 

3. 공통된 로직을 통하여 재사용할 수 있다. 

실제로 온보딩 화면에서 사용한 useFunnel 훅을 여러 질문에 대한 답을 하는 페이지에서 재사용하였다. 

 

<Funnel>
  {steps.map((step, idx) => (
    <Step key={idx} name={step.name}>
      <step.component
        onNext={handleNext}
        onPrev={handlePrev}
      />
      <S.ProgressWrapper>
        <StepProgress steps={steps} cur={step.name} />
      </S.ProgressWrapper>
    </Step>
  ))}
</Funnel>

한 번 생성한 useFunnel 훅을 다른 페이지에서도 이용할 수 있다.

해당 페이지에서는 뒤로가기 버튼도 추가하여 용도에 맞게 기능을 추가하여 구현할 수 있었다. 

 

진행 상황을 Progress 바로 표현하기

Progress '바' 라는 표현이 적절한 건지는 모르겠지만,,, 위와 여러 단계중 해당 단계를 표현하는 이미지를 렌더링 하는

컴포넌트를 함께 구현해보았다. 

퍼널 구조를 위해 선언한 steps 배열을 활용하였다. 

<Step key={idx} name={step.name}>
  <step.component
    onNext={handleNext}
    onPrev={handlePrev}
  />
  <S.ProgressWrapper>
    <StepProgress steps={steps} cur={step.name} />
  </S.ProgressWrapper>
</Step>

하위 컴포넌트에서 StepProgress를 관리하게 된다면 각 컴포넌트마다 StepProgress 컴포넌트를 호출해야 하며, 현재 단계를 props로 2번 넘겨주어야 하는 불편함이 존재한다. 

StepProgress 역시 상태의 흐름과 관련되었다고 생각했기 때문에 상위 컴포넌트에서 같이 렌더링해주었다. 

 

function StepProgress({ steps, cur }){
  return(
    <S.StepProgressWrapper>
      {steps.map((step, idx)=>(
        cur === step.name 
          ? 
            <IcCurrentProgressCircle key = {idx} /> 
          : 
            <IcProgressCircle key = {idx} />
      ))}
    </S.StepProgressWrapper>
  );
}
export default StepProgress;

StepProgress 컴포넌트에서는 현재 step이 타겟 step과 동일하다면 단계를 표현하는 이미지를 렌더링하도록 하였다. 


useFunnel 훅을 처음 본다면 코드를 이해하기 어려울 수 있다고 생각한다.. 나도 그랬기 때문에...

Step 컴포넌트를 통하여 하위 컴포넌트를 렌더링하고

Funnel 컴포넌트를 통하여 해당 컴포넌트가 타겟 컴포넌트인지를 확인 후 이를 렌더링 하는 구조임을 생각하면 이해하기 쉬울 것이다. 

또 이러한 상태 관리가 useFunnel 내부에서 이루어지며 이를 통하여 상위 컴포넌트에서 컴포넌트의 흐름을 관리한다는 점도 생각하면 좋을 것 같다.  

 

 

https://www.slash.page/ko/libraries/react/use-funnel/README.i18n

 

useFunnel | Slash libraries

선언적이고 명시적으로 퍼널 스텝을 관리하고, 퍼널에 대한 상태의 흐름을 명확하게 파악할 수 있도록 만든 퍼널 컨트롤러입니다.

www.slash.page

 

 

 

 

728x90