퍼널 패턴 정리

토스의 퍼널 패턴

 

퍼널이라는 개념은 깔때기 모양처럼 단계별로 진행되는 과정을 말함

이 개념을 토스와 같은 서비스의 회원가입 절차에 적용한 것이 패널 패턴임

이를 통해, 사용자 경험을 보다 직관적이고 효과적으로 만드는 것을 목표로 함

 

토스에서는 여러 단계의 폼을 하나씩 입력하게 하는 방식으로 회원가입을 진행함

 

토스에서 제안한 퍼널 패넡은 다음과 같은 방식으로 구현할 수 있음

1. 단계별 UI 렌더링 

-> 각 단계는 별도의 컴포넌트로 만들어짐

-> 현재 단계에 따라 해당 컴포넌트를 조건부로 렌더링함

-> 상태 관리에서는 useState 훅을 사용해 현재 단계와 사용자 데이터 관리

 

2. 조건부 렌더링 및 상태 업데이트

ex. 첫 번째 단계인 가입 방식에서 선택이 완료되면 다음 단계인 주민버호 입력 화면으로 전환되고, 이와 동시에 상태가 업데이트 됨

-> 마지막 단계에서는 모든 데이터를 모아 api 호출 진행

 

3. 코드 응집도 및 추상화

-> 기존 방식은 여러 파일에 상태와 ui가 분산되어 있어 관리가 어려운 반면, 퍼널 패턴을 사용하면 한 파일 내에서 모든 단계를 관리할 수 있어 유지 보수성 향상됨

 


기존 방식

- 전역 상태로 분산 관리

- 단계별로 분리된 파일로 관리

- 파일, 상태가 분산되어 있어 코드 이해가 복잡

- 각 단계가 독립적이어서 중복 코드가 많을 수 있음

- 상태와 ui가 분리되어 있어 수정이 어려움

 

 

코드 구조와 흐름

1. 각 단계별 컴포넌트

function SignupStep1({ onNext }) {
  const handleNext = () => {
    // 사용자 데이터 수집
    onNext({ plan: "Basic Plan" });
  };
  return <button onClick={handleNext}>다음</button>;
}

코드에는 각 signupStep1, 2, 3이라는 3개의 컴포넌트가 있음

이 컴포넌트들은 각각 회원가입 과정의 하나의 단계를 담당함

 

첫번째 단계는 사용자가 요금제를 선택하게 함

버튼을 클릭하면 onNext 함수를 호출하며, 선택한 요금제 데이터를 부모 컴포넌트로 전달함

function SignupStep2({ onNext }) {
  const handleNext = () => {
    // 사용자 데이터 수집
    onNext({ personalInfo: "123456-7890123" });
  };
  return <button onClick={handleNext}>다음</button>;
}

두번째 단계는 사용자가 개인 정보를 입력하게 됨

버튼을 클릭하면 onNext 함수를 호출하며,

입력한 개인 정보를 부모 컴포넌트로 전달함

function SignupStep3({ onNext }) {
  const handleNext = () => {
    // API 호출
    onNext();
  };
  return <button onClick={handleNext}>가입 완료</button>;
}

세번째 단계는 모든 데이터를 종합해 최종적으로 가입을 완료함

버튼을 클릭하면 onNext 함수를 호출하며

최종 데이터를 처리하도록 부모 컴포넌트로 알려줌

function SignupFlow() {
  const [step, setStep] = useState(1); // 현재 단계 관리
  const [formData, setFormData] = useState({}); // 모든 단계에서 수집한 데이터를 저장

  const handleNextStep = (data) => {
    setFormData((prevData) => ({ ...prevData, ...data })); // 새로운 데이터 추가
    setStep(step + 1); // 다음 단계로 이동
  };

  return (
    <div>
      {step === 1 && <SignupStep1 onNext={handleNextStep} />}
      {step === 2 && <SignupStep2 onNext={handleNextStep} />}
      {step === 3 && <SignupStep3 onNext={() => console.log(formData)} />}
    </div>
  );
}

이 메인 컴포넌트는 전체 회원가입 플로우를 관리하는 메인 컴포넌트임

useState 1은 현재 플로우가 어느 단계에 있는지를 관리함

useState({})의 formData 상태는 각 단계에서 수집한 사용자 데이터를 저장하는 데 사용함

handleNextStep 함수는 각 단계에서 onNext 함수로 전달되며, 각 단계가 완료되면 호출됨

그리고 전달된 데이터를 formData 상태에 추가로 저장함

이후 step을 1 증가시켜 다음 단계로 전환함

 

각 단계에서 onNext 함수가 호출되면 handleNextStep 함수가 실행되어 step 상태가 업데이트 됨

이로 인해 다음 단계의 컴포넌트가 조건부 렌더링 되어 화면에 표시됨

 

그럼 handleNextStep 함수가 각 단계의 데이터를 formData 상태에 누적하여 저장하고

