
react-query는 2019년 말 오픈 소스 작성자 Tanner Linsley가 만든 것으로 data fetching, 캐싱, 동기화 및 서버 데이터에 따른 데이터갱신을 쉽게 해줄 수 있는 비동기 처리 라이브러리라고 소개한다.
그리고 react-query는 아래와 같은 동작들을 처리하기 위해 적합하다고 한다.
- Caching... (possibly the hardest thing to do in programming)
- Deduping multiple requests for the same data into a single request
- Updating "out of date" data in the background
- Knowing when data is "out of date"
- Reflecting updates to data as quickly as possible
- Performance optimizations like pagination and lazy loading data
- Managing memory and garbage collection of server state
- Memoizing query results with structural sharing
아직 쪼고미 개발자인 내게 와닿는 것은 중복된 데이터에 대한 한 번의 요청으로 처리와 업데이트된 데이터의 빠른 반영이 눈길을 끌었다.
react-query는 unique한 값인 queryKey를 기반으로 Data를 caching(저장)한다.
그럼 queryKey가 무엇인지 살펴보자
queryKey는 데이터를 저장하기위한 고유한 값으로 cache에 저장된 데이터를 식별하는데 사용된다. cache에 저장된 데이터를 통해 hard loading(EX) loading spinner 표시)없이 즉각적으로 data를 가져와서 더 높은 사용자 경험을 줄 수 있다.
그럼 queryKey의 특징은 무엇일까?
- string 또는 배열형태이다.
- 중첩 객체일 수 있지만
top-level은 배열이어야 한다.- queryKey는
hash 처리된다.
const fetchTodos = () => { ...fetching Todos logic }
useQuery('todos', fetchTodos)
useQuery(['todos'], fetchTodos)
queryKey는 string 또는 배열형태에 대한 예시이다.
여기서 fetchTodos는 queryFn으로서 queryKey에 기반하여 data를 fetching하는 함수이다.
queryKey는 중첩된 객체의 형태를 가질 수 있으며 중요한 점은 top-level은 array형태이어야 한다는 것이다. 즉, {['todos']}와 같이 top-level(가장 바깥)이 배열이 아닌 객체면 안 된다.
위의 단순한 queryKey의 예제에서 간단한 todos를 fetching하는 query instance를 생성해보았다. 만약에 filter값인 all, done, doneYet에 대한 todos를 fetching한다고 가정하면 아래와 같이 query를 짤 수 있다.
const fetchTodos = (filter) => { ... } // fetching filtered Todos
useQuery('todos', () => fetchTodos('All'))
useQuery('todos', () => fetchTodos('Done'))
useQuery('todos', () => fetchTodos('DoneYet')))
순차적으로 useQuery함수를 통해 query instance가 실행된다. 이 때 filter 값인 All, done, doneYet에 따라서 return 하는 todo가 다를 것이다. 따라서, 맨 마지막에 doneYet에 따른 useQuery가 실행되어 queryKey가 'todos'가 참조하고 있는 cache에 미완료된 todos에 대한 data가 들어갈 것이라고 쉽게 예측할 수 있다.
하지만, 예상한 동작과는 다르게 처음에 실행된 useQuery에 대한 데이터(filter='all')만이 존재한다. 분명 useQuery는 실행되었고 data도 fetching했지만done, doneYet과 관련된 todo들은 어디로 갔을까? 문제점은 data fetching은 하지만 cache에 data가 바뀌지 않았다.
이류를 살펴보자면, 위에서 queryKey는 해시 값으로 저장되며 data는 queryKey를 기반으로 cache에 저장된다고 했다. 이 말을 조금 바꿔서 말하자면 같은 queryKey를 참조하고 있는 query instance는 동일한 cache를 공유한다는 것이다. 따라서, filter='all'에 대한 query가 실행되면 cache에 모든 todo들이 저장되고 이후 done, doneYet애 대한 query실행은 이미 동일한 queryKey에 대한 data가 cache에 존재함으로 이 데이터를 가져온다.
동일한 querykey에 대해 query instance 3번 호출

