본문 바로가기

[TanStack Query] 중요한 기본 설정, Query 기본, Query key

출처 TanStack Query v5 공식문서
- https://tanstack.com/query/v5/docs/framework/react/guides/important-defaults
- https://tanstack.com/query/v5/docs/framework/react/guides/queries
- https://tanstack.com/query/v5/docs/framework/react/guides/query-keys

 

TanStack Query는 적극적이고 합리적인 기본 설정으로 구성되어 있습니다.

 

Tanstack Query 기본 설정

  • 기본적으로 useQuery useInfiniteQuery를 통해 조회된 쿼리 인스턴스는 캐시된 데이터를 "stale"(오래된) 상태로 간주합니다.
    • 이 동작을 변경하려면 staleTime 옵션을 사용하여 쿼리별로 또는 전역적으로 쿼리의 캐시 만료 시간을 설정할 수 있습니다. staleTime을 길게 설정하면 쿼리가 데이터를 자주 다시 가져오지 않게 됩니다.
  • 오래된(stale) 쿼리는 다음과 같은 경우 자동으로 백그라운드에서 다시 fetch됩니다:
    • 쿼리 인스턴스가 새로 마운트될 때
    • 윈도우가 다시 포커스될 때
    • 네트워크가 재연결될 때
    • 쿼리에 refetch interval이 설정되어 있을 때
    이 동작을 변경하려면 refetchOnMount, refetchOnWindowFocus, refetchOnReconnect, refetchInterval과 같은 옵션을 사용할 수 있습니다.
  • 활성 인스턴스가 없는 쿼리 결과는 "비활성(inactive)" 상태로 표시되며, 나중에 다시 사용될 수 있도록 캐시에 남습니다. 
    • 기본적으로 "비활성" 쿼리는 5분 후에 가비지 컬렉션(GC)됩니다.
    • 이 동작을 변경하려면 쿼리의 기본 gcTime을 1000 * 60 * 5 밀리초 외에 다른 값으로 설정할 수 있습니다.
  • 쿼리가 실패하면 기본적으로 3번까지 자동으로 재시도하며, 지수 백오프(exponential backoff) 방식으로 지연 시간이 적용됩니다.
    • 에러가 UI에 표시되기 전에 재시도가 이루어집니다. 이 동작을 변경하려면 retry와 retryDelay 옵션을 사용하여 기본값인 3번과 지수 백오프 함수를 변경할 수 있습니다.
  • 쿼리 결과는 기본적으로 구조적으로 공유되어 데이터가 실제로 변경되었는지 감지합니다.
    • 데이터가 변경되지 않으면 데이터 참조는 변경되지 않으며, 이는 useMemo와 useCallback을 사용할 때 값 안정성에 도움을 줍니다. 이 개념이 생소하게 느껴지더라도 걱정하지 마세요! 99.9%의 경우 이 기능을 비활성화할 필요는 없으며, 앱 성능에 아무런 비용을 들이지 않고 성능을 개선합니다.
    • 구조적 공유는 JSON 호환 값에 대해서만 작동하며, 다른 값 유형은 항상 변경된 것으로 간주됩니다. 예를 들어, 큰 응답으로 인한 성능 문제가 발생한다면 config.structuralSharing 플래그를 사용하여 이 기능을 비활성화할 수 있습니다. 만약 비JSON 호환 값을 다루고 있으며 데이터가 변경되었는지 감지하고 싶다면, config.structuralSharing에 사용자 정의 함수를 제공하여 이전과 새로운 응답을 비교하는 값을 계산하여 참조를 유지할 수 있습니다.

Query 기본 개념

Query는 unique key에 묶여 있는 비동기 데이터 소스에 대한 선언적인 의존성입니다.

Query는 서버로부터 데이터를 fetch하기 위해 GET이나 POST 메서드를 포함하여 모든 Promise 기반의 메서드와 함께 사용될 수 있습니다. 만약 당신의 메서드가 서버에 있는 데이터를 변경한다면, Mutation을 사용할 것을 권장합니다.

 

