
안녕하세요. 프론트개발자 진돌입니다.
오늘은 컴포넌트를 return하는 훅 패턴을 공유해보려고 합니다.
보통 hook에서는 그냥 비즈니스 로직을 담아서 상태를 return을 해주는 식으로 많이 사용합니다. (useForm, useQuery 등.. )
컴포넌트를 return하는 패턴은 말그대로 컴포넌트도 함께 return 해주는 hook 입니다.
const {Modal, handleModal} = useModal();
비즈니스 로직을 숨기는 용도로 사용하거나, 비스니스 로직과 상당히 밀접한 관련이 있는 컴포넌트를 묶는 용도로 사용할 수 있습니다.
제가 겪은 사례와 toss, kakao에서 사용하고 있는 코드를 통해 소개해드리도록 하겠습니다.
https://github.com/toss/slash/tree/main/packages/react/use-funnel
일단 토스에서 사용하는 패턴을 소개해보겠습니다.
const [Funnel, setStep] = useFunnel(['enter name', 'enter social security number', 'done'] as const)
return (
<Funnel>
<Funnel.Step name="enter name">
<EnterNameStep /> // Render when ?funnel-step="enter name"
</Funnel.Step>
<Funnel.Step name="enter social security number">
<EnterSocialSecurityNumberStep /> Render when ?funnel-step="enter social security number"
</Funnel.Step>
<Funnel.Step name="done">
<DoneStep /> // Render when ?funnel-step="done"
</Funnel.Step>
</Funnel>
)
toss 라이브러리에서는 useFunnel이라는 hook이 있습니다.
useFunnel은 비즈니스로직과 완전히 밀접한 관련이 있는 컴포넌트로 보이네요.
컴포넌트와 비즈니스 로직이 만약 분리되어있다면,
누군가는 컴포넌트만 import를 해와서 해당 컴포넌트를 사용할 때마다 비즈니스 로직 hook이나 local상태로 또 만들어서 사용했을 것입니다.
그렇게 사용하지 못하도록 강제로 비즈니스 로직과 컴포넌트를 하나의 export로 제공하여 코드가 파편화되지 않도록 관리할 수 있도록 해주었네요.
https://fe-developers.kakaoent.com/2023/230330-frontend-solid/
카카오에서는 100% 공개되어있는 코드는 아니지만 카카오 기술 블로그 코드들을 보다보면 사용하는 경우를 옅볼 수 있었습니다.
function Fetcher({query, children}){
const { isLoading, error, data } = query();
if(isLoading) {
return <Loading />
}
if(error){
return <Error />
}
return children;
}
function TicketInfoContainer() {
//useFetcher 구현채는 제공되지 않음
const [{waitfreePeriod, waitfreeChargedDate, rentalTicketCount, ownTicketCount}, TicketInfoFetcher] = useFetcher(useTicketInfoQuery);
const [{keywordInfo}, KeywordInfoFetcher] = useFetcher(useKeywordInfoQuery);
const [{commentCount}, CommentInfoFetcher] = useFetcher(useCommentInfoQuery);
return (
<TicketInfoFetcher>
<TicketInfo>
<TicketInfo.WaitfreeArea waitfreePeriod={waitfreePeriod} waitfreeChargedDate={waitfreeChargedDate} />
<TicketInfo.TicketArea rentalTicketCount={rentalTicketCount} ownTicketCount={ownTicketCount} />
<KeywordInfoFetcher />
<CommentInfoFetcher>
<TicketInfo.CommentArea commentCount={commentCount} keywordInfo={keywordInfo} />
</CommentInfoFetcher>
</TicketInfo>
</TicketInfoFetcher>
)
}
카카오에서는 useFetcher 형태로 컴포넌트 리턴 패턴을 사용하고 있습니다.
useFetcher의 구현채는 확인해볼 수 없었지만 Fetcher 함수를 봤을 때 대략적으로 hook에서 하는 일을 추측해보면,
요런 일들을 하니까 아래 형태의 hook정도로 추측되네요.
function useFetcher({query}){
const queryResponse = query();
const fetcherComponent = React.useMemo(({children}:{children: ReactNode}) => {
if(queryResponse.isLoading) {
return <Loading />
}
if(queryResponse.isError){
return <Error />
}
return children;
}, [queryResponse.isLoading, queryResponse.isError])
return [{...queryResponse}, fetcherComponent ];
}
해당 내용의 장점은 링크를 남겨놓은 카카오 블로그에서 너무 많은 얘기를 하고 있어서 직접 확인하시는걸 추천드립니다!
아래는 실제 개발할 때 겪은 시나리오입니다.
🥸 (기획자): 게시 버튼을 눌렀을 때, 게시할 수 있을 경우 A모달을 게시할 수 없을 경우에는 B모달을 또 어떠한 경우에는 C모달을 띄워주세요.
const TestButton = React.memo(function TestButton() {
const [isOpenA, setIsOpenA] = React.useState(false)
const [isOpenB, setIsOpenB] = React.useState(false)
const [isOpenC, setIsOpenC] = React.useState(false)
const [isOpenD, setIsOpenD] = React.useState(false)
function handleModal(){
if(...){
setOpenA(true)
return
}
if(...){
setOpenB(true)
return
}
if(...){
setOpenC(true)
return
}
}
return <>
<button onClick={handleModal}>모달열기</button>
<ModalA openA={openA} setOpenA={setOpenA}/>
<ModalB openA={openB} setOpenA={setOpenB}/>
<ModalC openA={openC} setOpenA={setOpenC}/>
<ModalD openA={openD} setOpenA={setOpenD}/>
</>;
});
밖에서는 무슨 모달이든 한개를 노출하는 것이 목적인데, 조건이 많을 수록 불필요한 선언이 많아져서 복잡도가 높아 보입니다.
function useHandleModal() {
// 모달 state 선언 등
const handleModal = () => {
// 모달 state 핸들링
};
return {
Modal: (
<>
<ModalA openA={openA} setOpenA={setOpenA} />
<ModalB openA={openB} setOpenA={setOpenB} />
<ModalC openA={openC} setOpenA={setOpenC} />
<ModalD openA={openD} setOpenA={setOpenD} />
</>
),
handleModal,
};
}
const TestButton = React.memo(function TestButton() {
const {Modal, handleModal} = useHandleModal()
return <>
<button onClick={handleModal}>모달열기</button>
<Modal />
</>;
});
다음과 같은 패턴을 사용하였더니, 비즈니스 로직은 전부 hook에 넣고 밖에서는 선언된 컴포넌트와 핸들링 하는 함수가 있다만 보고 선언적인 코드를 작성할 수 있게 되었습니다.
많은 장점을 소개했지만 단점도 있습니다.
컴포넌트를 return하는 hook을 사용했을 경우 밀집도가 높은 코드를 작성할 수 있지만 반대로 UI 컴포넌트만 사용할 수 없게되어 재사용성을 낮추게 됩니다.
게다가 비즈니스 로직과 섞여버린 컴포넌트는 해당 비즈니스 로직외에는 재사용할 수 없다고 생각해야됩니다.
흔하게 많이 사용되는 패턴은 아니지만 유용해서 사용하고 있기에 공유하는 글을 작성해보았습니다.
컴포넌트를 return하는 패턴을 무분별하게 사용할 경우, 비즈니스로직과 UI컴포넌트간의 분리가 되지 않아서 악취 덩어리의 코드가 될 것입니다.
하지만 밀접한 연관성이 있는 로직간에는 아주 좋은 해결책이 될 수 있습니다.
읽어주셔서 감사합니다.
썸네일 이미지는 gpt 한테 요청해봤는데 오타를 자꾸만드네요 ^^...