[React Query vs RTK Query] 리액트에서 서버 데이터 처리하기

Seungwon YI
noutecompany
Published in
8 min readAug 19, 2022

--

백엔드 서버와의 통신은 웹 프론트엔드 개발에 있어서 제일 핵심적인 부분이라고 생각합니다. 따라서 프로젝트마다 어떻게 하면 효율적인 방식으로 서버 데이터를 처리할 수 있을지 고민하며 여러 도구들을 찾아보았습니다. 이 글에서는 위버딩에서 웹 프론트엔드 개발을 경험하며 다루었던 React Query와 기존에 진행하던 프로젝트에서 사용한 RTK Query에 대해서 간단하게 소개해보려 합니다. (이에 앞서 저는 개발 경험이 깊지 않아 두 라이브러리에 대해 얕은 수준의 비교만 할 뿐이고, 설명하는 내용에 오류가 있을 수도 있다는 점을 말씀드립니다:))

결론부터 정리해 보면 React Query는 라이브러리에서 기본적으로 제공하는 기능들이 더욱 강력하다는 장점이 있지만, 코드 작성의 깔끔함이나 유지보수에 용이한 것은 RTK Query라고 생각합니다. 예시는 리액트의 함수형 컴포넌트를 기준으로 작성하겠습니다.

서버 데이터 상태관리 라이브러리를 사용하지 않는다면?

React Query와 RTK Query는 모두 서버의 데이터를 받아와 효율적으로 캐싱하는 것이 주요한 기능입니다. 예를 들어 서버에서 게시글 리스트를 받아와 사용자에게 보여줘야 하는 상황이라고 합시다. 해당 라이브러리들을 고려하기 전에는 리액트에서 대략 다음과 같은 방식으로 서버 데이터를 처리하곤 했습니다.

const [posts, setPosts] = useState([])
const getPosts = async() => {
const data = await ... // HTTP 클라이언트를 사용
setPosts(data)
}
useEffect(() => {
getPosts();
}, [])
return {posts.length ? posts.map(post => <Post key={post.id} post={post} />) : null}

하지만 대부분의 경우 위의 코드는 충분하지 않습니다. 로딩 뷰나 에러 처리를 위해서는 데이터를 받아오는 컴포넌트 내부에서 관리하는 상태(state)들이 늘어나고, UI 작업을 위한 코드들을 반복적으로 작성해야 합니다. 컴포넌트 구조가 복잡하면 props drilling을 하는 경우도 늘어납니다.

서버 데이터 상태관리 라이브러리의 도입

서버 데이터 관리 라이브러리를 사용하게 된 이유는 크게 다음과 같이 세 가지 이유가 있었습니다. 라이브러리를 사용하면,

  • 서버와의 통신 과정에서 로딩 상태, 에러 여부 등을 관련 컴포넌트 내부에서 직접 상태를 작성하여 관리하지 않아도 됩니다. props drilling을 할 필요도 거의 없어집니다.
  • 자동 데이터 캐싱을 통해 서버의 부담도 줄일 수 있습니다.
  • 또 일정 시간이 지나거나, 데이터 변동이 생겼을 때(유저가 게시글을 작성하는 등) 자동으로 캐시된 데이터를 제거하고 다시 받아와 유저에게 (적당히) 최신의 데이터를 보여줄 수도 있습니다.

이외에도 정말 다양한 기능들을 제공하지만, 라이브러리를 사용해야겠다고 마음먹은 시점에는 위에 언급한 장점들만으로도 사용할 가치가 충분히 있다고 판단했습니다.

RTK Query의 사용

처음 선택한 라이브러리는 RTK Query였는데, 해당 라이브러리를 선택한 이유는 다음과 같았습니다.

  • 당시 프로젝트에서 이미 사용하고 있었던 Redux Toolkit의 연장선이기도 했고, (redux-thunk나 redux-saga 등을 이용해 로직을 짜기에는 부담스러웠지만, RTK Query 역시 리덕스 미들웨어로 동작하기 때문에 Redux DevTools를 활용할 수도 있습니다)
  • 하나의 모듈을 중심으로 createApi 를 통해 관련 코드들을 모두 작성할 수 있어 코드의 작성과 유지보수가 편리할 것으로 생각했습니다. (공식문서에서 직접 “one API slice per base URL”를 권장하고 있습니다.)

또 RTK Query는 서버와의 통신 과정 전반에 있어 사용 규칙이 엄격한 편이라 파일과 모듈 관리에서 큰 고민을 할 필요가 없었고, HTTP 클라이언트로 기본 제공되는fetchBaseQuery를 사용하여 더욱 간단하게 작성할 수 있습니다. (axios나 fetch와 같이 다른 라이브러리를 사용할 수도 있긴 합니다.) 간단한 예시는 다음과 같습니다.

