최근 SRP와 DI를 비롯한 객체 지향 원칙들을 프로젝트에 적용하며 리팩토링을 진행하고 있었습니다.
그런데 컴포넌트를 기능별로 나누다보니 원래 한 컴포넌트에서 모든 기능이 모두 모여있던 것과 달리 props drilling 현상이 발생했습니다.
3, 4개 정도까지는 괜찮다고 생각하는 편입니다.
Props drilling
그래도 리팩토링 내내 맘에 걸렸고 useQuery를 통해 가져오는 Server State라 기능 별로 나눈 컴포넌트 모두에서
useQuery를 사용할 수는 없으니 그보다 높은 부모 컴포넌트에서 props를 내려주는 props drilling 방식을 선택하였습니다.
export default function MemberSelect({ maxCapacity }: { maxCapacity: number }) {
const memberValue = useRecoilValue(MemberValue)
const {
data: memberList,
refetch,
isLoading,
} = useQuery<UserItemType[], AxiosError>({
queryKey: userQueryKeys.searchStudent(),
queryFn: () => get(userUrl.searchStudent(memberValue)),
})
return (
<S.MemberSelectContainer>
<ModalTitle />
<MemberInput refetch={refetch} maxCapacity={maxCapacity} />
<ShowMemberList />
<MemberList
memberList={memberList}
isLoading={isLoading}
maxCapacity={maxCapacity}
/>
<ButtonContainer />
</S.MemberSelectContainer>
)
}
props로 받은 maxCapacity는 벌써 2차례의 drilling을 거쳤습니다.
그리고 또 drilling이 일어나는 것은 props가 어디서 왔는지 나중에 추적이 힘들어질 수도 있습니다.
프로젝트에서는 전역 상태 관리 라이브러리인 Recoil을 사용했기에
이 Server State를 전역 상태로 저장했습니다.
Recoil
export default function MemberSelect({ maxCapacity }: { maxCapacity: number }) {
const memberValue = useRecoilValue(MemberValue)
const {
data: memberList,
refetch,
isLoading,
} = useQuery<UserItemType[], AxiosError>({
queryKey: userQueryKeys.searchStudent(),
queryFn: () => get(userUrl.searchStudent(memberValue)),
})
const setGlobalMaxCapacity = useSetRecoilState(GlobalMaxCapacity)
useEffect(() => {
setGlobalMaxCapacity(maxCapacity)
}, [])
return (
<S.MemberSelectContainer>
<ModalTitle />
<MemberInput refetch={refetch} />
<ShowMemberList />
<MemberList memberList={memberList} isLoading={isLoading} />
<ButtonContainer />
</S.MemberSelectContainer>
)
}
이렇게 props drilling은 여기서 그치게 되었습니다.
하지만 전역 상태 관리 라이브러리는 말 그대로 관리에 목적을 두고 있습니다.
단순한 저장이 아니고 말이죠.
maxCapacity는 자식 컴포넌트들에서 값으로만 사용되고 관리(변경)되지는 않습니다.
처음 props로 받은 maxCapacity를 set하는 것 빼고는 setRecoilState를 사용하지 않습니다.
Client state와 Server state를 각각 관리하는 것이 React Query의 목적 중 하나이기도 합니다.
그래서 단순 저장에 목적이 있다면 평소 개념만 알고 실제로 적용은 해보지 않은 Context API를 사용해보는 것은 어떨까라는 생각이 들었습니다!
Context API
export default function MemberSelect({ maxCapacity }: { maxCapacity: number }) {
const memberValue = useRecoilValue(MemberValue)
const {
data: memberList,
refetch,
isLoading,
} = useQuery<UserItemType[], AxiosError>({
queryKey: userQueryKeys.searchStudent(),
queryFn: () => get(userUrl.searchStudent(memberValue)),
})
return (
<MaxCapacityContext.Provider value={maxCapacity}>
<S.MemberSelectContainer>
<ModalTitle />
<MemberInput refetch={refetch} />
<ShowMemberList />
<MemberList memberList={memberList} isLoading={isLoading} />
<ButtonContainer />
</S.MemberSelectContainer>
</MaxCapacityContext.Provider>
)
}
이렇게 되면 전역 상태는 사라지고 maxCapacity를 저장하는 context 하나가 추가됩니다.
context는 자신을 감싸는 가장 가까운 Provider를 찾아가기에 실질적으로 maxCapacity를
사용하는 컴포넌트들을 Provider로 감쌌습니다.
props drilling도 이 단계에서 그치며 자식 컴포넌트가 얼마든지 생겨도 useContext hooks를 통해서
Provider value 값을 가져올 수 있습니다.
🚨 주의
Context API는 저장만 되고 변경이 안 된다는 뜻이 아닙니다.
Provider에서 value로 넘겨줄 때 setState를 만들어서 변경이 가능하도록 할 수 있습니다.
다만, 이 경우에는 성능에 신경을 써야해서 관리는 전역 상태 관리 라이브러리를 사용하는 게 좋습니다!
'React' 카테고리의 다른 글
🌲 React도 생명이에요 (?) - React의 생명주기 (0) | 2024.05.12 |
---|---|
🗒️ react-hook-form이 얼마나 편하길래.. (0) | 2024.02.08 |
React? 🖐️ (2) | 2024.01.28 |