티스토리 뷰

API 호출에 필요한 상태, 한 눈에 관리하기

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>
)

 

위의 코드를 "Step"이 추가되더라도 유연하게 관리할 수 있도록 수정이 필요하다.

아래는 "회사주소"라는 Step이 추가된 예시이다.

const [registerData, setRegisterData] = useState()
const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공">("가입방식")

return (
	<main>
    	{step === "가입방식" && <가입방식 onNext={(data) => {
        	setRegisterData(prev => ({ ...prev, 가입방식: data }))
            setStep("주민번호")
        }} />}
        {step === "주민번호" && <주민번호 onNext={() => setStep("집주소")} />}
        {step === "집주소" && <집주소 onNext={async () => setStep("회사주소")} />}
        {step === "회사주소" && <회사주소 onNext={async () => {
        	await fetch("/api/register", { data }) // API 호출 장소변경
            setStep("가입성공")
        }} />}
        {step === "가입성공" && <가입성공 />}
    </main>
)

 

그렇다면 어떻게 해야할까?

바로 "Step"에 관련된 로직을 묶어내는 방식을 생각해 볼 수 있을 것이다.

const [registerData, setRegisterData] = useState()
const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공">("가입방식")

return (
	<main>
    	{step === "가입방식" && <가입방식 onNext={(data) => {
        	setRegisterData(prev => ({ ...prev, 가입방식: data }))
            setStep("주민번호")
        }} />}
        {step === "주민번호" && <주민번호 onNext={() => setStep("집주소")} />}
        ...
    </main>
)

 

우선, 우리는 "조건부 렌더링"을 추상화할 필요가 있다.

<Step if={step === "가입방식"}>
	<가입방식 onNext={() => setStep("주민번호")} />
</Step>

// 구현
function Show({ if, children }) {
	if (if === true) {
    	return children
    }
    
    return null
}

 

그리고, 추상화한 컴포넌트로 묶어내야 한다.

const [registerData, setRegisterData] = useState()
const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공">("가입방식")

return (
	<main>
    	<Step if={step ==="가입방식"}>
        	<가입방식 onNext={() => setStep("주민번호")} />
        </Step>
        <Step if={step ==="주민번호"}>
        	<주민번호 onNext={() => setStep("집주소")} />
        </Step>
        ...
    </main>
)

 

그러나, 코드를 잘 보게 되면 코드가 반복되는 것을 볼 수 있다.

반복되는 조건문을 추상화할 필요가 있다는 것이다.

const [registerData, setRegisterData] = useState()
const [step, setStep] = useState<"가입방식"|"주민번호"|"집주소"|"가입성공">("가입방식")

return (
	<main>
    	<Step name="가입방식">
        	<가입방식 onNext={() => setStep("주민번호")} />
        </Step>
        <Step name="주민번호">
        	<주민번호 onNext={() => setStep("집주소")} />
        </Step>
        ...
    </main>
)

 

이를 해결하기 위해서,

"Step"들을 받고 반복문을 돌리고, 커스텀 훅을 생성해 처리할 수 있을 것이다.

커스텀 훅인 "useFunnel" Hook을 생성해볼 수 있겠다.

function getGraph(step: string, children: ReactElement[]) {
	const result: string[] = ['graph TD']
    
    children.map(stepElement => {
    	const stepName = stepElement.props.name
        const children = stepElement.props.children
        
        const childrenProps = children?.props as [string, unknown]
        Object.entries(childrenProps).map(el => {
        	const [functionName, value] = el
            if (typeof value === 'function') {
            	value.toString().match(/setStep\(.+\)/g)
                	?.map(matchedSetStep => {
                    	const matchedStepName = matchedSetStep.match(/setStep\((?:'|")(.+)(?:'|")\)/)?.[1]
                        const graphNode = `${stepName}[${stepName}] -->|${functionName}| ${matchedStepName}[${matchedStepName}]`
                        result.push(graphNode)
                    })
            }
        })
    })
    
    return result.join('\n')
}
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" Hook을 기존 코드에 적용시키면,

const [registerData, setRegisterData] = useState()
const [step, setStep] = useFunnel<"가입방식"|"주민번호"|"집주소"|"가입성공">("가입방식")

return (
	<Funnel>
    	<Funnel.step name="가입방식">
        	<가입방식 onNext={() => setStep("주민번호")} />
        </Funnel.step>
        <Funnel.step name="주민번호">
        	<주민번호 onNext={() => setStep("집주소")} />
        </Funnel.step>
        ...
    </Funnel>
)

 

여기에 히스토리 관리 기능을 추가해야 하지 않을까?

"name"에 맞는 데이터가 나타나야 할테니 말이다.

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]
}

 

그러나, 위의 방법도 불편한 점이 있을 것이다.

첫 번째로, 1초 만에 파악하기 힘든 페이지 흐름이다. "step"이 많아질수록 코드가 길어질 것이고, 그래서 한 번에 읽기 어려움이 발생할 것이다.

<Funnel>
    	<Funnel.step name="가입방식">
        	<가입방식
            	on신규가입클릭={() => {
                	updateState({ 가입방식: "신규가입" })
                    setStep("주민번호")
                }}
                on번호이동클릭={() => {
                	updateState({ 가입방식: "번호이동" })
                    setStep("주민번호")
                }}
            />
        </Funnel.step>
        <Funnel.step name="주민번호">
        	<주민번호 
            	on다음클릭={() => {
                	if (emptyHomeAdress) {
                    	setStep("집주소")
                    } else {
                    	setStep("회사주소")
                    }
                }}
            />
        </Funnel.step>
        ...
    </Funnel>