export const postsApi = createApi({
reducerPath: "posts",
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers) => { // 비동기 로직도 작성할 수 있습니다.
...
return headers;
},
})
tagTypes: ["Post List"], // 캐싱에 사용될 태그의 타입
endpoints: (build) => ({
getPosts: build.query({ // HTTP GET의 경우
query: () => ({ url: "posts" }),
provideTags: [{ type: "Post List" }], // 받아온 데이터에 태그 부여
}),
postPost: build.mutation({ // POST, PATCH, DELETE 등의 경우
query: (body) => {
return {
url: "/posts",
method: "POST",
body,
};
},
invalidateTags: [{ type: "Post List" }], // 해당 태그를 가진 캐시 데이터 제거(타입 체크도 자동으로 됨)
}),
}),
});

앞서 언급했듯 하나의 모듈에서 서버와의 통신을 거의 전부 해결할 수 있다는 점이 큰 장점이라고 생각합니다. 하지만 React Query와 비교했을 때 참고할 자료가 적고, 기능이 부족하다는 점(예를 들어 React Query에서는 RTK Query와는 달리 무한 스크롤 관련 쿼리들을 더욱 간단하게 처리할 수 있습니다.)에서 아쉬움을 느끼기도 했습니다. (기능 관련 부분은 각 라이브러리 공식문서에 서로를 비교해놓은 표가 있습니다:))

React Query의 사용

이렇게 한계를 느끼던 와중에 위버딩에서는 리액트 프로젝트를 개발하며 상태관리 라이브러리로 Recoil을 사용하게 되었습니다. 리덕스의 미들웨어 기능이 필요하지 않았기 때문에 다른 라이브러리를 사용해보고 싶었고, 리코일은 리덕스와 비교하여 러닝 커브가 낮아 부담없이 도전할 수 있었기 때문입니다.

하지만 리코일의 경우 비동기 데이터 처리와 관련한 기능에 부족한 점이 많아 서버 데이터 관리 라이브러리가 필요하다고 판단했습니다. 예를 들어 리코일에서도 selectorget 속성을 이용하여 비동기 데이터를 처리할 수는 있지만, set 속성에서는 비동기 로직을 이용할 수 없다는 점이 불편했습니다.

이에 더해 리덕스를 사용하지도 않았고 새로운 라이브러리를 시도해보고 싶었기에, React Query를 사용하게 되었습니다. React Query는 비교적 사용이 자유롭기 때문에 파일 구조나 모듈 작성에 있어 많은 고민을 했는데, 결과적으로는 대략 다음과 같은 패턴을 사용하게 되었습니다.

const getPosts = async () => {
return await axios.get("/posts");
};

우선 위와 같이 비동기 함수를 작성하고, 아래와 같이 useQueryuseMutation 훅에 전달해 주는데, 리액트의 컴포넌트 단계에서는 비동기 데이터 처리 관련 코드 작성을 최소화하고자 했습니다. 공식문서에서도 소개하고 있는 TkDodo’s Blog에서도 작성 패턴을 참고했습니다.

const POSTS = "posts"const useGetPosts = () =>  // HTTP GET의 경우
return useQuery([POSTS], getPosts); // 데이터에 POSTS 키를 부여
const usePostPost = () => { // POST, PATCH, DELETE 등의 경우
const queryClient = useQueryClient();
return useMutation(postPosts, {
onSuccess: () => { // 성공했을 경우 POSTS 키를 가진 캐시 데이터를 제거
queryClient.invalidateQueries([POSTS]);
},
};
};

이렇게 하면 컴포넌트 단계에서는 HTTP 클라이언트나 useQuery를 직접 사용하지 않고 useGetPostsusePostPost와 같은 커스텀 훅을 이용하게 되어 RTK Query와 비슷한 방식으로 사용할 수 있습니다.

그런데 React Query의 경우에는 비동기 함수를 직접 관리하는 것은 아닙니다. 따라서 RTK Query와는 달리 오히려 비동기 함수를 분리해서 작성하는 것이 유지보수에 편리하고, HTTP 통신에 필요한 헤더를 관리하거나 캐싱에 중요하게 작용하는 key의 관리 방식 등을 스스로 결정해야 합니다. 물론 자유도가 높다는 장점이 있지만, 하나의 slice에서 어플리케이션의 거의 모든 서버 통신 로직을 작성하는 RTK Query와 비교했을 때 스스로 패턴을 작성하는데 있어 더욱 많은 고민이 필요했습니다.

참고자료

RTK Query 공식문서
React Query 공식문서
TkDodo’s Blog
Recoil — 비동기 데이터 쿼리

--

--