-
서버 상태 관리를 위한 라이브러리 선택Frameworks, Platforms and Libraries/React 2023. 10. 15. 15:04
Store에서 전역 상태 관리뿐만 아니라 서버 상태 관리(비동기 처리 상태 관리)에 대한 처리도 같이 하다 보니 Store가 너무 비대해지는 현상이 발생했습니다. 이에 따라 전역 상태 관리와 서버 상태 관리를 분리시켜 Store의 경량화를 위해 서버 상태 관리 라이브러리를 찾아보게 되었습니다.
관련 라이브러리
레퍼런스가 가장 많은 React Query와 SWR 그리고 현재 RTK(Redux Toolkit)를 사용 중이기 때문에 RTK Query 이렇게 도입을 위해 세 라이브러리를 비교해 보았습니다.
- React Query
https://react-query.tanstack.com/overview - SWR
https://swr.vercel.app/ko - RTK Query
https://redux-toolkit.js.org/rtk-query/overview
RTK Query의 경우 RTK에 포함되어 있어 정확한 통계 수치를 얻긴 힘들지만 npm trends의 최근 1년 다운로드 수와 State of JS 2022에서의 각 라이브러리 비교는 다음과 같습니다.
각 라이브러리 비교
각 라이브러리끼리 가볍게 비교하며 살펴보겠습니다.
- React Query와 RTK Query는 Provider가 필요하며 Devtool을 지원하지만 SWR은 Provider 없이 사용 가능하며 공식적인 Devtool이 없습니다.
- React Query와 SWR은 두 번째 인자로 fetcher을 넘기지만 RTK Query는 첫 번째 인자로 서버 상태를 가져오기 위한 정보가 담긴 객체를 넘깁니다. SWR은 공통 fetcher 함수를 지정할 수 있지만 React Query는 매번 fetcher을 넘겨줘야 하며 RTK Query의 경우에도 fetcher를 넘겨야 하지만 createApi와 injectEndpoints을 통해 feature 별로 공통의 옵션을 재활용할 수 있습니다.
- React Query와 RTK Query는 isLoading / isFetching / isSuccess / isError로 상태를 나타내지만 SWR은 isLoading / isValidating로 상태를 나타냅니다.
- React Query와 RTK Query는 HTTP request methods의 post, put, patch, delete로 서버 상태를 변경시키지만 SWR은 useSWR()을 통해 받아온 데이터를 mutate()을 통해 클라이언트 사이드에서 변형시킵니다.
사용 방법
각 라이브러리 별로 간단하게 사용 방법을 살펴보겠습니다.
React Query
React Query를 전역에서 사용하기 위해 Provider를 main에서 선언해 줍니다.
// main.tsx ... import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; ... const queryClient = new QueryClient(); ... ReactDOM.render( <Provider store={store}> <QueryClientProvider client={queryClient}> ... </QueryClientProvider> </Provider>, document.getElementById('root'), ); ...
Data를 가져오는 케이스에선 useQuery Data를 mutate 하는 케이스에선 useMutation을 사용합니다. query key을 통해 해당 query caching을 관리합니다.
// Components.tsx ... import { useQuery, useMutation, QueryClient } from '@tanstack/react-query'; import API from '@/api'; ... function Components() { ... const queryClient = new QueryClient(); const { isLoading, data } = useQuery('user', async () => { const response = await API.getUser(userID); return response.data; }); const mutation = useMutation( async (userID: string) => { await API.deleteUser(userID); }, { onSuccess: () => { queryClient.invalidateQueries('user'); }, }, ); ... return ( <Page> {isLoading && <PageLoader />} {!isLoading && ( ... )} </Page> ); }
SWR
SWR은 main에서 공통 config를 설정할 수 있으며 공통으로 fetcher함수를 지정하여 사용할 수 있습니다.
// fetcher example import axios from 'axios'; const baseUrl = import.meta.env.VITE_BASE_URL; export const fetcher = (url: string) => axios.get(`${baseUrl}/${url}`).then((res) => res.data);
// main.tsx ... import { SWRConfig } from 'swr' ... ReactDOM.render( <Provider store={store}> <SWRConfig value={{ <options> }} > ... </SWRConfig> </Provider>, document.getElementById('root'), ); ...
Data를 가져오는 케이스에선 useSWR을 사용하여 데이터 갱신이 필요한 케이스에선 useSWRConfig hook을 통해 가져온 mutate를 사용합니다. React Query와 key를 통해 제어합니다.
// Components.tsx ... import useSWR, { useSWRConfig } from 'swr'; import { fetcher } from './fetcher'; ... function Components() { ... const { mutate } = useSWRConfig(); const { data } = useSWR('api/users', fetcher); ... return ( <> <Page> {!data && <PageLoader />} {data && ( ... )} </Page> </> ); }
RTK Query
RTK Query의 경우 main 세팅은 RTK 세팅과 같이 redux의 Provider를 사용합니다.
// main.tsx ... import { Provider } from 'react-redux'; ... ReactDOM.render( <Provider store={store}> ... </Provider>, document.getElementById('root'), ); ...
RTK Query의 경우 createApi를 통해 데이터를 가져오거나 mutate 하는 비동기 처리를 구성합니다. createApi을 이용하여 공통으로 설정된 api를 가져와 feature별로 injectEndpoints을 통해 feature 단위의 api로 구성합니다. 비동기 처리를 endpoints라는 파라미터를 통해 구성하며 endpoints로부터 build(EndpointBuilder)를 전달받아 데이터를 가져오는 케이스에선 build.query 데이터를 mutate 하는 케이스에선 build.mutate를 사용합니다. 이렇게 구성된 api를 RTK Query에선 API Slice로 부릅니다.
// store/services/user.ts import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; const baseURL = <'api base url'>; export const userSplitApi = createApi({ reducerPath: 'userApi', baseQuery: fetchBaseQuery({ baseUrl: baseURL }), tagTypes: ['User'], endpoints: () => ({}), });
// store/services/userApi.ts import { userSplitApi } from './user'; import { UserData } from '@/interface/User'; export const userApi = userSplitApi.injectEndpoints({ endpoints: (build) => ({ getUser: build.query<UserData, string>({ query: (id) => `user/${id}`, }), // mutation case // build.mutation 활용 }), }); export const { useGetUserQuery } = userApi;
reducer의 경우 기존에 RTK만 사용하는 케이스와는 다른 형태로 store에 등록합니다. reducer 뿐만 아니라 custom middleware 둘 다 포함되어야 합니다.
// store/reducer.ts import { api } from './services/api'; ... export default { ... [api.reducerPath]: api.reducer, }; // store/index.ts ... export const store = configureStore({ reducer: reducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware), }); ...
Component에서 API Slice가 필요한 케이스에 사용하면 됩니다.
// features/user/Components.tsx import { useGetUserQuery } from '@/store/services/userApi'; ... function Components() { ... const { data, isSuccess, isLoading } = useGetUserQuery(userID); ... return ( <Page> {isLoading && <PageLoader />} {isSuccess && ( ... )} </Page> ); }
각 라이브러리 별로 사용방법을 알아보았습니다. 간단하게 예시를 들었기 때문에 제대로 사용하기 위해선 추가로 설정해야 할 사항들이 있겠습니다.
결국...?
현재는 React Query를 도입하여 사용 중입니다. 가장 많은 사용수와 많은 레퍼런스도 이유긴 하지만 가장 큰 이유는 현재 작업 중인 프로젝트에 제일 유연하게 도입이 가능했기 때문입니다. 서버 상태 관리 라이브러리를 도입 함으로써 Store의 역할이 전역 상태 관리에 집중하게 되었고 많이 경량화되어 현재 만족하며 사용하고 있습니다.
참고 자료
https://tanstack.com/query/v4/docs/quick-start
https://swr.vercel.app/ko/docs/getting-started
https://redux-toolkit.js.org/rtk-query/overview
https://techblog.woowahan.com/6339/
https://fe-developers.kakaoent.com/2022/220224-data-fetching-libs/
'Frameworks, Platforms and Libraries > React' 카테고리의 다른 글
React Query 도입기 (0) 2023.11.09 useState와 useRef의 차이 (0) 2023.10.29 Redux Selector 최적화 (0) 2023.09.10 Redux Toolkit으로 상태 관리 하기 (0) 2023.08.20 useEffect vs useLayoutEffect (0) 2023.08.06 - React Query