React Query

5. React Query의 Status

yoxxin 2024. 2. 21. 17:48

1. 쿼리 상태와 페치 상태

React Query는 현재 상태를 알려주는 플래그를 제공한다.

내부 상태 머신에서 파생된 여러가지 boolean 플래그 값을 제공하는데,

선언된 타입을 보면 `쿼리`상태, `페치`상태가 각각 3가지씩 존재한다.

  • `쿼리`상태 : 데이터(캐시)에 대한 정보
    • `success`: 쿼리가 성공했고, 이에 대한 `data`가 쿼리캐시에 설정된다.
    • `error`: 쿼리가 실패했고, `error`가 설정된다.
    • `pending`: 쿼리캐시에 데이터가 없다.
  • `페치`상태: Fn에 대한 정보
    • `fetching`: `queryFn` 요청이 실행중이고 데이터를 기다리고 있다.
    • `paused`: `queryFn`, `mutationFn` 이 실행중이지 않고 다시 인터넷에 연결될 때 까지 일시중지된 상태이다.(offline)
    • `idle`: `~Fn`이 실행중이 아니다.

쿼리상태와 페치상태는 서로 독립적이다.

또한 각 상태 내부에서는 하나의 상태만 가질 수 있다.

즉 `fetching`이면서 `success`일 수 있고, `fetching`이면서 `error`일 수 있지만,

`pending` 이면서 `success` 일 수는 없다!

useQuery와 useMutation의 플래그

 

위는 내부 상태 머신에서 파생된 여러가지 boolean 플래그 값이다.

캐시 상태정보인 쿼리상태와 함수 상태정보인 페치상태를 잘 알고있다면 적절히 써먹을 수 있다.

 

참고로 `isLoading` 은 `isPending && isFetching`을 의미한다. (캐시가 없고, 요청중)

즉 그 쿼리의 첫번째 `fethcing` 이라는 의미이다.

2. 표준 예시

const todos = useTodos()

if (todos.isPending) {
  return 'Loading...'
}
if (todos.error) {
  return 'An error has occurred: ' + todos.error.message
}

return <div>{todos.data.map(renderTodo)}</div>

위 예시는 쿼리가 요청중인지, 에러인지를 순서대로 확인하고 데이터를 렌더링한다.

대부분의 경우 괜찮지만 React Query가 제공하는 백그라운드 페칭(refetchOnMount, refetchOnWindowFocus 그리고 refetchOnReconnect) 때문에 UX가 나빠질 수 있다.

 

어떤 문제가 발생하는지 알아보자.

3. 백그라운드 에러

많은 상황에서 백그라운드 페칭이 실패하면 조용히 무시될 수 있지만, 위 코드에서는 불가능하다.

일어날 수 있는 상황을 보자.

  • 사용자가 페이지를 열고 첫번째 쿼리가 성공한다. 이후 잠시동안 다른 탭에서 작업하다가 페이지로 다시 돌아오면 React Query가 백그라운드 페칭을 한다. 그리고 페칭이 실패한다.
  • 사용자가 아이템 목록 페이지에 들어온다. 아이템 하나를 골라 클릭한다. 쿼리가 성공하고 목록 페이지로 돌아온다. 이제 다시 그 아이템 페이지로 들어가면 캐시에 남아있는 데이터를 볼 것이다. 하지만 이때 백그라운드 페칭이 실패한다.

위 두 상황에서 쿼리의 상태는 다음과 같다.

{
  "status": "error",
  "error": { "message": "Something went wrong" },
  "data": [{ ... }]
}

보다시피 사용 가능한 오래된 데이터와 에러가 공존한다.

이는 오래된 캐싱을 사용하면서 유효성을 다시 검사하는 (stale-while-revalidate caching) 메커니즘을 이용하기 때문이다.

즉 오래된 데이터라 할지라도, 존재한다면 보여준다.

 

이제 다시 표준예시를 살펴보면..

const todos = useTodos()

if (todos.isPending) {
  return 'Loading...'
}
// 캐시가 남아있지만 백그라운드 페칭 때 에러가 발생한다.
if (todos.error) {
  return 'An error has occurred: ' + todos.error.message
}

return <div>{todos.data.map(renderTodo)}</div>

캐시가 남아있지만 백그라운드 페칭 때 데이터 대신 에러가 렌더링되는 문제가 발생한다.

 

이제 어떤 것을 우선해서 보여줄지는 우리에게 달려있다.

에러를 보여주는게 중요할까? 오래된 데이터가 있는 경우 데이터만 표시하는 것으로 충분할까?

아니면 둘 다 표시하고 에러는 토스트로 보여주면 어떨까?

 

구현은 요구사항에 따라 달라질 것이므로  이 질문에 대한 명확한 답변은 없다.

하지만 위 두가지 예시를 고려할때 렌더링 되어있는 캐시 데이터가 갑자기 에러 화면으로 바뀐다면 혼란스러운 사용자 경험을 줄 수 있을 것 같다.

 

React Query가 실패한 쿼리는 세번까지 다시 시도하므로, 에러 화면으로 대체될 때까지 몇 초가 걸릴 수 있기 때문에 더 문제가 될 수도 있다. 사용자는 그 몇 초동안 아무런 피드백을 받을 수 없기 때문이다.

 

이러한 이유로 캐시 데이터를 먼저 확인하는 방법이 있다.

const todos = useTodos()

if (todos.data) {
  return <div>{todos.data.map(renderTodo)}</div>
}

if (todos.error) {
  return 'An error has occurred: ' + todos.error.message
}

return 'Loading...'

다시 말하지만 구현은 요구사항에 따라 달라지기 때문에 "옳은 방법"은 없다.

다만 개발자로서 React Query의 공격적인 리페칭이 가져올 수 있는 결과를 알고있어야한다.

 

추가) 백그라운드 페칭 에러만 토스트로 보여주기

 

Reference

https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientisfetching

https://tkdodo.eu/blog/status-checks-in-react-query