본문 바로가기

React

Tanstack-Query(React-Query)

React-Query란?

서버로부터 데이터 fetching, caching, 서버 데이터와의 동기화 및 업데이트 등 데이터를 효율적으로 관리할 수 있도록 하는 라이브러리

 

** React-query v4부터는 패키지 이름이 @tanstack/react-query로 변경되면서 React에만 국한되지 않은 범용 프론트엔드 라이브러리로 개선되었다.

 

 

왜 필요하나?

 

대표적인 기능은 다음과 같다.

- 데이터 가져오기 및 캐싱

- 동일 요청의 중복 방지

- 자동 새로고침을 통해 데이터 최신 상태 유지

- 성능 최적화: 무한스크롤, 페이지네이션 등

 

사용 방법

 

아래는 공식문서에 있는 예시 코드이다.

세가지 핵심 개념인 Queries, Mutations, Query Invalidation 정도만 알면

React-Query를 바로 시작해볼 수 있다.

 

해당 개념을 위주로 코드를 훑어보며 감을 잡아보자.

import {
  useQuery,
  useMutation,
  useQueryClient,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
import { getTodos, postTodo } from '../my-api'

// Create a client
const queryClient = new QueryClient()

function App() {
  return (
    // Provide the client to your App
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  )
}

function Todos() {
  // Access the client
  const queryClient = useQueryClient()

  // Queries
  const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })

  // Mutations
  const mutation = useMutation({
    mutationFn: postTodo,
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

  return (
    <div>
      <ul>{query.data?.map((todo) => <li key={todo.id}>{todo.title}</li>)}</ul>

      <button
        onClick={() => {
          mutation.mutate({
            id: Date.now(),
            title: 'Do Laundry',
          })
        }}
      >
        Add Todo
      </button>
    </div>
  )
}

render(<App />, document.getElementById('root'))


Queries

쿼리란 서버에서 데이터를 가져오는 방법을 정리한 약속이라고 할 수 있다.

각각의 쿼리는 고유한 키를 가지고 있고, Promise 기반의 메소드로 데이터를 받아온다. 

import { useQuery } from '@tanstack/react-query'

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

 

fetchTodoList를 통해 데이터를 서버에서 받아오고,

해당 데이터에 todos라는 key를 붙여, 데이터 재사용 및 캐싱, refetching에 용이해진다.

 

  • 첫 번째 파라미터에 들어가는 배열의 첫 요소는 unique key로 사용되고,
    두 번째 요소부터는 query 함수 내부의 파라미터로 값들이 전달된다.
  • 두 번째 파라미터로 실제 호출하고자 하는 비동기 함수가 들어간다. 이때 함수는 Promise를 반환하는 형태여야 한다
  • 최종 반환 값은 API의 성공, 실패 여부, 반환값을 포함한 객체이다

 

쿼리는 한번에 하나의 상태만 가질 수 있는데, 다음 중 하나이다.

 

  • isPending (status == 'pending)
    - 아직 데이터를 가져오지 못한 상태
    - 데이터 요청 중 로딩 화면에 사용
    if (isPending) return <div>Loading...</div>;
  • isError (status == 'error')
    - 요청 중 에러가 발생한 상태

  • isSuccess (status == 'success')
    - 데이터 요청이 성공적으로 완료된 상태

이 외에도 보조적인 상태 정보 제공:

  • error: isError 상태일 때 에러 메시지를 담고 있음
  • data: isSuccess 상태일 때 받아온 데이터를 담고 있음
  • isFetching: 데이터가 백그라운드에서 갱신(refetch) 중인지 여부를 나타냄 (언제든 true가 될 수 있음)
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>
  )
}

 

 

Mutation

데이터를 update/delete/create 할 때, 또는 사이드 이펙트 처리 시사용

  const mutation = useMutation({
    mutationFn: (newTodo) => {
      return axios.post('/todos', newTodo)
    },
  })
  • 반환값은 useQuery와 같고 첫번째 파라미터에 비동기 함수가 들어가고, 두번째 파라미터에 상황별 분기 설정이 들어간다.
  • 실제 사용 시에는 mutation.mutate 메서드를 사용하고, 첫 번째 인자로 API 호출 시에 전달해야하는 데이터를 넣어주면 된다

