기존 프로덕트는 React 내장 hook인 useState 를 사용하면서, Props drilling 을 막기 위해서 Context API 를 사용하고 있었어요.
관심 범위 내에서 ContextProvider 로 상태를 제공하면 상기한 이슈에는 문제가 없었습니다.
그러나 몇가지 이슈가 있어 대체 방법을 찾아야 했는데요,
Context 내부 값이 업데이트 되면 리렌더링을 일으켜요.
Provider 가 상위에 위치하기 때문에 불필요한 리렌더링으로 인한 성능 저하가 발생합니다.
개별 Provider 가 늘어남에 따라 Provider hell 이라고 불리는 Provider가 끝없이 내부를 감싸는 현상이 생겨요.
const App = () => {
return (
<>
<GlobalProvider value={globalValue}>
<OtherProvider value={otherValue}>
<OtherOtherProvider value={otherOtherValue}>
{/** ... */}
</OtherOtherProvider>
</OtherProvider>
</GlobalProvider>
</>
)
}
부모 → 자식 방향으로만 전달이 가능한 구조적 한계가 있어 ContextProvider 를 무조건 상위로 올려야하나, Provider 가 여럿일 때 우선순위가 꼬이는 현상이 있었어요.
상기한 이유로 상태관리 라이브러리를 새로 골라야했고,
의논 끝에 Jotai 를 채택하게 되었습니다.
최소 선택 조건은 아래와 같습니다.
이 조건을 만족하는 것이 Jotai 와 Zustand 였고,
둘 중 무엇을 선택할지 고민했었는데요.
제가 이해하기로 둘의 컨셉은 약간의 차이가 있었어요.
Zustand
Jotai
[참고1] https://stateofreact.com/en-US
[참고2] https://dev.to/nguyenhongphat0/react-state-management-in-2024-5e7l
각 Provider 를 나눠서 사용하는 상황에서는 Zustand 가 매력적이었지만,
상태의 단위를 작개 쪼개 사용하는 Jotai 가 추후 구조 변경이 있더라도 간편하게 유지보수가 가능해 보였어요.
atom 의 갯수가 많아지면 관리가 어렵다는 단점은 존재했지만, 파일 단위로 관심사를 분리해서 사용하면 추적이 어렵지 않을 것으로 보았습니다.
기존에 정의된 ContextProvider 내부의 구성은 다음과 같았어요.
export const GlobalContextProvider = ({ children }) => {
// Tanstack QueryState
const { data, isSuccess, ... } = useCustomQuery();
// useState
const [value, setValue] = useState(null);
...
// 재사용 function 및 handler
const getNormalizeData = () => {
...
};
const handleOnClick = () => {
...
};
return (
<GlobalContext.Provider
value={{
data,
...
}}
>
{childern}
</GlobalContext.Provider>
);
}
Context 내부에서 Tanstack Query(React Query)의 fetching 데이터를 조회해서 내려주고 있고, 불필요하게 내려주는 함수나 핸들러가 존재하고 있는 구조였습니다.
단일 컴포넌트에만 적용되는 경우도 존재했는데, 굳이 Context 에 넣으면서 단일 책임 원칙에 위배되는 상황이 생겼죠.
(처음에는 컴포넌트를 보기좋고 간결하게 만들기 위해 사용하기 시작했는데 말입니다... 🥲)
Tanstack Query hook들은 필요한 컴포넌트에서 직접 호출하는 쪽으로 변경했습니다. Tantack Query 는 Jotai와 별개로 비동기 서버 상태를 관리해주는 라이브러리인 만큼, 클라이언트 상태관리안에 종속되지 않게 하는게 우선이라고 판단했어요.
그리고 컴포넌트 책임 단위를 분명히 하기 위해 리팩터링 작업도 같이 진행했습니다.
이 작업들을 정리하고 나니 atom 을 추가하는 것은 빠르고 간단했습니다.
Context Provider 정리 : Context를 완전히 삭제하는 방향으로 잡지는 않았습니다. 예를 들어 상품 정보를 붙들고 있는 Provider 같은 경우, 개별 컴포넌트의 상위에 존재하며 다른 컴포넌트에 영향을 미치지 않기 때문에 삭제하는 것은 불필요하다고 판단했습니다.
성능 향상 : state를 atom 단위로 잘라 필요한 경우 사용하는 방식으로 변경, 이와 동시에 누락된 메모이제이션을 함께 수행하여 불필요한 리렌더링을 방지했으며, 약 20% 정도 감소시킨 것으로 측정했습니다.
가독성 향상 : 각 컴포넌트의 책임을 명확하게 하고, Props Drilling 을 제거했습니다.

약 6,000줄의 코드를 올리면서 배포가 상당히 부담스럽기도 하고,
그 동안 너무 쫒겨서 코드를 짰다는 걸 느끼며 반성하는 시간을 가졌습니다.
React 생태계에서 어떤 아키텍쳐를 결정할지 논의하면서, 깊게 생각하지 않았던 것들을 정리할 수 있는 기회가 되어 좋았습니다.
꾸준히 더 나은 코드를 빚어내는 경험을 쌓아야 겠습니다.