[React-Query] 데이터와 쿼리 상태를 분리하기: isLoading vs isFetching

Gyuwon Lee·2023년 5월 16일
6
post-thumbnail

useQuery return always isLoading with true when enabled is false

문제: 로딩이 안끝나요

react query를 사용하면 다른 비동기 데이터에 의존하여 API 요청을 보내는 dependent query 작성이 용이한데, 이 때 사용하는 enabled 값이 false 이면 IsLoading이 계속 true인 문제가 발생했다.

enabled 가 true 인 경우 해당 query 의 isLoading 이 true인 동안 기다린 뒤 UI가 보여야 하는데,

if (isLoading) return <Skeleton />

그냥 이렇게 작성한 경우 enabled가 false일 때 영원히 로딩 UI만 보이게 된다.

Why two different states?

Queries | TanStack Query Docs
isLoading or status === 'loading' - The query has no data yet”

공식문서를 읽어보면, isLoading은 '쿼리가 아무 데이터도 캐싱하고 있지 않은 경우'를 나타낸다고 생각할 수 있을 것 같다. enabled: false 인 경우 쿼리를 보내지 않았으므로 당연히 데이터가 비어 있고, isLoading이 true로 남아 있는 것이 의도된 작동인 듯하다.

내가 의도했던 것처럼 dependent query 에서 데이터를 요청했고, 이를 기다리는 중인지를 알려면 isFetching을 사용해야 한다.

The status gives information about the data: Do we have any or not?
The fetchStatus gives information about the queryFn: Is it running or not?

쿼리가 쓸 만한(요청에 성공해서 받아온) 데이터를 들고 있는지를 status로, 쿼리의 요청 상태는 fetchStatus로 알 수 있는 것이다.

이렇게 데이터의 유무 / fetching 작동 여부 로 상태를 나누어 놓은 것은, react query의 핵심 기능인 background refetch와 stale-while-revalidate 로직이 두 가지 상태 모두를 조합해야 명확하게 구분될 수 있기 때문이다.

Stale While Revalidate

background refetch 와 stale-while-revalidate 는 별개의 두 가지 feature 가 아니다.

React Query는 데이터를 캐싱하고, 해당 데이터가 필요할 때 더 이상 최신 상태가 아니더라도(stale) 데이터를 제공한다.오래된 데이터가 없는 것보다는 낫다는 것. 왜냐하면 데이터가 없다는 것은 대개 Loading Spinner의 표시를 의미하며, 이는 사용자들에 의해 “느린” 것으로 인식될 것이기 때문이다.

따라서 오래된 데이터를 보여줌과 동시에 background refetch를 수행하여 해당 데이터를 다시 검증(=stale-while-revalidate)한다.

  • 예를 들어, 현재 fetching에 성공한 올바른 데이터가 캐싱되어 있는 경우는 오직 status === success 일 때 뿐이다. 즉, isLoading 이 true 인 경우(= status === loading)에는 쿼리에 아무 데이터도 없는 상황으로 해석된다.

  • 이는 데이터가 stale 한 것과는 다르다. 어쨌든 가장 최근의 query가 성공해서 데이터를 들고 있다면, status는 success 상태여야 한다. status는 데이터의 상태이기 때문이다.

  • 그런데 데이터를 보여주고 있는 와중에 해당 데이터가 stale한지 revalidate를 수행하고 싶다면(background refetch)?

  • status 하나만으로 관리하는 경우, 이 refetch에 대해서도 status === loading으로 변경되어야 할 것이다.

  • 그러면 기껏 캐싱된 데이터를 들고 있는 의미가 사라진다. 어차피 loading 인 동안에는 일반적으로 데이터가 없는 것으로 간주하고 로딩 UI를 보여주기 때문이다.

  • 즉 데이터가 아예 없는 경우와, 데이터가 있지만 오래되어 유효한지 검증 중인 경우를 구분할 수 없게 된다.

  • 따라서 background refetch를 포함해 쿼리의 요청 상태를 나타내기 위한 fetchStatus가 필요하다. status(loading)로 사용자에게 보여줄 수 있는 멀쩡한 데이터를 들고 있는지 판단하고, fetchStatus(fetching)로 데이터 업데이트를 시도 중인지 판단할 수 있게 된다.