당신의 컴포넌트나 커스텀 훅에서 Query에 구독하기 위해서는 useQuery 훅을 호출하세요. 다음과 같은 것들이 적어도 포함되어 있어야 합니다.

  • 쿼리를 위한 unique key
  • 데이터를 resolve하거나 에러를 throw하는 promise를 리턴하는 함수
import { useQuery } from '@tanstack/react-query'

function App() {
  const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}

 

당신이 useQuery에 제공하는 unique key는 내부적으로 refetching하고 캐싱하고, 전체 애플리케이션에서 당신의 query를 공유하는 데 사용됩니다.

useQuery에 의해 리턴되는 query 결과는 당신이 데이터를 활용하는 데 필요한, 쿼리에 대한 모든 정보를 포함하고 있습니다.

const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })

 

result 객체는 당신이 알아야 할 몇 가지 아주 중요한 상태들을 가지고 있습니다. query는 언제나 다음 중 하나의 state에 있습니다.

  • isPending 또는 status === 'pending'
    • query가 아직 데이터가 없는 상태입니다.
  • isError 또는 status === 'error'
    • query가 에러를 마주한 상태입니다.
  • isSuccess 또는 status === 'success'
    • query가 성공적이가 데이터가 사용 가능한 상태입니다.

query의 상태를 통해 더 많은 정보에 접근할 수 있습니다.

  • error
    • 만약 query가 isError 상태라면, error 속성을 통해 error에 접근할 수 있습니다.
  • data
    • 만약 query가 isSuccess 상태라면, data 속성을 통해 data에 접근할 수 있습니다.
  • isFetching
    • 어느 상태에서나, 만약 query가 fetching을 하고 있다면 (백그라운드 refetching을 포함해서) isFetching은 true일 것입니다.

대부분의 query들의 경우  isPending 상태를 확인한 다음 isError 상태를 확인한 다음 마침내 data가 접근 가능한 것으로 간주하고 성공적인 상태를 렌더링하는 것이 일반적입니다.

function Todos() {
  const { isPending, isError, data, error } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodoList,
  })

  if (isPending) {
    return <span>Loading...</span>
  }

  if (isError) {
    return <span>Error: {error.message}</span>
  }

  // We can assume by this point that `isSuccess === true`
  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

 

만약 불리언 값들이 당신이 생각하는 불리언 값이 아니라면 status 상태를 이용할 수도 있습니다.

function Todos() {
  const { status, data, error } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodoList,
  })

  if (status === 'pending') {
    return <span>Loading...</span>
  }

  if (status === 'error') {
    return <span>Error: {error.message}</span>
  }

  // also status === 'success', but "else" logic works, too
  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

 

당신이 data에 접근하기 전에 pending과 error를 체크한다면 Typescript 또한 data의 타입을 정확하게 좁혀줄 것입니다.

 

FetchStatus

 

status 필드에 더해서 당신은 또한 fetchStatus라는 속성도 얻게 됩니다. 이 속성은 다음과 같은 옵션을 가지고 있습니다.

  • fetchStatus == 'fetching'
    • query가 현재 fetching 중입니다.
  • fetchStatus === 'paused'
    • query가 fetch를 하고 싶지만 중단됐습니다.
  • fetchStatus === 'idle'
    • query가 현재 아무것도 하고 있지 않습니다.

status 필드와 fetchStatus 필드 두 가지 상태를 사용하는 이유는 무엇일까요?

  • status는 데이터에 대한 정보를 제공합니다: 데이터가 있나요, 없나요?
  • fetchStatus는 queryFn에 대한 정보를 제공합니다: 실행 중인가요, 아닌가요?

여러 가능한 상황들은 다음과 같습니다.

  • success 상태의 쿼리는 일반적으로 fetchStatus가 idle입니다.
    • 하지만 백그라운드에서 refetch가 진행 중이라면 fetchStatus가 fetching 상태일 수도 있습니다.
  • 데이터가 없는 쿼리가 마운트될 경우 일반적으로 status는 pending 상태, fetchStatus는 fetching 상태를 가집니다.
    • 하지만 네트워크 연결이 없다면 fetchStatus가 paused 상태일 수도 있습니다.

