토스의 퍼널 패턴
퍼널이라는 개념은 깔때기 모양처럼 단계별로 진행되는 과정을 말함
이 개념을 토스와 같은 서비스의 회원가입 절차에 적용한 것이 패널 패턴임
이를 통해, 사용자 경험을 보다 직관적이고 효과적으로 만드는 것을 목표로 함
토스에서는 여러 단계의 폼을 하나씩 입력하게 하는 방식으로 회원가입을 진행함
토스에서 제안한 퍼널 패넡은 다음과 같은 방식으로 구현할 수 있음
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 |