퍼널: 쏟아지는 페이지 한 방에 관리하기 by Toss & 진유림님

추교현의 FE(1 + LI)·2023년 9월 15일
3
post-thumbnail

이 시리즈는 저만의 FE 컨벤션, 코드 스타일 등을 만들고자 Toss의 SLASH 영상과 기술 블로그 등을 참고하여 만들었습니다.

👋🏼 참고자료 소개

SLASH 23에서 진유림님께서 발표하신 "퍼널: 쏟아지는 페이지 한 방에 관리하기" 영상을 분석하였습니다.

짧은 후기

토스의 훌륭한 UI/UX 뒤에는 열심히 노력하고 계시는 FE 엔지니어분들이 계시다는 것을 확실히 알게 되었습니다. 항상 감사드립니다😊

토스의 매끄러운 UX처럼 발표 흐름과 내용 또한 물 흐르듯 매끄러워 이해가 잘 되었습니다.

다시 한번 더 이런 양질의 영상을 만들어주시고 공유해 주신 Toss와 유림님께 감사하다는 말씀드리며 영상 내용에 대한 정리를 시작하겠습니다.

+) 살펴볼 코드가 조금 많습니다! 천천히 이해하면서 읽어주시면 더 유익한 글이 될 것 같습니다🙇🏻‍♂️


📌 내용 정리

[0] 대표적인 FE 패턴

  1. 상점

    Ex. 상점, 블로그, 뉴스, 투두리스트 등

  2. 단일 페이지 앱

    Ex. 채팅, 지도 등

  3. 설문조사

    Ex. 회원가입, MBTI 검사 등

⇒ 여러 페이지들을 통해 상태를 수집하고, 결과 페이지를 보여주는 형태이고 이 패턴을 “퍼널” 라고 부릅니다.

[기존 설계]

디자인 요구사항을 보고 보통 아래처럼 설계할 것입니다.

[아쉬운 점]

  1. 페이지 흐름이 흩어져 있다는 것
  2. 한 가지 목적을 위한 상태가 흩어져 있다는 것

⇒ 하나씩 해결해 봅시다!

[1] 응집도 개선하기

흩어진 페이지 흐름, 상태 헤쳐모여!

  1. RegisterData 지역 상태 만들기

    const [resgisterData, setRegisterData] = useState()
  1. step 지역 상태 만들기

    const [resgisterData, setRegisterData] = useState()
    const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공"|>("가입방식")
  2. 한 흐름으로 관리해야 하는 UI들을 컴포넌트로 넣기

    const [resgisterData, setRegisterData] = useState()
    const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공"|>("가입방식")
    
    return (
    	<main>
    		<가입방식 />
    		<주민번호 />
    		<집주소 />
    		<가입성공Step />
    	</main>
    )
  3. step에 따라 조건부 렌더링

    const [resgisterData, setRegisterData] = useState()
    const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공"|>("가입방식")
    
    return (
    	<main>
    		{step === "가입방식" && <가입방식 onNext={(data) => setStep("주민번호")} />}
    		{step === "주민번호" && <주민번호 onNext={() => setStep("집주소")} />}
    		{step === "집주소" && <집주소 onNext={async () => setStep("가입성공")} />}
    		{step === "가입성공" && <가입성공Step />}
    	</main>
    )
  4. API 호출에 필요한 상태 한눈에 관리

    const [resgisterData, 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 === "가입성공" && <가입성공Step />}
    	</main>
    )

이제는 1) 어떤 상태가 어떤 UI에서 수집이 되는지 한눈에 볼 수 있고, 2) 디자인 스펙이 변경되어도 유연하게 대응할 수 있게 되었습니다.

이 코드를 다른 퍼널에서도 재사용하고 싶다면?
"라이브러리로 추상화"를 해봅시다!

[2] 라이브러리로 추상화하기