뮤테이션은 다음과 같은 상태를 가질 수 있다.

 

  • isIdle(status === 'idle')
    뮤테이션이 대기 중이거나 초기화된 상태

  • isPending(status === 'pending)
    뮤테이션이 현재 실행 중인 상태

  • isError(status==='error')
    뮤테이션 실행 중 오류가 발생한 상태
  • isSuccess(status='success')
    뮤테이션이 성공적으로 완료되어 데이터를 사용할 수 있는 상태

 

Invalidation

쿼리를 강제로 stale(만료) 상태로 만들고 필요시 자동으로 다시 가져오게 하는 queryClient의 메서드

// 모든 쿼리 무효화
queryClient.invalidateQueries()
// 키값이 `todos`인 쿼리 무효화
queryClient.invalidateQueries({ queryKey: ['todos'] })

 

  • 쿼리를 stale 상태로 표시, 이때 기존의 staleTime 설정은 무시된다.
  • 화면에 렌더링 중인 쿼리는 자동으로 백그라운드에서 refetch
  • queryKey, exact, predicate 등 다양한 옵션으로 원하는 쿼리만 세밀하게 무효화할 수 있다.

 

주의 사항 (중요)

시간이 없어 gpt의 공식 문서 정리글로 대체합니다.. (수정 예정)

🚀 TanStack Query의 기본 동작 방식과 설정 변경 방법

TanStack Query는 기본적으로 공격적이지만 합리적인(defaults) 설정을 가지고 있어요.
이 설정을 잘 이해하면 원치 않는 동작을 방지하고, 더 효율적으로 활용할 수 있어요!


1. 기본적으로 캐시된 데이터는 'stale(오래됨)' 상태로 간주됨

  • useQuery나 useInfiniteQuery를 사용할 때,
    캐시된 데이터라도 기본적으로 "오래된(stale)" 상태로 간주돼서 자동으로 다시 가져오려 해요.
  • 이를 변경하려면 staleTime 옵션을 조정하면 돼요!
    • staleTime을 길게 설정하면, 쿼리가 데이터를 자주 새로 가져오지 않음
    • staleTime: Infinity로 설정하면, 데이터를 항상 최신이라고 간주(재요청 X)
const { data } = useQuery(['posts'], fetchPosts, { staleTime: 1000 * 60 * 5 }); // 5분 동안은 새로 안 가져옴

2. 쿼리는 특정 조건에서 자동으로 다시 요청됨

다음 상황이 발생하면 자동으로 데이터를 다시 가져옴(refetch):

  1. 새로운 useQuery 인스턴스가 마운트될 때
  2. 사용자가 창을 다시 포커스했을 때 (ex. 다른 탭 갔다가 돌아옴)
  3. 네트워크가 재연결되었을 때 (ex. Wi-Fi 다시 연결)
  4. refetchInterval을 설정한 경우 주기적으로 데이터 요청

👉 이 기능을 비활성화하거나 변경하려면?

  • refetchOnMount, refetchOnWindowFocus, refetchOnReconnect, refetchInterval 옵션을 조정하면 돼요!
const { data } = useQuery(['posts'], fetchPosts, { 
  refetchOnMount: false,  // 마운트될 때 다시 요청 안 함
  refetchOnWindowFocus: false,  // 창을 다시 봐도 요청 안 함
  refetchOnReconnect: true,  // 네트워크 연결 시 다시 요청 (기본값)
  refetchInterval: 1000 * 60 // 1분마다 자동 새로고침
});

3. 사용되지 않는 쿼리는 '비활성(inactive)' 상태가 됨

  • useQuery를 사용하지 않으면 자동으로 캐시에 저장된 채 '비활성 상태'로 전환됨
  • 이 데이터는 5분 동안 유지됨 (기본값: gcTime = 1000 * 60 * 5)
  • 5분이 지나면 자동으로 캐시에서 삭제됨(GC, Garbage Collection)

👉 더 오래 유지하거나 즉시 삭제하고 싶다면?

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 10, // 10분 동안 캐시 유지
    },
  },
});

4. 실패한 쿼리는 기본적으로 3번 자동 재시도됨

  • 네트워크 문제 등으로 쿼리가 실패하면 3번 자동으로 재시도
  • 재시도할 때마다 점점 긴 시간(지수 증가) 후에 다시 요청 (ex. 1초 → 2초 → 4초)

👉 재시도 횟수 변경하기

const { data } = useQuery(['posts'], fetchPosts, { 
  retry: 5, // 5번 재시도
  retryDelay: attempt => Math.min(1000 * 2 ** attempt, 30000) // 지수 백오프 (최대 30초)
});

👉 재시도를 비활성화하고 싶다면?

const { data } = useQuery(['posts'], fetchPosts, { retry: false });

5. TanStack Query는 '구조적 공유(Structural Sharing)'을 이용해 성능 최적화

  • 기본적으로 TanStack Query는 기존 데이터와 새 데이터를 비교해서
    변경되지 않았다면 같은 데이터 참조 유지 (불필요한 렌더링 방지)
  • 이 기능 덕분에 useMemo나 useCallback을 사용할 필요가 줄어듦

👉 대부분의 경우 이 기능이 성능 최적화에 도움되므로 꺼둘 필요 없음
👉 JSON이 아닌 데이터를 비교하려면 structuralSharing 옵션을 설정할 수 있음

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      structuralSharing: false, // 구조적 공유 비활성화
    },
  },
});

 

 

 

'React' 카테고리의 다른 글

리액트 서버 컴포넌트 톺아보기 (번역)  (0) 2025.04.03
useState의 렌더링 방식 이해하기(feat.batching)  (0) 2025.02.06
Redux 정리  (0) 2025.01.21
state를 reducer로 작성하기  (0) 2025.01.20
useRef와 권장사항  (1) 2024.11.01