Will find a way

[React] Tanstack Query (구: React Query) 본문

FrontEnd/React

[React] Tanstack Query (구: React Query)

Jaka_Park 2025. 8. 7. 14:41

 

들어가기 전

리액트를 공부하거나 다뤄본 사람들은 React Query를 한번쯤 다뤄봤거나 다루게 될 것이다. 그만큼 리액트에서는 반드시 알아야 라이브러리다. 리액트 쿼리에 대해서 잘 모를 때 서버에서 데이터를 가져올 때 사용했기 때문에 리액트에서 사용하는 비동기라이브러리(?) 중 하나라고만 생각했다. 공부를 하고 적용을 하며 내가 생각한만큼 단순한 것이 아님을 알게 됐다. 내가 가지고 있는 잘못된 개념을 다시 한번 정리하고자 이 글을 포스팅 하게 됐다.

Tanstack Query 란?

Tanstack Query는 서버에서 데이터를 가져오기, 데이터 캐싱, 캐시 제어 등 데이터를 효율적으로 관리할 수 있는 라이브러리다. 원래는 React Query라는 이름을 사용하였으나 Vue나 다른 곳에서도 사용할 수 있게 되면서 React Query에서 Tanstack Query로 이름을 변경하였다. 

 

'데이터를 관리한다.' 라는 말을 생각하면서
이 글을 읽으면 조금 더 도움이 될 것이다.

 

사용 이유

React는 클라이언트 상태 관리에만 집중되어 있다.

- 서버 상태는 언제 바뀔지 모른다 (외부 API, DB 등)

- 직접 useEffect + axios + useState 조합으로 짜면 코드가 복잡해지고 재사용도 어려움

- 캐시, refetch, 로딩 처리 등 반복적인 작업이 많음

 

위와 같은 것들을 자동으로 처리해주는 도구라고 생각하면 된다.

 

사용 예시

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

const fetchUser = async () => {
  const { data } = await axios.get('/api/user')
  return data;
};

export const UserComponent = () => {
  const {} = useQuery({
    queryKey: ['user'],
    queryFn: fetchUser,
  });

  if (isLoading) return <p>로딩 중...</p>;
  if (isError) return <p>에러 발생</p>;

  return <div>사용자 이름: {data.name}</div>;
}

 

핵심 기능 및 주요 훅

useQuery (Read)

Tanstack Query 에서 사용하는 주요 훅으로

컴포넌트에서 데이터를 가져올 때 사용, 비동기 캐싱 + 자동갱신 + 로딩/에러 관리

const { data, isLoading, isError, error, refetch } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  enabled: !!userId,
  staleTime: 1000 * 60 * 5, // 5분간 fresh
});

 

자주 사용하는 옵션

옵션 설명
queryKey 쿼리 고유 키 (캐싱 기준)
queryFn 데이터를 불러오는 함수
enabled 자동 실행 여부
staleTime 데이터가 fresh 상태 유지 시간
select 반환값 가공
refetchOnWindowFocus 창 복귀 시 리패치 여부
retry 실패 시 재시도 횟수
onSuccess, onError 성공/실패 콜백

 

useMutation - 데이터 수정하기 (Create / Update / Delete)

- 기본 용도: 서버에 데이터를 생성/수정/삭제할 때 사용
- 캐싱 X, 대신 성공 시 쿼리 무효화 (invalidate)

const userId = 42; // 여기서 정의됨
  
const mutation = useMutation({
  mutationFn: ({ userId, name }) =>
  axios.put(`/users/${userId}`, { name }),

  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['user', userId] });
  },
});

const handleSave = () => {
  mutation.mutate({ userId, name: 'Jerome' }); // 위의 userId가 사용됨
};

 

자주 쓰는 옵션

옵션 설명
mutationFn 서버 요청 함수
onSuccess 성공 후 캐시 무효화 등 처리
onError 실패 시 알림 등 처리
onSettled 성공/실패 모두 후처리

 

useInfiniteQuery - 무한 스크롤 (페이지네이션)

사용 예시

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
  getNextPageParam: (lastPage, allPages) => {
    return lastPage.hasNext ? allPages.lenth + 1 : undefined;
  },
});

 

 

반환 값

반환 값 설명
data 여러 페이지 데이터를 담은 객체 (pages, pageParams)
fetchNextPage 다음 페이지 요청
fetchPreviousPage() 이전 페이지 요청
hasNextPage 다음 페이지 있는지 여부 (getNextPageParam에 따라 결정됨)
hasPreviousPage 이전 페이지가 있는지 여부 (getPreviousPageParam에 따라 결정됨)
isFetchingNextPage 다음 페이지 로딩 중인지 여부
isFetchingPreviousPage 이전 페이지 로딩 중인지 여부
refetch() 전체 refetch

 

 

