전역상태관리 라이브러리 (Redux, MobX)
웹 개발을 공부하다보면 Redux, MobX 혹은 Recoil등 전역상태관리 라이브러리에 대해 알게된다. 이 라이브러리들에 대해 간단히 말하면 각 컴포넌트간의 상태를 props만으로 전달하는것은 프로젝트가 커질수록 굉장히 복잡해지기 때문에, 이런 상태들을 한번에 관리할 일종의 저장소를 만들고, 각 컴포넌트에서 해당 저장소의 값을 참고하여 사용하는것이다.
전역상태관리 라이브러리 도입의 이유
새로 진행하는 프로젝트에 React-query를 도입할 계획이었는데, 해당 라이브러리를 이용하면 다음과 같이 비동기 통신등으로 부터 가져온 데이터를 일정시간동안 캐싱해놓고, 다른 컴포넌트에서도 필요할 때 언제든지 가져다가 쓸 수 있었다.
// A Component
const { data } = useQuery("example", apiCall, {staleTime: 100000})
// B Componnet
const queryClient = useQueryClient();
queryClient.resetQueries("example");
이러한 React-query의 도움으로 인해, 전역으로 특정 값들을 저장해 놓을 필요가 많이 줄어들었기 때문에, 전역으로 상태를 저장할 필요가 많이 없어보였다. 따라서 개발팀에서는 굳이 상태관리 라이브러리를 사용하기보다, 필요한 부분, 예를들어 전역으로 사용할 팝업관리 라던지 등에만 useReducer를 활용하여 Context API를 사용하기로 하였다.
그러나 실제로 프로젝트를 진행중에 생각보다 상태를 전역으로 사용해야 할 일이 많았고, 그때마다 새로운 Context를 만들어서 주입하자니 꽤 큰 보일러플레이트 자체도 거슬렸고, Context API 자체가 상태관리 라이브러리를 대체하기에는 그 역할이 묘하게 다르게 느껴졌다. Context API로 분명 props drilling을 어느정도 피할 수는 있었으나, 애초에 '전역 상태 관리' 보다는 Provider의 이름처럼 '특정 상태를 주입' 해주는 느낌에 가까웠다.
return (
<FeedContextProvider>
<ChipContextProvider>
<ModalContextProvider>
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Modal />
<DefaultSeo {...SEO} />
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} />
</Hydrate>
</QueryClientProvider>
</ModalContextProvider>
</ChipContextProvider>
</FeedContextProvider>
);
프로젝트 규모가 크지 않은데에도 주입해야할 Provider가 생각보다 많이 늘어나고 있었고, 스케일업 됨에 따라 관리가 어려워질 것은 자명해보였다. (react-query로 인해 늘어난 provider는 덤) 이런 이유 등으로 인해 전역 상태 관리 라이브러리를 도입하기로 하였다.
MobX
전역 상태 관리 라이브러리는 Redux, MobX, Recoild 등 유명한것들이 많았는데, 그중에서도 MobX를 선택하기로 하였다. 일단 Recoil은 직접 사용해본적이 없었고, Redux는 Context API와 비슷하게 보일러 플레이트가 꽤 큰 라이브러리었다. 실제로 동작 원리도 Reducer를 사용하는 Context와 비슷하다. 반면 MobX는 그냥 store를 생성해서 hook을 활용해 전역으로 쉽게 사용할 수 있었고, 이전에 안드로이드 개발할 때 익숙한 아키텍쳐인 MVVM 패턴과 이 MobX가 유사했기 때문에 Context를 대체할 '간단한' 전역상태관리 라이브러리로서 MobX를 선택하였다.
전역상태관리 라이브러리의 목적
나의 목적은 오로지 전역 상태 관리 그 하나였다. 간단히 말하면 global로 선언할 수 있는 상태, 그것이 목적이었다. 그런데 이전에 공부할 때 MobX를 써봣기 때문에 해당 프로젝트를 옆에 띄워놓고 도입하다보니 나도 모르게 다음과 같은 코드를 작성한다.
class EstimateStore {
estimateResultList: EstimateData[] = []; // 비동기 통신의 결과값을 담을 배열
// ... 중략
addEstimateResults(data: EstimateResponse) {
this.estimateResultList.push(data)
}
async estimateFile() {
const { data } = await axios.post<EstimateResponse>(
`https://apiExample.com`,
formData,
{
headers: {
"Content-Type": "multipart/form-data",
},
}
);
this.addEstimateResults(data);
}
}
실제로 위와 같은 코드가 아주 잘못 작성된 코드는 아니라고 생각한다. (MobX에서 비동기 통신과 그 상태관리를 위해 flow를 추가로 제공한다.) 그러나 위 store는 본인 기준으로는 너무 많은 로직을 담고 있다. 앞서 말한대로 내가 MobX를 도입한 이유는 비동기 결과값을 제외한 전역 상태를 관리하기 위함이었고, react-query를 이용하면 비동기 통신의 결과값을 캐싱하여 쉽게 재사용할 수 있는데, 굳이 비동기 통신의 결과값을 하나의 상태로서 전역으로 저장하는 번거로운짓을 하고 있다. 굳이 따지면 react-query는 오로지 비동기 통신의 값을 전역으로 저장하는 store, MobX는 그 외의 상태를 전역으로 저장하는 store로 볼 수 있겠다.
또한 react-query의 장점 중 하나는 설정에 따라 자동으로 최신의 비동기 통신의 결과를 계속 가져올 수 있다는건데, 위처럼 store 안에 비동기 통신을 하고 그 값을 저장하면, 내가 해당 함수를 호출할 때에만 수동으로 값을 저장한다. 즉 비동기 데이터의 최신화를 보장할 수 없다. 물론 redux도, mobx도 비동기 통신을 도와주는 추가적인 라이브러리, 로직등이 존재하지만 해당 기능들을 사용하기 시작하면 store의 규모가 끝도없이 커진다.
class EstimateStore {
estimateResultList: EstimateData[] = []; // 비동기 통신의 결과값을 담을 배열
addEstimateResults(data: EstimateResponse) {
this.estimateResultList.push(data)
}
}
비동기 코드는 전부 떼어내자. 오로지 상태관리용으로만 store를 쓴다. 이러면 store 자체의 코드량도 정말 획기적으로 줄어든다.
결론: 관심사 분리
이처럼 react-query와 상태관리 라이브러리를 같이쓰면 결국은 '관심사 분리'의 측면에서 매우 효율적으로 코드를 유지보수할 수 있게 된다. 전역상태는 store에, 비동기 데이터는 전부 react-query의 queryClient로 전역에서 관리할 수 있다.
'WEB' 카테고리의 다른 글
| Next.js의 Middleware를 사용하여 A/B Testing하기 (with GA4) (0) | 2023.02.16 |
|---|---|
| [React] useState의 동작원리에 관하여 (0) | 2022.12.28 |
| next-sitemap을 사용하여 동적 생성 페이지 sitemap 생성하기 (0) | 2022.12.19 |
| DOM(Document Object Model) (0) | 2022.06.27 |
| CSS em & rem (0) | 2022.06.26 |