킹의 개발일지
서버 데이터를 최신으로 관리해보자(feat: react-query) 1 본문
문제의 발단
토이 프로젝트를 진행하면서 구현하고자 하는 로직은 간단했다. add task로 생성한 Task 컴포넌트를 drag and drop을 사용해서 원하는 보드에 올려두면 저장되는 로직이다. 생각했던 방법은, react의 Context Api를 사용해서 전역 상태를 관리하고, drag end(끌어서 보드위에 올리는) 이벤트가 발생시 POST를 날리는 것이다.
그러나 Sorting 기능을 넣고나서, 가끔씩 drag over(드래그 중인 컴포넌트가 task 또는 보드, 즉 droppable 컴포넌트 위로 지나갈 때) 이벤트가 일어날 때 문제가 발생했다. 보드 위에서, 현재 드래그 중인 Task의 순서를 정하기 위해 기존 Task들 사이로 컴포넌트를 움직이다가 컴포넌트를 놓으면 db에 저장된 Task의 순번과 로컬의 Task 순번이 달랐다.

위 그림처럼 draggable 컴포넌트가 droppbable 컴포넌트 위로 올라가면, droppable에서 관리하는 배열에 draggable 컴포넌트의 데이터를 집어 넣고, 순서를 sorting하는 콜백 함수가 발생한다.
내 생각에는, 콜백 함수로 순번이 정해지기 전에 drag end로 인해서 POST 요청이 생긴 모양이었다. 여하튼 db에 저장된 데이터와 클라이언트의 상태가 다른 문제가 생겼는데, 이를 해결할 방법이 필요했다.
해결법을 찾아보던중 react query에 대해서 알게 됐고 오늘은 react query의 특징들에 대해서 글을 써볼 것이다. 문제에 대한 해결 방법은 다음 포스팅에 있을 것이다. ㅜㅜ
서버 상태? 상태는 useState만 있는게 아니었어?
우선 리액트 프로젝트에서 일반적으로 사용하는 상태(state) 세 가지를 살펴보자.
- Local State: 리액트 컴포넌트 안에서만 사용되는 state
- Global State: Global Store에 정의되어 프로젝트 어디에서나 접근할 수 있는 state
- Server State: 서버로부터 받아오는 state
여기서 로컬, 글로벌 상태에 대해서는 들어봤지만 Server state에 대해서 생소하게 느껴질 수 있다. (내가 그랬다! )
Server state는 백엔드 서버로부터 받아오는 모든 데이터를 의미한다.
db에 있는 데이터를 한 클라이언트만 소유하는것이 아니라 다른 클라이언트들과도 공유하기 때문에 언제나 수정 될 수있다. 그리고 클라이언트가 요청을 보내 받은 응답은 서버 데이터의 '한 시점'(특정 시점에서 db의 사진을 찰칵 찍어 보내는 느낌?..) 이기에 항상 최신을 보장 할 수 없다.
이런 특성 때문에 앞서 말한 서버 상태와 클라이언트의 상태가 달라지는 문제가 발생할 수 있다. 그럼 이를 쉬이 해결 할 수있는 방법이 있을까?
Data fetching 해결사 React-Query!
앞서 말한 문제는 흔히 발생할 수 있으므로 다양한 해결 방법들이 있는데, redux 같은 상태 관리 라이브러리를 사용해서도 서버 상태를 관리할 수 있다.
클라이언트 상태관리는 액션과 리듀서를 사용해서 할 수 있지만, 서버 상태를 처리하기 위해선 미들웨어가 필요하다. 리덕스 미들웨어는 액션을 중간에서(이런 의미로 '미들'웨어라 한다) 다른 로직을 실행할 수 있도록 해주는데, 비동기 로직을 처리하는 대표적인 미들웨어는 redux-thunk와 redux-saga가 있다.
미들웨어를 사용하면, 서버 상태가 변경 됐을 때 동기화를 위해서 주기적으로 데이터를 폴링해서 리덕스 스토어의 데이터를 업로드 해주어야한다. 때문에 동일한 API 호출이 여러번 발생하게 된다. 또한 방대한 보일러플레이트 때문에 사용하기 까다로운 면이 다소 있다.
위에서 언급한 리덕스의 단점을 보완하는 라이브러리가 있는데..., 바로 React Query이다.
(비슷한 data fetching 라이브러리로 vercel의 SWR도 있다.)
React query의 장점
React Query는 미들웨어를 사용할 때의 단점을 간단히 해결할 수 있게 해준다.
1. 긴 보일러 플레이트 코드를 없앨 수 있다.
redux thunk를 사용해서 데이터를 가져오는 메서드를 먼저 살펴보자.
export function fetchSomeData(todoId) {
return async function fetchSomeDataThunk(dispatch, getState) {
const response = await client.get(`/some/url`)
dispatch(someDataLoaded(response.something))
}
}
여기서 끝날 일이 아니라, 받아온 데이터를 전역 상태로 관리하기 위해 액션 타입에 따라 응답을 처리해주는 함수도 구현해주어야 한다.
그러나 react query는 긴 코드 없이, 아래와 같이 매우 간단하게 데이터를 가져올 수 있다.
const {isLoading, error, data} = useQuery(`/some/url`, fetcher, options);
2. 동일한 API 요청이 여러 번 호출 되더라도 한 번만 호출한다.
react query는 내부적으로 query key에 해당하는 캐시 값을 저장하고 있다. 이 값은 useQueryClient()의 반환값을 콘솔로그를 찍어보면 확인할 수 있다. queriesMap이라는 프로퍼티로 캐시들을 저장하고 있는데, 각 useQuery의 인자로 준 queryKey에 해당하는 값 형태임로 저장됨을 확인 할 수 있을 것이다.
때문에 동일한 요청이 여러번 생기더라도 query key에 저장된 데이터를 가져오는 것 이기에 중복 호출이 생기지 않는다.
3. react query는 서버 데이터가 변경 됐을 시점에 알아서 업데이트 해준다.
react query는 백그라운드에서 서버로 요청을 보내 주기적으로 데이터를 캐싱하여 최신화 시킨다. 이는 useQuery가 반환하는 isFetching 값을 통해서 확인할 수 있는데, isFetching이 true인 순간은 react query가 몰래 서버에 요청을 보내는 중이라는 뜻이다! 이렇게 받아온 데이터가 변경된 것이라면 react query알아서 업데이트 해주는 것이다.
오늘은 react query의 특징들을 설명하면서 react query가 어떻게 동작하고 있는지 간략하게 살펴보았다. 다음 포스팅에서는 기본적인 사용법과 앞서 마주쳤던 문제를 해결한 방법에 대해서 포스팅 하겠다.