아마 해결책으로 todos관련된 data임을 내포함과 동시에 queryKey를 식별할 수 있게 만들면 좋지 않을까?라고 생각할 수 있다. 이를 해결해주는 것이 계층적으로 배열을 선언하거나 중첩 객체를 이용하여 queryKey를 만들면 된다.
useQuery(['todos', 'all'], () => fetchTodos('All')) // 계층 배열 queryKey
useQuery(['todos', {'done': true}], () => fetchTodos('Done')) // 중첩 객체 queryKey
useQuery(['todos', {'done': false}], () => fetchTodos('DoneYet')) // 중첩 객체 queryKey
querykey가 다른 query instance 3번 호출

그림에서 확인할 수 있듯이 각각 다른 queryKey라고 인식을 하기 때문에 의도대로 data를 저장할 수 있다.
위와 같이 동일한
todos라는 data를 공유하면서 특정조건에 따라 data를 제공하고 싶을 때 위와 같이 queryKey를 정의하는 것을 종속성 쿼리라고 한다. tkTodo docs를 참조하면 이는 useEffect의 dependency array와 유사하게 작동한다고 언급한다.⭐️⭐️⭐️ 추가적으로 위의 queryKey에 대한 내용은 react-query version3를 기반으로 사용한 것으로 최근 버전인 5에서는 queryKey를
string이 아닌 배열 형태로 강제하고 있다.// react-query version 5 useQuery('todos', () => fetchTodos('All')) // queryKey -> string ❌ (version 3) useQuery(['todos'], () => fetchTodos('All')) // queryKey top-level array ✅ (version 5)
react-query를 배우면서 매우 헷갈렸던 것이다. 이를 잘 이해해야 추후에 isLoading과 isFetching에 대한 개념을 이해할 수 있는 초석이 된다. cacheTime에 대해서는 대충 감이 왔었는데 staleTime을 이해하는데 시간이 좀 걸렸다. stale은 사전적 의미로 신선하지 않은, 오래된이란 의미를 갖는다. 즉, data가 얼마나 최신상태인지를 뜻한다. cacheTime은 data가 cache에서 얼마나 오랫동안 cache에 저장될 수 있는지에 관한 시간이다.
staleTime: staleTime = 5분으로 설정했다 가정하자. 처음 query가 실행(0초)되고 5분간 데이터가신선(최신 상태)하다고 판단하는 시간이다.staleTime이 유효하지 않다면 refetch를 진행한다.
cacheTime: cacheTime = 10분으로 설정하면 10분 동안 cache에 data를 저장한다.
추가적으로, cacheTime > staleTime으로 설정을 하는게 좋다고 생각한다. 아니 맞다고 생각한다. 왜냐하면 staleTime이 더 커버리면 최신상태라도 불구하고 cache에서 꺼낼 데이터가 없기 때문에 hard loading을 유발하기 때문에 캐싱의 기능을 제대로 사용하지 못 하기 때문이다...?
💡 cacheTime동안 데이터가 refetch가 되지 않는다면 garbage-collector에 의해서 cache가 비워진다.
react-query는 refetching기능을 다양한 조건에 따라서 제공한다. 내가 찾아서 추린 것으로 7가지에 대해서 다루겠다.
- 쿼리 키 변경
- refetchOnWindowFocus
- refetchInterval
- staleTime 만료
- 사용자에 의한 명시적 재요청
- refetchOnMount
- refetchOnReconnect
쿼리의 키가 변경되면 react-query는 알아서 refetching을 진행한다.
const { data } = useQuery(["todos", todoId], fetchTodos);
의존성 쿼리인 todoId가 변경될 시 refetching한다.
이는 사용자가 browser에 focus를 두었을 때 발생한다. 예를 들면 사용자가 다른 window 탭을 갔다가 다시 돌아오면 fetching이 trigger된다.
const { data } = useQuery("todos", fetchTodos, {
refetchOnWindowFocus: true,
});
refetchOnWindowFocus: true가 기본 값으로 해제하고 싶으면 refetchOnWindowFocus: false로 바꿔주면 된다.
정해진 시간 간격으로 data를 refetching하라고 알려줄 수 있다.
const { data } = useQuery("todos", fetchTodos, {
refetchInterval: 60 * 1000, // 60초마다 refetching
});
refetchInterval는 기본 값은 false이기 때문에 이 기능을 활성화하려면 명시적으로 시간 값을 지정해주어야한다.
staleTime이 만료되면 다음에 query가 실행될 때 refetching한다. staleTime의 기본값은 0으로 data를 fetching하자 마자 오래된 데이터라고 간주한다.
const { data } = useQuery("todos", fetchTodos, {
staleTime: 10000,
처음 data를 fetching하고 나서 10초동안은 최신 data 10초 이후는 갱신될 필요가 있는 data로 간주됨으로서 refetching한다.
사용자가 발생키니 event, action에 따라 refetch를 명시할 수 있다. 이 때 refetch는 useQuery가 return하는 값에서 가져올 수 있다.
const TodosComponent = () => {
const { data, refetch } = useQuery("todos", fetchTodos); // 👉🏻 refetch함수를 받아와야 함
return (
<button onClick={() => refetch()}>Refresh Todos</button>
);
};
사용자가 click event를 발생시키면 refetch한다.
이는 query가 마운트 될 때 refetch할 지 말지에 대한 옵션으로 특정한 상황을 제외하고는 편하게 component가 마운트 될 때라고 생각하면 이해하기 쉽다. ₩component가 mount되는 시점에 코드가 실행되어 query instance가 생성되기 때문₩이다.
const { data } = useQuery("todos", fetchTodos, {
refetchOnMount: true,
});
기본 값은 true이다.
브라우저와 재연결 되었을 때 refetch한다. 예를 들어서 오프라인 -> 온라인으로 전환될 때 실행된다.
const { data } = useQuery("todos", fetchTodos, {
refetchOnReconnect: true,
});
기본 값은 true이다.
react-query는 개발의 편리를 위해 debugging tool인 devtool을 지원한다.
import { ReactQueryDevtools } from 'react-query' // version 3
import { ReactQueryDevtools } from '@tanstack/react-query-devtools' // version 5
function App() {
return (
<QueryClientProvider client={queryClient}>
<Component />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
- process.env.NODE_ENV === 'development'일 때 js bundle에 포함되며, production에서는 lazy loading으로 추가시킬 수 있다.
- 현재 react native는 지원하지 않는다.
- floating mode를 지원하며 toggle형식으로 devtool을 끄고 킬 수 있다.
- version5부터는 별도로
@tanstack/react-query-devtools를 설치해야 사용할 수 있다.
version 5

floating mode라는 것이 그냥 position: fixed느낌인 것 같다.

위의 todo예시에 관한 것을 devtool로 볼 수 있으며 fresh, fetching, stale, inactive는 version 3에도 있었으며 3이후 버전에서 paused가 추가되었다. 각 devtool에 나와있는 상태에 대해서 간략히 요약해보자.
- fresh: 저장된 데이터가 staleTime이 만료되지 않은
최신 데이터이다.- fetching: 데이터를 가져올 때 나타나는 상태다.
- stale: fresh의 반대 상태로
데이터가 오래 되어 최신 상태로 갱신이 필요함을 의미하는 상태다.- inactive: 현재 페이지에서 사용되지 않는 데이터를 표시할 때 나타낸다.
그리고 paused상태에 대해서는 잘 모르겠는데 docs를 찾아보다가 네트워크 연결 끊김으로 인해 중단된 fetching을 의미하지 않나 싶다.
지금 블로그를 정리하는 시점에 react-query를 이용하여 무한 스크롤, pre-fetching을 이용한 pagination을 간단하게 구현하여 react-query에 대해서 익숙해져 가는 중이다. 비동기 처리에 관해서 error와 loading시 적절한 UI를 사용자에게 보여주어야 하며, 조금의 버벅거림과 계속 표시되는 loading은 좋지 않은 사용자 경험을 줄 수 있는 것을 보다 편하게 구현할 수 있었다. 아직 초보 개발자이지만 비동기처리에 관해서는 모두가 어려워하는 부분이지 않나 생각이 들며 caching, 최신 데이터를 빠르게 가져오고 저장하고 쓸 수 있는 것이 react-query의 매력인 것 같다. 현재는 v3버전을 먼저 살펴보면서 v5 버전과 비교해보면서 공부해보고 있다. 그리고 공식문서 말고도 한국인은 여기도 같이 참조하면서 공부하면 좋을 것 같다.
queryKey
react-query overview
react-query deep dive???
react-query devtool
react-query tutorial korean