이 시리즈는 저만의 FE 컨벤션, 코드 스타일 등을 만들고자 Toss의 SLASH 영상과 기술 블로그 등을 참고하여 만들었습니다.
SLASH 23에서 진유림님께서 발표하신 "퍼널: 쏟아지는 페이지 한 방에 관리하기" 영상을 분석하였습니다.
토스의 훌륭한 UI/UX 뒤에는 열심히 노력하고 계시는 FE 엔지니어분들이 계시다는 것을 확실히 알게 되었습니다. 항상 감사드립니다😊
토스의 매끄러운 UX처럼 발표 흐름과 내용 또한 물 흐르듯 매끄러워 이해가 잘 되었습니다.
다시 한번 더 이런 양질의 영상을 만들어주시고 공유해 주신 Toss와 유림님께 감사하다는 말씀드리며 영상 내용에 대한 정리를 시작하겠습니다.
+) 살펴볼 코드가 조금 많습니다! 천천히 이해하면서 읽어주시면 더 유익한 글이 될 것 같습니다🙇🏻♂️
상점
Ex. 상점, 블로그, 뉴스, 투두리스트 등
단일 페이지 앱
Ex. 채팅, 지도 등
설문조사
Ex. 회원가입, MBTI 검사 등
⇒ 여러 페이지들을 통해 상태를 수집하고, 결과 페이지를 보여주는 형태이고 이 패턴을 “퍼널” 라고 부릅니다.
[기존 설계]
디자인 요구사항을 보고 보통 아래처럼 설계할 것입니다.
[아쉬운 점]
⇒ 하나씩 해결해 봅시다!
흩어진 페이지 흐름, 상태 헤쳐모여!
RegisterData
지역 상태 만들기
const [resgisterData, setRegisterData] = useState()
step
지역 상태 만들기
const [resgisterData, setRegisterData] = useState()
const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공"|>("가입방식")
한 흐름으로 관리해야 하는 UI들을 컴포넌트로 넣기
const [resgisterData, setRegisterData] = useState()
const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공"|>("가입방식")
return (
<main>
<가입방식 />
<주민번호 />
<집주소 />
<가입성공Step />
</main>
)
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>
)
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) 디자인 스펙이 변경되어도 유연하게 대응할 수 있게 되었습니다.
이 코드를 다른 퍼널에서도 재사용하고 싶다면?
"라이브러리로 추상화"를 해봅시다!
고심해서 만든 해결책을 다른 사람들도 쓸 수 있게끔 공통 로직만 싹 발라내는 설레는 작업!
step에 관련된 로직 묶어내기
const [resgisterData, setRegisterData] = useState()
const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공"|>("가입방식")
return (
<main>
{step === "가입방식" && <가입방식 onNext={(data) => {
setRegisterData(prev => ({ ...prev, 가입방식: data }))
setStep("주민번호")
}} />}
{step === "주민번호" && <주민번호 onNext={() => setStep("집주소")} />}
// ...
</main>
)
조건부 렌더링 추상화
function Show({ if, children }) {
if (if === true) {
return children
}
return null
}
추상화한 컴포넌트로 묶어내기
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>
)
반복되는 조건문 추상화
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>
)
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]
}
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>
)
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]
}
제가 2주 전에 작성한 실무에서 바로 쓰는 FE 클린코드 by Toss & 진유림님도 보시면, FE에 대한 더욱 클린한 코드와 클린한 설계에 대한 감이 잡히실 것 같습니다.
감사합니다🙇🏻♂️