React Query - 쿼리생성, 에러/로딩 상태

박정호·2023년 1월 11일
1

React Query

목록 보기
2/14
post-thumbnail

🚀 Start

먼저 React Query를 사용하기 위해서 해야할 것들이 있다.

query client 생성하기

  • 쿼리 및 캐시를 관리하는 클라이언트
// App.jsx

import {QueryClient } from 'react-query'

const queryClient = new QueryClient();

QueryProvider 적용하기

  • 자녀 컴포넌트에 캐시와 클라이언트 구성을 제공

  • 쿼리 클라이언트를 값으로 사용

    • 클라이언트가 가지고 있는 캐시와 모든 기본 옵션을 자녀 컴포넌트도 사용 가능
// App.jsx
import { QueryClientProvider } from 'react-query'

 <QueryClientProvider client={queryClient}>
   	  //자녀 컴포넌트
      <div className="App">
            <h1>Blog Posts</h1>
        <Posts />
      </div>
 </QueryClientProvider>


⭐️ useQuery

그럼 이제 useQuery를 실행하여 서버에서 데이터를 가져와보자.

useQuery는 서버의 값을 가져오는 훅으로, 다양한 속성을 가진 개체를 반환한다.

그리고 useQuery는 몇가지 인수를 사용한다.

첫번째는 쿼리키이다. 그리고 두번째로 쿼리함수이다. 쿼리함수는 쿼리에 대한 데이터를 가져오는 방법을 말한다. 이때 사용하는 함수는 데이터를 가져오는 비동기 함수여야한다.

// Posts.jsx

async function fetchPosts() {
  const response = await fetch(
    "https://jsonplaceholder.typicode.com/posts?_limit=10&_page=0"
  );
  return response.json();
}

const {data} = useQuery("posts", fetchPosts);

한가지 주의할 점!

앞서 말했듯이 비동기함수에 의해 데이터가 비동기적으로 들어오고 있기 때문에 배열을 출력하는 map함수가 Error가 발생한다. 쉽게 말해 이터러블한 배열에 대해서만 map이 사용되아야하는데 아직 데이터가 들어오고 있다보니 Error가 나는 것이다.

따라서, 비동기함수가 완전히 해결될 때까지는 data는 거짓인 것이고, 그에 대한 부분을 정의해줘야 한다.

if(!data) return <div>데이터 가져오는중</div>

💡 {JSON} Placeholder
테스트 및 프로토타이핑을 위한 무료 가짜 API로, JSON 서버 + LowDB로 구동되어 임시 데이터를 가져오도록 하자.



👉 로딩,에러 상태 처리

아까 말했듯이 useQuery에는 다양한 속성들이 존재한다. (공식문서)

이 중 로딩,에러 상태를 처리하는 속성을 통해 데이터가 정의되지 않으면 오류가 되게 두지 않고 적절한 조치를 취할 수 있다.

  • isLoading
  • isError

이 둘은 데이터가 로딩 중인지, 데이터를 가져올 때 오류인지 여부를 알려주는 boolean 값이다.

isLoading

아까 데이터의 유무를 정의했듯이 다음과 같이 작성할 수 있다.


  const {data, isLoading} = useQuery("posts", fetchPosts);

  if(isLoading) return <div>데이터 가져오는중</div>

💡 isFetching VS isLoading

isFetching은 비동기 쿼리가 해결되지 않음을 뜻한다. 이 경우에는 아직 패칭을 완료하지 않았다는 의미이지만, 쿼리가 Axios호출 또는 GraphQL 호출할 수 있다.

반면 , isLoading은 이에 대한 하위 개념으로 가져오는 상태를 의미한다. 즉, 쿼리함수가 아직 해결되지 않은 것이다. 또한 캐시된 데이터도 존재하지 않는다.

이렇게 말해도 별차이 없어 보이지만, pagination을 진행할때는 캐시 된 데이터가 있을 때와 없을 때를 구분해야 할 때 차이를 느낄 수 있을 것 같다.


isError

만약 쿼리함수에서 오류가 발생하면 데이터도 얻지 못할 것이다. 따라서, 데이터가 아예 반환되지 않기 때문에 그에 대한 조기 return이 필요할 것이다.


  const {data, isError} = useQuery("posts", fetchPosts);

  if(isError) return <div>데이터를 가져오지 못했습니다.</div>

그리고 만약 어떤 Error가 발생하였는지를 확인하고 싶다면, error 속성을 사용하자


  const {data, isError, error} = useQuery("posts", fetchPosts);

  if(isError) return <div>데이터를 가져오지 못했습니다. 문제는 {error.toString()}</div>

💡 참고로 onError callback을 통해서 더 깔끔하게 에러차리 가능하다.



⏰ Stale Time / Cache Time

Stale Data(만료된 데이터) 란?

React Query에서 데이터가 만료됐다는 것은 무슨 뜻일까?
데이터 리패칭은 만료된 데이터에서만 실행된다. 데이터 리패칭 실행에는 만료된 데이터 이외에도 여러 트리거들이 있다. 예를 들어 컴포넌트가 remount 되는 것, 윈도우가 refocus 되는 것이다.

다시 말하지만, 데이터 리패칭은 만료된 데이터의 경우에서 발생하고 나머지 트리거들은 부가적인 것들이다.

Stale Time은 데이터를 허용하는 '최대나이'라고 할 수 있다. 즉, 만료됐다고 여겨지기 전까지의 허용되는 시간들을 말한다. (Like 유통기한)

