프로젝트에서 전역 데이터 관리를 위해 Redux-toolkit 을 사용하기로 결정하고 데이터 fetching 및 관리를 어떻게 할까 고민하다가 rtk-query 라는 걸 알게됐다.
찾아보니 react-query 와 비슷하게 로직을 별도로 작성하지 않아도 fetching, caching, loading, error 등 기능을 제공해주는 라이브러리로 redux-toolkit 을 사용하면 설치없이 사용이 가능했다.
게다가 서버와 통신해서 가져온 데이터를 Redux store 에 저장할 수 있어서 상태 관리도 가능했기에 매우 유용하다는 생각이 들어서 프로젝트에 적용해보기로 했다.
rtk-query 는 base URL 당 하나의 API Slice를 사용한다.
이런식으로 notice
에 해당되는 로직만 작성하면 되니 한번에 관리할 수 있다는 점이 좋았다.
getNoticeList 와 같이 작성하면 알아서 앞 뒤로 use + getNoticeList + query(or mutation)
를 붙여서 훅으로 만들어준다.
export const noticeApi = createApi({
reducerPath: 'noticeApi',
baseQuery: fetchBaseQuery({
baseUrl: `${process.env.REACT_APP_API_URL}/notices`,
}),
tagTypes: ['notice'],
endpoints: (builder) => ({
// <--! notice list 를 불러오는 로직 작성 !-->
getNoticeList: builder.query<
NoticeType,
{ pageNumber: number; pageSize: number; category?: NOTICE_CATEGORY_KEYS }
>({
query: (arg) => {
const { pageNumber, pageSize, category } = arg;
return {
url: '/',
params: { pageNumber, pageSize, category },
};
},
providesTags: [{ type: 'notice' }],
}),
// <--! notice detail 를 불러오는 로직 작성 !-->
getNoticeDetail: builder.query<TNoticeDetail, number>({
query: (id) => `/${id}`,
}),
}),
});
export const { useGetNoticeListQuery, useGetNoticeDetailQuery } = noticeApi;
그럼 아래와 같이 사용하면 된다😎
const { data, isLoading } = useGetNoticeListQuery({
pageNumber: page,
pageSize: NOTICE_LIST_PAGE_SIZE,
category: category,
});
rtk-query 를 사용하면 아래와 같이 isLoading, error 객체를 활용해서 각 상황에 맞는 UI 를 표시할 수 있다.
그러나 매번 api 요청을 할때마다 유사한 코드를 작성해야 하는 번거로움이 있었다.
이러한 반복을 피하기 위해 React 에서 제공하는 Suspense
와 ErrorBoundary
를 활용하여 전역에서 한번에 처리하려고 했다.
그러나 rtk-query 에서는 아직 Suspense 를 지원하지 않고 있었다...😭
function PostsList() {
const { data, error, isLoading } = useGetPostsQuery();
if(isLoading) return <div>Loading...</div>;
return (
<div>
{error.status} {JSON.stringify(error.data)}
</div>
)
}
아래 글을 보면 아직 여러 이슈로 인해 rtk-query에서는 Suspense를 지원하지 않고 있었다.
따라서 Suspense 와 rtk-query 를 함께 사용 하는건 어려울 듯 하여 우선은 Loading 은 위와 같은 방법으로 작성하도록 했다.
마찬가지로 rtk-query 에서는 ErrorBoundary 를 함께 사용하지 않는 듯 했다.
그래서 api 에러를 전역에서 처리하기 위해 미들웨어를 활용했다.
리덕스 툴킷에서 미들웨어는 리듀서가 상태를 업데이트하기 전에 액션과 상태를 가로채어 추가적인 작업을 수행할 수 있는데, 주로 애플리케이션의 로깅, 비동기 작업 처리, 라우팅 등과 같은 것들을 처리할 수 있다.
먼저, 하위 컴포넌트에서 발생하는 에러를 감지하고 처리하기 위해 다음과 같이 미들웨어를 작성했다.
rtkQueryErrorHandler 함수는 에러가 발생하면 setError 액션을 dispatch하여 에러 상태를 업데이트한다.
export const rtkQueryErrorHandler: Middleware = (api: MiddlewareAPI) => (next) => (action) => {
if (isRejected(action)) {
console.log('error : ', action.payload.originalStatus);
next(setError(action.payload.originalStatus));
}
return next(action);
};
다음으로, 전역적으로 에러 상태를 관리하기 위해 errorSlice를 작성했다.
이 errorSlice 는 단순히 에러 코드를 받아서 저장하고, 정리하는 역할을 한다.
import { createSlice } from '@reduxjs/toolkit';
import { RootState } from './store';
const initialState = {
status: null,
};
export const errorSlice = createSlice({
name: 'error',
initialState,
reducers: {
setError: (state, action) => {
state.status = action.payload;
},
clearError: (state) => {
state.status = null;
},
},
});
export const errorStatus = (state: RootState) => state.error;
export const { setError, clearError } = errorSlice.actions;
export default errorSlice.reducer;
마지막으로, 최상위 컴포넌트인 App 컴포넌트에서 에러 상태를 감지하고 처리하도록 했다.
에러가 있으면 ErrorModal을 띄우도록 구현하여, 하위 컴포넌트에서 어디서든 API 에러가 발생하면 모달이 띄워지도록 했다.
function App() {
const error = useSelector(errorStatus);
const dispatch = useDispatch();
const [isErrorModalOpen, setIsErrorModalOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const { errorHandler } = useApiError();
useEffect(() => {
if (error.status !== null && !isErrorModalOpen) {
setIsErrorModalOpen(true);
const message = errorHandler(error.status);
setErrorMessage(message);
}
}, [error]);
const handleCloseModal = () => {
setIsErrorModalOpen(false);
dispatch(clearError());
};
return (
<Container>
{isErrorModalOpen && <ErrorModal closeModal={handleCloseModal} message={errorMessage} />}
<Outlet />
</Container>
);
}
export default App;
구현한 화면👇
rtk-query 를 사용하면서 자연스레 react-query 와 비교를 하게 됐는데,
react-query
는 리액트 기반 라이브러리로 상대적으로 사용이 더 간편하고 로직이 간단하다는 장점이 있다.
Suspense 나 ErrorBoundary 와 같이 리액트 기반 기능들을 지원하기에 함께 사용하는데 어려움이 없었고, 관련 리소스가 많아서 문제 해결에 있어서 문서나 블로그 등을 참고할 수 있어서 적용이 쉬웠다.
반면, rtk-query
는 리덕스 기반으로 설계되어 있기 때문에 리덕스에 대한 지식이 필요하고 상태관리와 데이터 로직을 리덕스만의 표준화된 방식을 따라야 했다. 따라서 react-query 보다는 자유도는 떨어지지만 설계대로 코드를 작성하면 되기 때문에 적용하는데 큰 어려움은 없었다.
다만, react-query 에 비해 리소스가 너무 적어 자료를 참고하는데 어려움을 많이 겪었다.
또한 리액트와 함께 사용할 때 지원하지 않는 기능들이 있어 당황스러운 부분도 있었다. 하지만 상태 관리와 데이터 패칭 및 관리를 한 번에 할 수 있다는 점은 매우 유용했다.
앞으로 프로젝트에서 데이터 패칭 및 관리 라이브러리를 선택할때, 이번 경험을 바탕으로 rtk-query 와 react-query 의 각각의 특징을 고려해서 어느 것이 더 유리할지 선택하는데 많은 도움이 될 것 같다.