디자이너가 피그마에 작업해 둔 수많은 화면을
프론트엔드에서 관리하기 위해 많은 분기와 상태가 생기게 됨
=> 쏟아지는 페이지들을 한번에 관리하기 위한 설계 방법 = 퍼널
=> 3번의 설문조사형 형태를 바로 '퍼널'이라고 한다.
총 4개의 페이지 가정
가장 정석적인 설계이고 동작에도 문제가 없지만 유지보수에 아쉬운 점이 있음
=> 이렇게 되면 나중에 API에 기능을 추가하거나 버그를 수정할 때 앱 전체를 대상으로 데이터 흐름을 추적해야 함 (여러 파일을 넘나들면서 디버깅 필요)
연관된 페이지를 가까운 곳에 배치하는 것
=> 흩어져 있는 페이지 흐름 및 상태를 한 곳으로 모으기
const [registerData, setRegisterData] = useState();
const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공">("가입방식");
return (
<main>
{step === "가입방식" && <가입방식 onNext={(data) => {
setRegisterData((prev) => ({ ...prev, 가입방식: data }));
setStep("주민번호");
} />}
{step === "주민번호" && <주민번호 onNext={(data) => {
setRegisterData((prev) => ({ ...prev, 주민번호: data }));
setStep("집주소");
} />}
{step === "집주소" && <집주소 onNext={(data) => {
await fetch("/api/register", { data }); // 3번째 페이지에서 API 호출
setStep("가입성공");
} />}
{step === "가입성공" && <가입성공 />}
</main>
);
=> 더 이상 파일을 넘나들면서 전역 상태를 관리할 필요 없게 됨
=> API 디자인 스펙 변경 시 유연하게 대응 가능
const [registerData, setRegisterData] = useState();
const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공">("가입방식");
return (
<main>
// ...
{step === "집주소" && <집주소 onNext={(data) => {
setRegisterData((prev) => ({ ...prev, 집주소: data }));
setStep("회사주소");
} />}
{step === "회사주소" && <회사주소 onNext={(data) => {
await fetch("/api/register", { data }); // 3번째 페이지에서 API 호출
setStep("가입성공");
} />}
{step === "가입성공" && <가입성공 />}
</main>
);
// 추상화
function Step({ if, children }) {
if (if === true) return children;
return null;
}
// 사용
return (
<Step if={step === '가입방식'}>
<가입방식 onNext={() => setStep("주민번호")} />
</Step>
<Step if={step === '주민번호'}>
<주민번호 onNext={() => setStep("집주소")} />
</Step>
);
return (
<Step name="가입방식">
<가입방식 onNext={() => setStep("주민번호")} />
</Step>
<Step name="주민번호">
<주민번호 onNext={() => setStep("집주소")} />
</Step>
);
=> 근데 이렇게 하려면 Step 컴포넌트가 현재 step을 알고 있어야 함
function useFunnel() {
const [step, setStep] = useState();
const Step = (props) => {
return <>{props.children}</>
};
const Funnel = ({ children }) => {
// Funnel 컴포넌트로 받은 name 프로퍼티와 step이 일치하는 컴포넌트를 찾아 반환
const targetStep = children.find((childStep) => childStep.props.name === step);
return Object.assign(targetStep, { Step });
}
return [Funnel, setStep];
}
const [registerData, setRegisterData] = useState();
const [step, setStep] = useFunnel<"가입방식"|"주민번호"|"집주소"|"가입성공">("가입방식");
return (
<main>
<Funnel.Step name='가입방식'>
<가입방식 onNext={() => setStep("주민번호")} />
</Funnel.Step>
<Funnel.Step name='주민번호'}>
<주민번호 onNext={() => setStep("집주소")} />
</Funnel.Step>
// ...
</main>
);
=> useFunnel 훅 호출만으로 여러 페이지를 셋트로 렌더링해야 하는 다양한 상황을 해결
function useFunnel() {
const step = useQueryParam("funnel-step");
const setStep = (step: string) => {
const nextUrl = `${QS.create({ ...prevQuery, "funnel-step": step })}`;
router.push(url, undefined, { shallow: true });
}
// ...
return [Funnel, setstep];
}
이러한 디테일한 기능 로직이 비즈니스 로직과 섞여 있으면 코드 가독성이 떨어질 수 있으니 꼭 분리해주자.
useFunnel을 Mermaid 라이브러리로 다이어그램 시각화한 것
개발자로서 배울 점이 많은 글이었습니다. 감사합니다.