react-query-[basics]

GI JUNG·2023년 11월 29일
0

react-query

목록 보기
2/4
post-thumbnail

🍀 react-query

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

queryKey는 데이터를 저장하기위한 고유한 값으로 cache에 저장된 데이터를 식별하는데 사용된다. cache에 저장된 데이터를 통해 hard loading(EX) loading spinner 표시)없이 즉각적으로 data를 가져와서 더 높은 사용자 경험을 줄 수 있다.

그럼 queryKey의 특징은 무엇일까?

  1. string 또는 배열형태이다.
  2. 중첩 객체일 수 있지만 top-level은 배열이어야 한다.
  3. queryKey는 hash 처리된다.

1️⃣ 단순한 queryKey

const fetchTodos = () => { ...fetching Todos logic }

useQuery('todos', fetchTodos)
useQuery(['todos'], fetchTodos)

queryKey는 string 또는 배열형태에 대한 예시이다.
여기서 fetchTodos는 queryFn으로서 queryKey에 기반하여 data를 fetching하는 함수이다.

2️⃣ 계층 & 중첩 객체 queryKey

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)

⏰ staleTime VS cacheTime

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하는 조건

react-query는 refetching기능을 다양한 조건에 따라서 제공한다. 내가 찾아서 추린 것으로 7가지에 대해서 다루겠다.

  1. 쿼리 키 변경
  2. refetchOnWindowFocus
  3. refetchInterval
  4. staleTime 만료
  5. 사용자에 의한 명시적 재요청
  6. refetchOnMount
  7. refetchOnReconnect

1️⃣ 쿼리 키 변경

쿼리의 키가 변경되면 react-query는 알아서 refetching을 진행한다.

const { data } = useQuery(["todos", todoId], fetchTodos);

의존성 쿼리인 todoId가 변경될 시 refetching한다.

2️⃣ refetchOnWindowFocus

이는 사용자가 browser에 focus를 두었을 때 발생한다. 예를 들면 사용자가 다른 window 탭을 갔다가 다시 돌아오면 fetching이 trigger된다.

const { data } = useQuery("todos", fetchTodos, {
  refetchOnWindowFocus: true,
});

refetchOnWindowFocus: true가 기본 값으로 해제하고 싶으면 refetchOnWindowFocus: false로 바꿔주면 된다.

3️⃣ refetchInterval

정해진 시간 간격으로 data를 refetching하라고 알려줄 수 있다.

const { data } = useQuery("todos", fetchTodos, {
  refetchInterval: 60 * 1000, // 60초마다 refetching
});

refetchInterval는 기본 값은 false이기 때문에 이 기능을 활성화하려면 명시적으로 시간 값을 지정해주어야한다.

4️⃣ staleTime 만료

staleTime이 만료되면 다음에 query가 실행될 때 refetching한다. staleTime의 기본값은 0으로 data를 fetching하자 마자 오래된 데이터라고 간주한다.

const { data } = useQuery("todos", fetchTodos, {
  staleTime: 10000, 

처음 data를 fetching하고 나서 10초동안은 최신 data 10초 이후는 갱신될 필요가 있는 data로 간주됨으로서 refetching한다.

5️⃣ 사용자에 의한 명시적 재요청

사용자가 발생키니 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한다.

6️⃣ refetchOnMount

이는 query가 마운트 될 때 refetch할 지 말지에 대한 옵션으로 특정한 상황을 제외하고는 편하게 component가 마운트 될 때라고 생각하면 이해하기 쉽다. ₩component가 mount되는 시점에 코드가 실행되어 query instance가 생성되기 때문₩이다.

const { data } = useQuery("todos", fetchTodos, {
  refetchOnMount: true,
});

기본 값은 true이다.

7️⃣ refetchOnReconnect

브라우저와 재연결 되었을 때 refetch한다. 예를 들어서 오프라인 -> 온라인으로 전환될 때 실행된다.

const { data } = useQuery("todos", fetchTodos, {
  refetchOnReconnect: true,
});

기본 값은 true이다.

🔎 devtool

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

profile
step by step

0개의 댓글