SLASH 23 - 퍼널: 쏟아지는 페이지 한 방에 관리하기

date
Jul 2, 2023
slug
slash23-funnel
author
status
Public
tags
Frontend
summary
퍼널 페이지 관리 with Step, history, state
type
Post
thumbnail
스크린샷 2023-07-02 오후 10.42.09.png
updatedAt
Jul 2, 2023 01:42 PM
category

퍼널: 쏟아지는 페이지 한 방에 관리하기

 

주요 프론트엔드 패턴

  • 상점, 블로그, 뉴스, 투두 등
    • 목록 페이지, 상세 페이지
  • 채팅, 지도
    • 단일 페이지
  • 설문조사 (퍼널)
    • 여러 페이지 → 결과페이지
 
 

퍼널 FE개발하기

  • 개발 / 개선 with 3키워드
    • 응집도, 추상화, 시각화

개발 요구사항

notion image
 

기존의 방식

notion image
  • 유지보수 적 아쉬움
    • 페이지 흐름이 흩어져 있음
      • 가입방식 → 주민번호 → 집주소 파일을 넘나 들면서, 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를 통해서 흐름을 시각화하여 파악할 수 있음
 

useFunnel @Toss/slash

 
 
 
 
chat
Chat으로 물어보세요!
chat-bubble
채팅으로 물어보세요
안녕하세요!
프론트엔드 개발자, 봉승우입니다.
저에 대해 궁금하신 것이 있나요?
너는 어떤 사람이야?
너가 진행한 프로젝트를 간략히 소개해줘
너의 학업은 어때?
가장 어려웠던 프로젝트는 뭐였어?
어떤 특허를 가지고 있어?