따라서 쿼리가 데이터를 실제로 가져오지 않고도 status가 pending 상태일 수 있다는 점을 유념하세요.


Query key

TanStack Query는 Query Key를 기반으로 쿼리 캐싱을 관리합니다.

Query Key는 최상위 수준에서 배열 형태로 지정해야 하며, 단일 문자열로 구성된 배열처럼 간단할 수도 있고, 여러 문자열과 중첩된 객체로 이루어진 배열처럼 복잡할 수도 있습니다. Query key가 직렬화 가능하고 쿼리 데이터에 대해 고유하기만 하다면 그 Query key를 사용할 수 있습니다.

 

간단한 Query Key

key의 가장 심플한 형태는 상수 값으로 이루어진 배열 array이다. 이러한 형태는 다음과 같은 상황에서 유용하다.

  • 제네릭 리스트/인덱스 리소스
  • 비-계층적인 리소스
// A list of todos
useQuery({ queryKey: ['todos'], ... })

// Something else, whatever!
useQuery({ queryKey: ['something', 'special'], ... })

 

변수들을 가진 배열 key

만약 query가 그것의 데이터를 고유하기 묘사하기 위해 더 많은 데이터가 필요하다면, 당신은 문자열과 직렬화 가능한 객체 여러 개를 포함한 배열을 사용할 수 있습니다.이는 다음과 같은 경우에 유용합니다.

  • 계층적인 또는 중첩된 리소스
    • ID, 인덱스, 또는 기타 원시 값을 전달하여 항목을 고유하게 식별하는 것이 일반적입니다.
  • 추가 매개변수가 있는 쿼리
    • 추가 옵션 객체를 전달하는 것이 일반적입니다.
// An individual todo
useQuery({ queryKey: ['todo', 5], ... })

// An individual todo in a "preview" format
useQuery({ queryKey: ['todo', 5, { preview: true }], ...})

// A list of todos that are "done"
useQuery({ queryKey: ['todos', { type: 'done' }], ... })

 

Query Key는 해시 처리됩니다. 

이는 객체 내부에서 키의 순서와 상관없이 다음과 같은 모든 쿼리가 동일한 것으로 간주된다는 것을 의미합니다.

useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})
useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... })

 

하지만 배열의 요소 순서는 상관이 있습니다. 다음 Query Key들은 동일하지 않습니다.

useQuery({ queryKey: ['todos', status, page], ... })
useQuery({ queryKey: ['todos', page, status], ...})
useQuery({ queryKey: ['todos', undefined, page, status], ...})

 

만약 당신의 query function이 변수에 의존하고 있다면, 그것을 Query Key에 포함시키세요

Query Key는 그것이 fetching하는 데이터를 고유하게 설명하기 때문에, 이 key는 당신의 query function에서 변화하는 변수를 포함시켜야 합니다.

function Todos({ todoId }) {
  const result = useQuery({
    queryKey: ['todos', todoId],
    queryFn: () => fetchTodoById(todoId),
  })
}

 

query key는 당신의 query function에 대해 의존성처럼 동작합니다. 당신의 query key에 의존적인 변수를 추가하는 것은 query가 독립적으로 캐시되는 것을 보장하고 변수가 변화할 때 query는 자동적으로 refetch될 것입니다. (당신의 staleTime 세팅에 따라서요)

 


소감

프로젝트를 할 때 query key 캐싱 때문에 버그가 발생했던 적이 있었다. 한 사용자가 로그아웃을 하고 다른 사용자가 로그인을 했는데도 마이페이지에서는 예전 사용자의 정보를 불러오고 있었던 문제였다. 이는 로그아웃을 할 때 query key 무효화를 해줌으로써 해결할 수 있었다. query key의 캐싱이 정말 잘 동작한다는 것을 알 수 있었던 경험이었다 ㅎㅎ

'React' 카테고리의 다른 글

[TanStack Query] 개요  (1) 2025.01.01