만약 웹사이트에 표시된 데이터가 10초까지는 그대로여도 괜찮다면 StaleTime 10초로 설정할 것이다. 그리고 그 이후로는 Stale한 상태로 데이터가 유지될 것인데, 만약 업데이트를 하고 싶다면?

useQuery를 호출할 때 세번째 인자로 들어가는 옵션 값을 설정하는 것이다.

업데이트할 옵션은 StaleTime이고 1/1000초 단위이다.

const {data, isError, isLoading, error} = useQuery("posts", fetchPosts, {
  staleTime : 2000
});

왜 Stale Time의 기본값은 0일까?

StaleTime을 기본값으로 0을 설정했다는 말은 늘 만료상태라는 뜻이고 늘 리패칭을 하여 서버에서 다시 가져온다는 뜻이다. 즉, 실수로라도 클라이언트에게 만료된 데이터를 제공할 일을 없앨 수 있기 때문이다.

관점을 '어떻게 하면 업데이트를 할 수 있죠'가 아닌 '어떻게 하면 늘 데이터를 최신 상태로 유지할 수 있죠'로 바라보면 될 것 같다.

그래서 Cache Time은 뭔데?

캐시 구조에 저장된 데이터는 메모리상에 존재하게 된다. 데이터가 inactive 상태일 때 캐싱된 상태로 남아있는 시간이다.

쿼리 인스턴스가 unmount 되면 데이터는 inactive 상태로 변경되며, 캐시는 cacheTime만큼 유지된다.
cacheTime이 지나면 가비지 콜렉터로 수집되고 cacheTime이 지나기 전에 쿼리 인스턴스가 다시 마운트 되면, 데이터를 fetch하는 동안 캐시 데이터를 보여준다.

cacheTime은 staleTime과 관계없이, 무조건 inactive 된 시점을 기준으로 캐시 데이터 삭제를 결정한다.

💡 참고하자
👉 React Query: cacheTime vs staleTime



Query Key

예를 먼저 하나 들어보자.

각각의 게시글에 따른 댓글 데이터들이 있다. 그럼 이번에는 쿼리함수에 해당 데이터의 ID값을 파라미터로 주어 각각 다른 데이터에 대한 요청이 필요할 것이다.

// postDetail.jsx
async function fetchComments(postId) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/comments?postId=${postId}`
  );
  return response.json();
}

...

 const {data, isError, isLoading , error} = 
   useQuery('comments',  () => fetchComments(post.id));

if(isLoading) return <div>댓글 가져오는중</div>
if(isError) return <div>댓글을 가져오지 못했습니다. 문제는 {error.toString()}</div>

하지만, 첫번째 게시글에 대한 댓글만 모든 게시글에 출력되고 각각의 댓글이 출력되지 않는다.

모든 쿼리가 comments 쿼리 키를 동일하게 사용하고 있기 때문이다.

위와 같이 comments 같이 알려진 쿼리 키가 있을 때는 어떠한 트리거가 있어야만 데이터를 다시 가져오게 된다.

트리거 예시

  • 컴포넌트 remount
  • 윈도우 refocus
  • 수동으로 리패칭 실행
  • 지정된 간격으로 자동 리패칭 실행
  • Mutation 생성 뒤 쿼리 무효화시

즉, 게시글의 제목을 클릭했을 때 위와 같은 트리거는 일어나지 않는다. 따라서, 데이터가 만료되었지만, 리패칭은 이루어지지 않았던 것이다.

그렇다면 어떻게?

블로그 게시물 제목을 클릭할 때마다 데이터를 무효화시켜서 데이터를 다시 가져오게 만들 수 있는데 이 방법은 그렇게 좋은 방법은 아니다.

따라서, 각각 게시글에 대한 캐시 데이터를 활용하는 것이 좋다.
쿼리는 게시글 ID를 포함하기 때문에 쿼리별로 캐시를 남길 수 있으며 comments 쿼리에 대한 캐시를 공유하지 않고 각 쿼리에 해당하는 캐시를 가질 수 있다.

그러기 위해서는 각 게시물에 대한 쿼리에 라벨을 설정하면 된다. 방법은 쿼리 키에 문자열 대신 배열을 전달하면 가능하다. 그러 쿼리 키를 쿼리에 대한 의존성 배열로 취급하고 쿼리 키가 변경되면 React Query가 새 쿼리를 생성해서 완전히 다른 값으로 간주하는 것이다.

const {data, isError, isLoading , error} = 
useQuery(['comments', post.id],  () => fetchComments(post.id));



🛠 DevTools

개발자 도구는 앱에 추가할 수 있는 컴포넌트 형식으로, 개발 중인 모든 쿼리의 상태를 표시해준다. 또한 예상대로 작동하지 않는 경우 문제를 해결하는데 도움이 된다. (Devtools 공식문서)

  • 쿼리키로 쿼리를 표시

    • 활성(active), 비활성(inactive), 만료(stale) 등 모든 쿼리 상태를 알려줌
    • 업데이트된 타임스탬프도 알려줌
  • 데이터 탐색기

    • 쿼리에 의해 반환된 데이터를 확인 가능
  • 쿼리 탐색기

    • 쿼리를 확인 가능

사용

import { ReactQueryDevtools} from 'react-query/devtools'

...

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      ...
      <ReactQueryDevtools />
    </QueryClientProvider>   
  );
}

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글