React Query(리액트 쿼리) - useQuery, Caching, Background Refetching, Initial Data

iskkiri2024년 09월 21일
React Query
useQuery
queryKey
queryFn
Caching
Background Refetching
InitialData
React Query(리액트 쿼리) - useQuery, Caching, Background Refetching, Initial Data

이번 글에서는 리액트 쿼리의 useQuery hook에 대해서 알아보겠습니다. 

 

코드와 예제 결과는 Codesandbox에서 직접 확인할 수 있습니다.

 

 

공식 문서에 설치 및 설정 방법이 자세히 나와 있어, 이 글에서는 따로 다루지 않겠습니다. 이 예제를 구현하기 위해 API 요청은 MSW(Mock Service Worker)를 사용해 모킹했으며, 라우팅에는 react-router-dom을 활용했습니다. 다만, MSW와 react-router-dom에 대한 상세한 설명은 이 글의 범위를 벗어나므로 생략하겠습니다.

 

 

useQuery 그리고 queryKey & queryFn

 

useQuery는 React Query의 가장 기본적인 훅으로, 서버에서 데이터를 페칭하고 이를 관리하는 역할을 합니다. 이 훅은 데이터를 가져오는 동안의 로딩 상태, 에러 처리, 그리고 데이터 캐싱 등 서버 상태 관리의 복잡함을 간단하게 처리할 수 있게 도와줍니다. 

 

다음은 useQuery 기본예제의 코드를 바탕으로한 useQuery, queryKey, queryFn에 대한 설명입니다.

 

useQuery의 기본 사용 방법

 

const { data, isLoading } = useQuery({
  queryKey: ["POSTS"],
  queryFn: () => getPostsApi(),
});

 

위 코드에서 useQuery 훅은 서버에서 데이터를 가져오는 데 사용됩니다. 이 훅은 데이터를 요청하고, 그 요청의 상태와 응답을 반환합니다. 예제에서는 data와 isLoading만을 사용하였지만 useQuery는 더 많은 반환값이 존재합니다. 공식문서를 참고해주세요.

 

  • data: API 요청이 성공했을 때 반환된 데이터를 나타냅니다.
  • isLoading: 데이터가 로딩 중일 때 true를 반환합니다.

 

queryKey

 

queryKey는 캐시된 데이터를 구분하기 위한 고유 식별자 역할을 합니다. 이 키는 캐싱 및 데이터 무효화(invalidation) 시 중요한 역할을 합니다.

 

queryKey: ["POSTS"]

 

  • queryKey는 배열로 정의되며, 이 예제에서는 [“POSTS”]라는 배열을 사용하여 서버에서 받아온 게시글 데이터를 식별하고 있습니다.
  • React Query는 queryKey를 이용해 동일한 키를 가진 쿼리의 데이터를 자동으로 캐싱하고, 캐시가 만료되지 않으면 캐시된 데이터를 재사용합니다.
  • 동적인 쿼리를 할 경우, queryKey에 추가적인 값을 넣어 동적으로 구분할 수도 있습니다. 예를 들어, 페이지네이션 데이터를 다룰 때는 [ "POSTS", page ]와 같은 식으로 구분합니다.

 

queryFn

 

queryFn은 실제 데이터를 페칭하는 함수로, 서버에서 데이터를 가져오는 로직을 담당합니다.

 

queryFn: () => getPostsApi()

 

  • queryFn은 비동기 함수여야 하며, 데이터를 받아오는 방식(예: axios, fetch, graphql 등)을 정의할 수 있습니다.
  • 여기서는 getPostsApi() 함수를 호출하여 서버로부터 게시글 데이터를 가져오고 있습니다.
  • queryFn에서 반환된 데이터는 useQuery의 data에 저장되며, isLoading, error와 같은 상태도 자동으로 관리됩니다.

 

 

Caching

 

React Query의 핵심 기능 중 하나인 캐싱은, 한 번 요청한 데이터를 캐싱해두고(기본값 5분), 이후 동일한 데이터가 요청될 경우 백그라운드에서 재요청을 진행하는 동안 캐싱된 데이터를 즉시 제공함으로써 사용자 경험을 향상시킵니다. 

캐싱의 핵심은 queryKey입니다. queryKey는 데이터를 캐싱하고 관리하는데 사용되며, 동일한 queryKey로 데이터 요청이 발생할 경우, 캐시에 저장된 데이터를 사용하게 됩니다.

 

Background Refetching

 

React Query는 캐싱된 데이터를 먼저 반환한 후, 백그라운드에서 데이터를 다시 요청하여 최신 데이터를 반영하는 방식으로 동작합니다. 이를 통해 사용자는 빠르게 캐싱된 데이터를 볼 수 있으며, 백그라운드에서 최신 데이터로 업데이트가 이루어집니다. 백그라운드 리패칭을 설정하려면 staleTimerefetchOnWindowFocus 등의 옵션을 조정할 수 있습니다. 

 

stale state, fresh state와 같은 상태값은 React Query(리액트 쿼리) - state에서 예제와 함께 설명하였으므로 참고해주세요. stale과 fresh에 대해서  간단히 요약하자면 다음과 같습니다.

 

  • fresh state: staleTime 내에 데이터를 요청한 경우, 캐시된 데이터가 사용되고 백그라운드 리패칭이 발생하지 않습니다. 즉, 서버에 재요청하지 않습니다.

     

  • stale state: staleTime이 지나 stale 상태가 되면, 캐시된 데이터를 먼저 사용하되, 백그라운드에서 데이터를 다시 페칭하여 최신 상태로 업데이트합니다.

 