최종적으로 formData 상태에는 모든 단계의 데이터가 포함됨

 

마지막 단게인 signUpStep3에서는 모든 데이터를 수집한 후 onNext가 호출되면 console.log(formData)를 통해 모든 데이터를 출력함. 실제 환경에서는 이 데이터를  api를 통해 서버로 전송 가능


퍼널 방식

function useFunnel(initialStep) {
  const [step, setStep] = useState(initialStep); // 현재 단계를 관리
  const [formData, setFormData] = useState({}); // 각 단계에서 수집된 데이터를 저장

  const nextStep = (data) => {
    setFormData((prevData) => ({ ...prevData, ...data })); // 새로운 데이터를 기존 데이터에 병합하여 저장
    setStep(step + 1); // 다음 단계로 이동
  };

  return {
    step,        // 현재 단계
    formData,    // 수집된 데이터
    nextStep,    // 다음 단계로 이동하는 함수
  };
}

먼저 커스텀 훅을 사용함

회원가입 플로우의 단계를 관리하고 데이터 저장

 

function SignupFlow() {
  const { step, formData, nextStep } = useFunnel(1); // 초기 단계는 1로 설정

  return (
    <div>
      {step === 1 && (
        <SignupStep1 onNext={(data) => nextStep(data)} /> // 1단계: onNext 호출 시 nextStep 실행
      )}
      {step === 2 && (
        <SignupStep2 onNext={(data) => nextStep(data)} /> // 2단계: onNext 호출 시 nextStep 실행
      )}
      {step === 3 && (
        <SignupStep3 onNext={() => {
          console.log(formData); // 모든 데이터 출력
          // API 호출 (예시)
        }} />
      )}
    </div>
  );
}

이 컴포넌트는 useFunnel을 사용해 단계별로 ui를 렌더링하고, 사용자 데이터를 수집하는 메인 컴포넌트임

현재 step 값에 따라 signupStep 1, 2, 3 중에 하나를 조건부 렌더링함. 그리고 각 단계의 onNext 이벤트에서 nextStep 함수를 호출해 다음 단계로 이동하며, 데이터를 전달함

 

function SignupStep1({ onNext }) {
  const handleNext = () => {
    onNext({ plan: "Basic Plan" }); // 사용자가 선택한 데이터를 onNext에 전달
  };
  return <button onClick={handleNext}>다음</button>;
}

function SignupStep2({ onNext }) {
  const handleNext = () => {
    onNext({ personalInfo: "123456-7890123" }); // 사용자가 입력한 데이터를 onNext에 전달
  };
  return <button onClick={handleNext}>다음</button>;
}

function SignupStep3({ onNext }) {
  return <button onClick={onNext}>가입 완료</button>; // 최종 단계에서 onNext 호출
}

각 단계별 컴포넌트는 사용자 데이터를 수집하고 다음 단계로 이동하는 버튼 제공

 


정리

 

1. 기존

const [registerData, setRegisterData] = useState();
const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공">("가입방식");

return (
  <main>
    {step === "가입방식" && <가입방식 onNext={(data) => setStep("주민번호")} />}
    {step === "주민번호" && <주민번호 onNext={() => setStep("집주소")} />}
    {step === "집주소" && <집주소 onNext={async () => setStep("가입성공")} />}
    {step === "가입성공" && <가입성공Step />}
  </main>
)

각 단계에서 onNext 함수가 호출될 때마다 단순히 setStep을 호출해 다음 단계로 이동함

registerData를 저장하거나 업데이트하는 로직이 없음

= 각 단계에서 수집된 데이터를 따로 저장하지 않고 다음 단계로 넘어감

코드 전체에서 api 호출을 하는 부분이 없음

 

2. 보완된 방식

const [registerData, setRegisterData] = useState();
const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공">("가입방식");

return (
  <main>
    {step === "가입방식" && <가입방식 onNext={(data) => {
      setRegisterData(prev => ({ ...prev, 가입방식: data }));
      setStep("주민번호");
    }} />}
    {step === "주민번호" && <주민번호 onNext={() => setStep("집주소")} />}
    {step === "집주소" && <집주소 onNext={async () => {
      await fetch("/api/register", { data: registerData });
      setStep("가입성공");
    }} />}
    {step === "가입성공" && <가입성공Step />}
  </main>
)

각 단계에서 setRegisterData를 사용해 사용자가 입력한 데이터를 registerData 상태에 저장

registerData에 저장된 데이터를 /api/register 엔드 포인트로 전송하는 api 호출 수행

 

 

 

참고

'TIL' 카테고리의 다른 글

24. 08. 08 TIL  (0) 2024.08.08
24. 08. 07 TIL  (0) 2024.08.07
CORS 정리  (0) 2024.08.05
24. 08. 01 TIL  (0) 2024.08.01
SSG, ISR, SSR, CSR 정리  (0) 2024.07.31