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