굳이 외울 필요 없이, stale 상태와 fresh 상태는 단어 그대로 이해하면 됩니다. Fresh는 이미 신선한 상태이므로 재요청으로 데이터를 교체할 필요가 없고, stale은 신선하지 않은 상태이기 때문에 데이터를 다시 요청하여 교체해야 한다고 생각하면 이해하기 쉽습니다.

 

 

Initial Data

 

때로는, 페이지 상세 정보를 미리 캐싱된 데이터로 보여주고 싶을 때가 있습니다. 예를 들어, 게시글 목록에서 특정 게시글의 데이터를 미리 불러왔다면, 상세 페이지에서는 해당 데이터를 즉시 보여주고, 백그라운드에서 최신 데이터를 가져올 수 있습니다. 이를 위해 initialData를 사용할 수 있습니다.

 

예제를 통해 이해하기

 

위에서 설명한 caching, background refetching, initial data의 이해를 돕기위한 예제 코드예제 실행 페이지를 바탕으로 설명하겠습니다.

 

예제 코드의 useQuery부분을 보면 다음과 같습니다.

 

  const { data: post, isLoading } = useQuery({
    queryKey: ["POSTS", { id }],
    queryFn: async () => {
      await new Promise((resolve) => setTimeout(resolve, 3000));
      return getPostDetailApi(id);
    },
    // initialData: () => {
    //   const result = queryClient.getQueryData<PaginationResponse<Post>>(["POSTS"]);
    //   const post = result?.data.find((post) => post.id === id);

    //   return post;
    // },
  });

 

다음의 순서로 동작을 확인해주세요.

 

  1. 게시글 목록 페이지에서 1번 게시글을 클릭하여 상세 페이지로 이동
  2. 3초 동안 "Loading..." 이 보이고, 상세 페이지의 내용이 보입니다.
  3. 뒤로가기
  4. 다시 1번 게시글을 클릭하여 상세 페이지로 이동
  5. 이번에는 "Loading..." 이 보이지 않고 바로 상세 페이지의 내용이 보입니다.

 

첫 요청 시에는 "Loading..."이 보였지만, 재요청시에는 "Loading..." 이 나타나지 않고 바로 상세페이지의 내용이 보이는 것에 주목해야 합니다. 이는 앞서 설명하였던 캐싱동작으로 인해 캐싱된 데이터를 보여주기 때문입니다. 

여기서 주의해야할 것은 서버에 다시 요청하지 않는 것이 아니라 백그라운드에서 데이터가 재요청 된다는 것입니다. 개발자 도구의 네트워크 탭을 보면 데이터를 재요청(background refetching)을 진행하는 것을 확인할 수 있습니다.

 

참고로 background refetching이 발생하는 이유는 react query 옵션 중 refetchOnMount 옵션의 기본값이 true이기 때문입니다. 즉, 옵션을 별도로 설정하지 않으면 컴포넌트가 새롭게 마운트가 될 경우 데이터의 재요청이 발생합니다.

 

그런데, 현재 게시물 목록 페이지에서 이미 게시물들(posts)의 데이터를 가져왔는데, 상세 페이지에서도 posts 데이터를 바로 이용할 수 있다면 로딩 창을 보여주지 않고, 데이터값을 바로 보여줌으로써 사용자 경험을 높일 수 있지 않을까요?? (물론 설명하고 있는 예제에서는 데이터의 양이 극히 적지만, 그런 상황을 가정할 경우입니다)

 

이제 주석처리가 되어 있는 initialData 부분의 주석을 해제하고 예제를 실행해보세요. 

 

  const queryClient = useQueryClient();
  
  const { data: post, isLoading } = useQuery({
    queryKey: ["POSTS", { id }],
    queryFn: async () => {
      await new Promise((resolve) => setTimeout(resolve, 3000));
      return getPostDetailApi(id);
    },
    initialData: () => {
      const result = queryClient.getQueryData<PaginationResponse<Post>>([
        "POSTS",
      ]);
      const post = result?.data.find((post) => post.id === id);

      return post;
    },
  });

 

다시 실행시키고 상세 페이지로 이동해보면 Loading... 이라는 글씨가 보이는 대신 해당 게시물의 내용이 바로 보이는 것을 확인할 수 있습니다. 이렇게 동작하는 이유는 게시글 목록에서 캐싱했던 데이터를 이용하여 initialData 값을 설정했기 때문입니다. 다시 한 번 initialData 부분만 살펴보죠.

 

    initialData: () => {
      const result = queryClient.getQueryData<PaginationResponse<Post>>([
        "POSTS",
      ]);
      const post = result?.data.find((post) => post.id === id);

      return post;
    },

 

  • ["POSTS"]: 게시글 목록 페이지에서 query key를 ["POSTS"] 로 설정하여 쿼리를 생성했었습니다.
  • queryClient.getQueryData: 이 함수는 ["POSTS"]라는 queryKey를 가진 캐시된 데이터를 가져옵니다.
  • Background fetching: 참고로 Loading이 보여지지 않을 뿐이지, background에서는 fetching이 진행되고 있습니다. 데이터가 다르다면 fetching 진행된 이후에 바뀐 데이터가 보여지게 되겠죠. 예제에서는 동일한 데이터를 사용하므로 변화가 없습니다.

 

 

여기까지 리액트 쿼리의 기본적인 useQuery, caching, background refetching, initialData에 대해서 알아보았습니다.

React Query(리액트 쿼리) - useQuery, Caching, Background Refetching, Initial Data