자주 쓰는 옵션

옵션 설명
queryKey 캐시 키, 일반 쿼리처럼 사용
queryFn - 데이터를 불러오는 함수. 인자로 { pageParam }을 받음
- pagaParam은 getNextPageParam에서 반환한 값
getNextPageParam(lastPage, allPages) 다음 페이지를 어떻게 불러올지 알려주는 함수
- lastPage: 직전에 불러온 페이지 데이터
- allPages: 지금까지 불러온 모든 페이지
getPreviousPageParam 이전 페이지가 있는 경우 정의 (선택사항)
initialPageParam 초기 pageParam 값 (기본값은 undefined)

 

queryClient - 전역 캐시 관리

import { useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();

 

함수

함수 설명
invalidateQueries(queryKey) 쿼리 무효화 (-> refetch 유도)
refetchQueries(queryKey) 강제 리패치
removeQueries(queryKey) 쿼리 캐시 삭제
setQueryData(queryKey, data) 수동으로 캐시 변경
getQueryData(queryKey) 캐시 값 가져오기

 

캐싱, 리패칭, 무효화 용어 요약

캐싱 (cahing) : 서버에서 받아온 데이터를 저장하여 다시 사용할 수 있게 함

무효화 (invalidate) : 기존 캐시를 오래됨(stale) 으로 표시해 다음에 refetch 유도

리패칭 (refetch) : 데이터를 새로 요청하여 업데이트 (자동 or 수동)

 

useInfiniteQuery 사용 예제

import { useInfiniteQuery } from '@tanstack/react-query';
import axios from 'axios';

const fetchPosts = async (page: number) => {
  // page 번호에 따라 게시글 데이터를 받아오는 API 호출
  const res = await axios.get(`/api/posts?page=${page}`);
  return res.data; // 응답 데이터 반환
};

const PostList = () => {
  // useInfiniteQuery 훅 사용
  const {
    data, // 전체 페이지 데이터를 담고 있는 객체
    fetchNextPage, // 다음 페이지를 가져오는 함수
    hasNextPage, // 다음 페이지가 존재하는지 여부 (getNextPageParam이 반환값으로 결정)
    isFetchingNextPage, // 다음 페이지를 불러오는 중인지 여부
    isLoading, // 최초 로딩 여부
    isError, // 에러 발생 여부
    error, // 에러 객체
  } = useInfiniteQuery({
	queryKey: ['posts'], // 캐시 키 (고유한 요청 식별자)
	queryFn: async ({ pageParam = 1 }) => {
	  // 데이터를 가져오는 함수. pageParam은 자동으로 넘겨짐 (없ㅁ으면 기본 값1)
	  return await fetchPosts(pageParam);
	},
	getNextPageParam: (lastPage, allPages) => {
	  // 다음 페이지를 불러오기 위한 pageParam을 리턴
	  // 예 : 응답 객체에 nextPage가 있으면 그걸 반환, 없으면 undefined로 종료
	  return lastPage.hasNext ? lastPage.nextPage : undefined;
	},
	initialPageParam: 1, // 초기 pageParam (기본 페이지 번호)
  });
  // 로딩 중이면 로딩 메시지 표시
  if (isLoading) return <div>Loading...</div>;
  
  // 에러 발생 시 에러 메시지 출력
  if (isError) return <div>Error: {error.message}</div>;

  return (
    <div>
      {/* 여러 페이지를 반복하며 렌더링 */}
      {data?.pages.map((page, index) => (
        <div key={index}>
          {page.posts.map((post: any) => (
            <div key={post.id}>{post.title}</div>
         ))}
        </div>
      ))}
      {/* 여러 페이지를 반복하며 렌더링 */}
      {hasNextPage && (
        <button
          onClick={() => fetchNextPage()} // 다음 페이지 요청
          disabled={isFetchingNextPage} // 요청 중이면 버튼 비활성화
          >
            {isFEtchingNextPage ? 'Loading more...' : 'Load More'}
        </button>
      )}
    </div>
  );
};

export default PostList;

 

마무리

Tanstack Query를 사용할 때 유독 queryKey가 어떤건지 유독 헷갈렸다. 캐싱과 리페칭 개념을 공부하면서 비로소 리액트쿼리가 뭔지 조금 보이기 시작했다. 글을 쓰면서 부족한 부분은 차후에 추가나 수정 할 예정이다.