SLASH 23 - 퍼널: 쏟아지는 페이지 한 방에 관리하기
퍼널: 쏟아지는 페이지 한 방에 관리하기
주요 프론트엔드 패턴
- 상점, 블로그, 뉴스, 투두 등
- 목록 페이지, 상세 페이지
- 채팅, 지도
- 단일 페이지
- 설문조사 (퍼널)
- 여러 페이지 → 결과페이지
퍼널 FE개발하기
- 개발 / 개선 with 3키워드
- 응집도, 추상화, 시각화
개발 요구사항

기존의 방식

- 유지보수 적 아쉬움
- 페이지 흐름이 흩어져 있음
- 가입방식 → 주민번호 → 집주소 파일을 넘나 들면서, router.push 를 따라가야함
- 한가지 목적을 위한 상태가 흩어져 있음
- 상태를 수집하는 곳과 사용하는 곳이 다름 (마지막 집주소 페이지에서 전역 상태 데이터를 fetching 함) → 이럴 경우, 만약 전역 상태에 변경이 생긴다면, 앱 전체를 대상으로 데이터 흐름을 추적해야함
- 위 문제를 해결하기 위해 ⇒ 응집도를 높이자
- 연관된 코드는 가까운 곳에 배치하자
상태와 흐름을 한 곳에서
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 }); // API 호출 장소 변경 setStep('가입성공'); }} /> )} {step === '가입성공' && <가입성공 />} </main> );
- 특징
- UI 세부 사항은 하위 컴포넌트에서 관리
- step의 이동은 상위에서 관리하여 UI 흐름을 한 곳에서 관리
- API 호출에 필요한 상태 한 눈에 관리 가능
- 파일을 넘나들며, 전역 상태를 관리하지 않아도 됨
- step이 변경되어도 유연하게 관리할 수 있음
라이브러리로 추상화하기
about 퍼널 흐름과 관련된 코드
- 해결책을 다른 사람들도 사용할 수 있도록 공통 로직을 발라내는 것
step에 관련된 로직 묶어내기
- 조건부 렌더링 하는 부분을 컴포넌트로 추상화해보자
- 조건과 컴포넌트를 받는 컴포넌트면 되겠음.
function Step({if, children}){ if(if === true) return children return null } // index.tsx - 1차 <Step if={step === "가입방식"}> <Example /> </Step> // index.tsx - 2차 <Step name="가입방식"> <Example /> </Step> <Step name="주민번호"> <Example /> </Step> //... // -> 이를 위해서는 Step내부에서 전체 Step과정을 알고 있어야 함
useFunnel 훅 생성
- step 상태관리도 훅에서.
function useFunnel(){ const [step, setStep] = useState() const Step = (props) => { return <>{props.children}</> } const Funnel = ({children}) => { // name이 현재 step 상태와 동일할 경우 렌더링 const targetStep = children.find(chilStep => chilStep.props.name === step) return Object.assign(targetStep, {Step}) } return [Funnel, setStep] }
- client에서는
const [registerData, setRegisterData] = useState(); const [Funnel, setStep] = useFunnel<'가입방식' | '주민번호' | '집주소' | '가입성공'>('가입방식'); return ( <Funnel> <Funnel.step name="가입방식"> <가입방식 onNext={data => setStep('주민번호')} /> </Funnel.step> <Funnel.step name="주민번호"> <주민번호 onNext={data => setStep('집주소')} /> </Funnel.step> // ... </Funnel> )
- 다양한 환경에서 사용가능
- 하지만, 히스토리 관리 기능 등
useFunnel의 히스토리 관리 기능
- 현재 코드는 단일 URL이라 스텝 사이에 뒤로가기, 앞으로가기 등 지원안됨
- 이를 router의 shallow push API를 사용하여 해결할 수 있음
function useFunnel(){ const step = useQueryParam("funnel-step") const setStep = (step: string) => { const nextUrl = `${QS.create({...prevQuery, "funnel-step": step})}` router.push(nextUrl, undefined, {shallow: true}) } const Step = (props) => { return <>{props.children}</> } const Funnel = ({children}) => { // name이 현재 step 상태와 동일할 경우 렌더링 const targetStep = children.find(chilStep => chilStep.props.name === step) return Object.assign(targetStep, {Step}) } return [Funnel, setStep] }
- 위 디테일한 기능 구현이 비즈니스 로직과 섞여있었다면 코드를 읽기는 어려웠을 것
여전히 불편한 점 - 1초 만에 파악하기 힘든 페이지 흐름
- funnel debugger를 통해서 흐름을 시각화하여 파악할 수 있음