Query state in dependent queries

내가 마주친 케이스 (enabled를 사용한 dependent queries) 를 다시 React-Query 스럽게 짚어 보았다.

1. 컴포넌트 최초 마운트~ :
- status : 두 쿼리 모두 데이터가 없다. 모두 isLoading 상태다.
- fetchStatus : 첫 번째 쿼리는 마운트 이후 isFetching 상태가 된다. 두 번째 쿼리는 아직 idle이다.
2. 첫 번째 쿼리의 fetching 완료, 두 번째 쿼리의 enabled 값 결정
- status : 첫 번째 쿼리가 데이터를 성공적으로 가져왔다면 success 상태가 된다(= isLoading === false). 이에 기반하여 두 번째 쿼리의 enabled 값이 결정된다. 하지만 여전히 isLoading 상태다.
- fetchStatus : enabled === true ? isFetching : idle
3. 두 번째 쿼리의 fetching 완료
- status : 여기서 비로소 두 쿼리 모두 status === success 상태가 된다.
- fetchStatus : 두 쿼리 모두 fetchStatus === idle 상태가 된다.
4. 어느 쿼리가 background refetch되는 경우
- status : 두 쿼리 모두 status === success 상태다. (isLoading === false)
- fetchStatus : refetch되는 쿼리의 fetchStatus가 fetching으로 변경되었다가 idle(or error)이 된다.

따라서 내 원래 의도대로

  • 두 쿼리 모두 요청이 갔을 때 (enabled === true) : 두 번째 쿼리의 데이터가 들어올 때까지 로딩 UI
  • 처음 쿼리만 요청되었을 때 (enabled === false) : 첫 번째 쿼리의 데이터가 들어올 때까지만 로딩 UI

이렇게 구현하기 위해서는 조건을 나눠야만 했던 것이다.

  • enabled 조건으로 사용한 값을 이용해 로딩 UI 역시 위처럼 조건을 나누거나,
  • 각 쿼리의 isLoading, isFetching 값을 조합해 조건을 작성할 수도 있을 것이다.

isInitialLoading

Lazy queries will be in status: 'loading' right from the start because loading means that there is no data yet. It also means you likely cannot use this flag to show a loading spinner.

이 글을 작성하며 리액트 쿼리 공식문서를 다시 보니, isLoading 값을 loading flag로 사용할 수 없다고 명시해두었다.

그 대신, isLoading && isFetching 과 동일한 isInitialLoading 값을 사용하면 된다. 캐시된 데이터가 없는(isLoading) 쿼리를 요청(isFetching)하면 isInitialLoading === true 가 된다.

다만 cacheTime과 staleTime의 default value가 각각 5분, 0초라는 점을 고려하자. staleTime을 따로 설정하지 않으면 데이터는 캐싱된 직후 stale한 값이 된다. 따라서 매번 refetch된다. 하지만, cache는 사라지지 않고, 갱신될 뿐이다. 즉, 최초 요청 이후 최대 5분 간격으로 계속 refetch하는 동안에는 계속 캐시 데이터를 갖고 있게 된다. 즉, refetch하는 동안 isLoading은 계속 false다.

따라서 refetch될 때마다 Loading indicator를 보여주고 싶다면 isInitialLoading 역시 적절한 방법은 아니다. (물론 is 'initial' loading 이라는 변수명에서부터 이미 알 수 있다.)

TL;DR

원래 어디선가 isLoading은 최초의 데이터 요청, isFetching은 그 이후의 데이터 요청 상태를 알기 위해 사용된다는 글을 읽고 그렇게 이해하고 있었는데, 뜯어보니 단순히 이렇게 이해하는 것과는 조금 다른 개념이었다.

그보다는 위에 적은 대로 status 는 데이터의 상태, fetchStatus 는 쿼리의 요청 상태로 이해하고 loading과 fetching은 각각 위 상태의 일부라고 생각하는 것이 정확한 듯하다.

stale한 데이터를 갱신하는 요청은 isFetching, 모종의 이유로 (아직 호출된 적 없음 / cacheTime over / invalidateQuery ...) 캐시가 없는 쿼리의 요청은 isLoading && isFetching 이다.

profile
하루가 모여 역사가 된다

0개의 댓글