고심해서 만든 해결책을 다른 사람들도 쓸 수 있게끔 공통 로직만 싹 발라내는 설레는 작업!

  1. step에 관련된 로직 묶어내기

    const [resgisterData, setRegisterData] = useState()
    const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공"|>("가입방식")
    
    return (
    	<main>
    		{step === "가입방식" && <가입방식 onNext={(data) => {
    			setRegisterData(prev => ({ ...prev, 가입방식: data }))
    			setStep("주민번호")
    		}} />}
    		{step === "주민번호" && <주민번호 onNext={() => setStep("집주소")} />}
    		// ...
    	</main>
    )
  2. 조건부 렌더링 추상화

    function Show({ if, children }) {
    	if (if === true) {
    		return children
    	}
    	return null
    }
  3. 추상화한 컴포넌트로 묶어내기

    const [resgisterData, setRegisterData] = useState()
    const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공"|>("가입방식")
    
    return (
    	<main>
    		<Step if={step === "가입방식"}>
    			<가입방식 onNext={(data) => {
    			setRegisterData(prev => ({ ...prev, 가입방식: data }))
    			setStep("주민번호")
    		}} />
    		</Step>
    		<Step if={step === "가입방식"}>
    			<주민번호 onNext={() => setStep("집주소")} />
    		</Step>
    		// ...
    	</main>
    )
  4. 반복되는 조건문 추상화

    const [resgisterData, setRegisterData] = useState()
    const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공"|>("가입방식")
    
    return (
    	<main>
    		<Step name="가입방식">
    			<가입방식 onNext={(data) => {
    			setRegisterData(prev => ({ ...prev, 가입방식: data }))
    			setStep("주민번호")
    		}} />
    		</Step>
    		<Step name="주민번호">
    			<주민번호 onNext={() => setStep("집주소")} />
    		</Step>
    		// ...
    	</main>
    )
  5. useFunnel 훅 생성

    function useFunnel() {
    	const [step, setStep] = useState()
    
    	const Step = (props) => {
    		return <>{props.children}</>
    	}
    
    	const Funnel = ({children}) => {
    		// name이 현재 step 상태와 동일한 Step만 렌더링
    		const targetStep = children.find(childStep => childStep.props.name === step);
    		return Object.assign(targetStep, { Step })
    	}
    
    	return [Funnel, setStep]
    }
  6. useFunnel 훅을 원본 코드에 적용

    이 훅을 통해 여러 페이지를 세트로 렌더링 해야 되는 다양한 상황에서 일관적으로 개발할 수 있게 됩니다.

    const [resgisterData, setRegisterData] = useState()
    const [step, setStep] = useFunnel<"가입방식"|"주민번호"|"집주소"|"가입성공"|>("가입방식")
    
    return (
    	<Funnel>
    		<Funnel.Step name="가입방식">
    			<가입방식 onNext={() => setStep("주민번호")} />
    		</Funnel.Step>
    		<Funnel.Step name="주민번호">
    			<주민번호 onNext={() => setStep("집주소")} />
    		</Funnel.Step>
    		// ...
    	</Funnel>
    )
  7. useFunnel의 히스토리 관리 기능

    단일 URL 이면, step 사이에 뒤로가기, 앞으로가기 지원이 안 되는 불편함이 있습니다.
    ⇒ router의 shallow push API를 사용해 쿼리파라미터를 업데이트해 줘서 가능하도록 구현할 수 있습니다.

    function useFunnel() {
    	const [step, setStep] = useState()
    
    	const Step = (props) => {
    		return <>{props.children}</>
    	}
    
    	const Funnel = ({children}) => {
    		// name이 현재 step 상태와 동일한 Step만 렌더링
    		const targetStep = children.find(childStep => childStep.props.name === step);
    		return Object.assign(targetStep, { Step })
    	}
    
    	return [Funnel, setStep]
    }

🚀 실전에 써먹을 것

  • 이러한 ‘퍼널’ 형태의 패턴이 실제로 필요하다면?
  • 전역 상태를 활용하는 것이 최선이 아닐 수도 있다!
    • 나는 Recoil의 atom을 통해 전역적으로 상태 관리하는 것이 편하다고 느낀다.
    • 그런데 이게 꼭 최선이 아닐 수 있다는 것을 배웠다.
  • 현재의 숫자가 큰 숫자로 바뀌었을 때를 항상 고려해서 코드를 짜라.
    • 무슨 말이냐면 : 현재 구현할 페이지가 적다고 느껴질 때, 보통 비효율적인 부분이 보여도 ‘에이 고작 3페이지니까 이 정도 불편해도 괜찮아!’라고 종종 나는 생각했다.
    • 그런데 만약 3페이지가 아니라 100페이지라면?!
      • 그래도 괜찮으면, 그대로 코드를 작성하고
      • 말도 안 되게 디버깅 난이도가 올라간다면, 결국 나중에 리팩토링해야 될 코드인 것이다.

⭐️ 이전 시리즈 추천

제가 2주 전에 작성한 실무에서 바로 쓰는 FE 클린코드 by Toss & 진유림님도 보시면, FE에 대한 더욱 클린한 코드와 클린한 설계에 대한 감이 잡히실 것 같습니다.

감사합니다🙇🏻‍♂️

profile
프론트엔드 재밌네? | 유쾌하게 & 치열하게

0개의 댓글