TanStack Query - TIL

2026. 6. 23. 16:56·FE/Next
TanStack Query를 공부한 후 남긴 기록을 담은 포스팅
직접 기록한 내용을 GPT를 활용하여 마크다운으로 변환했다!

1. TanStack Query 

  • 리액트 쿼리(React Query): 서버 상태 관리를 쉽게 하도록 도와주는 라이브러리
  • 서버 상태(Server State): 원본이 서버에 있고, 클라이언트가 복사본을 잠깐 빌려와서 보고 있는 모든 비동기 처리 데이터

💡 주요 기능

  • API 호출 시 로딩, 에러, 성공 상태 자동 트래킹 (isPending, error, data)
  • API 응답 데이터 캐싱 및 재사용
  • 데이터베이스(DB) 데이터와 UI 데이터의 동기화 및 업데이트

2. 데이터 조회: useQuery (Read)

📌 기본 규칙

  1. queryKey는 반드시 배열 형태
  2. queryFn은 반드시 Promise를 반환!
  3. Next.js App Router 환경에서는 Root Layout에서 QueryClientProvider로 감싸주어야 전역에서 사용할 수 있습니다.
const getTodos = async () => {
  return [1, 2, 3];
};

const { data, isPending, error } = useQuery({
  queryKey: ["todos"], // QueryCache(전역 저장소) 내에 데이터를 저장하는 고유 주소
  queryFn: getTodos,
});​

useQuery()는 서버로부터 데이터를 fetching해올 때 사용한다.
마운트 이후 queryFn을 실행하여 가져온 데이터를 queryKey에 저장한다.

 

🙋‍♂️ 궁금증과 그에대한 답 (Q&A)

Q1. queryKey는 왜 꼭 '배열'이어야 할까?

  • 상세한 주소 지정과 계층 구조 관리: 단순히 문자열 하나만 쓰면 todos 전체를 다루기 어렵지만,
    배열을 쓰면 ['todos', 'page', 1] 또는 ['todos', id]처럼 세부적인 조건을 순서대로 담아 QueryCache 안에서 고유한 주소로 삼을 수 있다.
  • v4부터의 강제 규칙: 내부적인 안정성과 예측 가능성을 위해 TanStack Query(v4 이후)부터는 무조건 배열 형태로만 전달하도록 규칙이 고정되었다.

Q2. queryFn이 반드시 Promise를 반환해야 하는 이유는?

  • 서버 상태 관리는 비동기 요청에만 해당하기 때문
  • 비동기 상태 추적의 기반: useQuery의 핵심 역할은 데이터를 가져오는 과정에서 발생하는
    isPending(로딩), error(실패), data(성공) 상태를 자동으로 추적하는 것
  • 약속된 신호: 자바스크립트에서 비동기 작업의 성공(resolve)과 실패(reject)를 처리하는 표준 객체가 바로 Promise useQuery는 이 Promise의 상태 변화를 감시하다가 UI를 그에 맞춰 실시간으로 업데이트

Q3. 여기서 말하는 '마운트(Mount)'란 무슨 뜻?

  • 화면에 컴포넌트가 처음 등장하는 순간: 리액트에서 마운트(Mount)는 컴포넌트가 처음 화면에 그려지며
    메모리에 얹어지는 시점
  • TanStack Query에서의 의미: * 최초 실행: 컴포넌트가 화면에 마운트되면
    useQuery는 queryFn을 실행하여 데이터를 가져오고 캐시에 저장
    • stale 상태와 refetch: 쿼리는 기본적으로 받아온 데이터를 즉시 stale(신선하지 않은 상태)로 취급한다. 유저가 다른 페이지에 갔다가 다시 이 컴포넌트로 돌아왔을 때(= 재마운트), "어? 데이터가 stale하네? 새로 받아와야지!" 하고 자동으로 queryFn을 다시 실행(refetchOnMount: true)하여 화면을 최신 데이터로 동기화

3. 데이터 조작: useMutation (Create, Update, Delete)

서버의 데이터를 수정, 추가, 삭제할 때 사용
CUD 작업 이후에는 UI와 DB의 싱크를 맞춰주는 동기화(Synchronization) 작업이 필수적

 

📌 특징 및 규칙

  • mutationFn은 Promise를 반환
  • mutationFn은 단 하나의 매개변수만 받을 수 있다. (여러 인자가 필요할 경우 객체 구조 분해 할당을 사용)
  • 변경 작업이 성공하면 기존 캐시를 stale 상태로 변환시켜 새 데이터를 다시 패칭(fetching)해와야 한다.
    이때 창고 관리자인 queryClient의 invalidateQueries() 메서드를 사용!

 

🛠️ 구현 흐름 및 사용 방법

  1. RootLayout에 QueryCache(창고) 생성
    const client = new QueryClient();
    
  2. 사용 컴포넌트에서 관리자 호출 및 호출 처리
    const queryClient = useQueryClient();
    
    const postTodoMutation = useMutation({
      mutationFn: postTodo,
      onSuccess: () => {
        // 'todos' 키를 가진 캐시를 stale하게 만들어 데이터를 새로고침(refetch)하도록 유도
        queryClient.invalidateQueries({ queryKey: ["todos"] });
      },
      onError: (error) => { console.error(error); },
      onSettled: () => { /* 성공/실패 여부와 상관없이 항상 실행 */ }
    });
    
  3. 이벤트 트리거 (버튼 클릭 등)
    <button onClick={() => {
      // mutate()는 설정한 mutationFn을 당장 실행하라는 명령어 역할을 합니다.
      postTodoMutation.mutate({
        id: Date.now(),
        title: 'TanStack Query 공부'
      });
    }}>
      투두 추가
    </button>
    

4. TanStack Query의 캐싱 전략과 Lifecycle

🔄 Stale-While-Revalidate (SWR)

"새로운 데이터를 가져오는 동안, 우선은 기존에 캐싱된 데이터를 보여준다." 라는 전략

  • 예시 (Cache-Control: max-age=1, stale-while-revalidate=59)
    • 0~1초 사이 재요청: 서버 호출 없이 캐시 데이터를 바로 사용
    • 1~60초 사이 재요청: 일단 가지고 있던 캐시 데이터를 화면에 즉시 보여주고, 백그라운드에서 서버로부터 최신 데이터를 받아와 자연스럽게 교체

💡 캐시 데이터는 전역 상태

QueryClientProvider는 내부적으로 리액트의 Context API를 사용하므로,
그 자식에 속한 모든 컴포넌트는 전역적으로 관리되는 캐시 데이터 창고에 접근할 수 있다.

⚙️ 기본 설정 (Default Config)과 라이프사이클 항목들

하나의 쿼리 인스턴스(Query Key)는 다음과 같은 생명주기를 가집니다.

  • staleTime: 0
  • 데이터를 패치해 오는 즉시 해당 데이터는 stale(신선하지 않은 상태)이 됩니다.
  • refetchOnMount: true
  • 컴포넌트가 마운트될 때 데이터가 stale 상태라면 자동으로 refetch를 실행합니다.
  • refetchOnWindowFocus: true
  • 브라우저 화면(창)을 다시 포커스할 때마다 자동으로 refetch를 실행합니다.
  • refetchOnReconnect: true
  • 네트워크가 끊겼다가 다시 연결되었을 때 자동으로 refetch를 실행합니다.
  • gcTime: 5분 (v5 전에는 cacheTime)
  • 컴포넌트가 언마운트되면 해당 쿼리는 inactive 상태가 됩니다. 이 상태로 5분이 지나면 가비지 컬렉터(GC)에 의해 캐시 메모리에서 완전히 지워집니다.
  • retry: 3
  • queryFn 요청이 실패하더라도 즉시 에러를 내지 않고 3번까지 재시도합니다.

🔥 staleTime vs gcTime 핵심 요약

  • staleTime: 얼마의 시간이 흐른 뒤에 데이터를 '신선하지 않은 것'으로 취급할 것인가?
    (staleTime > 0 이면 fresh, 0 이면 즉시 stale)
  • gcTime: 컴포넌트가 언마운트(inactive)된 후, 얼마의 시간 뒤에 메모리 창고에서 데이터를 완전히 삭제할 것인가?

5. 꼭 알아야 할 주요 옵션들 (Must-Know Options)

① enabled (조건부 실행)

useQuery({
  queryKey: ["todos"], 
  queryFn: getTodos,
  enabled: true // false로 지정하면 컴포넌트가 마운트되어도 자동 실행되지 않음
});
  • 동작: true일 때만 queryFn이 실행됩니다.
  • 활용: 이벤트 발생 시에만 쿼리를 트리거하고 싶을 때, 혹은 순차적 쿼리(Dependent Queries)를 실행하고 싶을 때 사용합니다.
// 순차적 쿼리 예제: 유저 정보가 먼저 조회되어 유저 ID가 존재할 때만 프로젝트 목록 조회
const { data: user } = useQuery({
  queryKey: ['user', email],
  queryFn: getUserByEmail,
});

const userId = user?.id;

const { data: projects } = useQuery({
  queryKey: ['projects', userId],
  queryFn: getProjectsByUser,
  enabled: !!userId, // userId가 존재(true)할 때만 쿼리 가동
});

② select (데이터 가공)

서버에서 받아온 원본 데이터를 컴포넌트에 전달하기 전, 필요한 형태로 정제/변형하여 리턴

function User() {
  const { data } = useQuery({
    queryKey: ['user'], 
    queryFn: fetchUser,
    select: (user) => user.username, // 데이터 중에서 username만 쏙 골라냄
  });
  
  return <div>Username: {data}</div>;
}

6. 로딩 및 페칭 상태 비교 (isPending vs isLoading vs isFetching)

속성명 핵심 정의 캐시 데이터 존재 여부 현재 API 요청 여부 (Fetching) 주요 활용 UI 예시 비고
isPending 데이터가 없고 최초 실행 대기 중인 상태 없음
(undefined)
상관없음
(대기 중일 수도 있음)
진입 시의
전반적인 예외 처리
v5부터 status === 'loading'이 pending으로 명칭 변경
isLoading 데이터가 전혀 없는 상태에서 최초로 서버에 요청을 보낸 상태 없음
(undefined)
현재 요청 중
(Yes)
최초 화면 진입 시
전체 스켈레톤/로딩 스피너
isPending && isFetching 개념 (하드 로딩 감지)
isFetching 기존 데이터 유무 불문, 지금 이 순간 서버에 요청을 보낸 상태 상관없음 현재 요청 중
(Yes)
백그라운드 새로고침 시
화면 상단
조그만 인디케이터
초기 로딩과 원격 새로고침을 모두 포함하는 속성

7. useQuery 에러 핸들링 패턴

Pattern A. 컴포넌트 내부에서 처리 (표준 방식)

가장 보편적인 방법으로, 컴포넌트 내의 분기 처리를 통해 처리하거나 필요한 부가 작업(Side-effect)은 useEffect를 활용

function TodoList() {
  const { data: todos, error, isPending } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  });

  // 렌더링과 상관없는 사이드 이펙트는 useEffect 내에서 처리 (예: 알림창)
  useEffect(() => { 
    if (error) {
      alert(error.message);
    }
  }, [error]);

  if (isPending) return <p>Loading...</p>;

  // Standard Error Handling UI
  if (error) return <div style={{ fontSize: 24 }}>에러가 발생했습니다: {error.message}</div>;

  return (
    <div>
      {todos.data.map((todo) => (
        <Todo key={todo.id} {...todo} />
      ))}
    </div>
  );
}

Pattern B. QueryCache를 통한 전역 에러 핸들링 (Global Callback)

특정 도메인이나 공통적인 에러 처리가 반복될 때 중앙 저장소(QueryCache) 레벨에서 일괄 관리하는 방식

// 1. main.jsx (설정부)
const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      // meta 설정을 확인하여 특정 출처의 에러만 골라 처리 가능
      if (query.meta?.source === "todos") {
        toast.error(`Something went wrong in TodoList: ${error.message}`);
      }
    }
  }),
});

// 2. src/components/TodoList.jsx (사용부)
function TodoList() {
  const { data: todos, isPending } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    meta: { source: "todos" } // 전역 콜백에서 알아채도록 메타데이터 심어두기
  });

  if (isPending) return <p>Loading...</p>;

  return (
    <div>
      {todos.data.map((todo) => (
        <Todo key={todo.id} {...todo} />
      ))}
    </div>
  );
}

☝ 에러 처리 가이드라인

기본적으로 UI 제어가 수월한 컴포넌트 내 처리를 기본으로 하되, 아래와 같은 명확한 공통 로직이 필요할 때만 전역 콜백(QueryCache) 활용을 권장

  1. 전역 에러 로깅 처리 알고리즘 적용시
  2. Sentry 등 외부 모니터링 서비스로의 일괄 전송
  3. 401 Unauthorized 에러 발생 시 공통 로그아웃 및 /login 페이지 리다이렉트 처리 코드 등

'FE > Next' 카테고리의 다른 글

렌더링 전략: SSG, ISR, SSR, CSR  (0) 2026.06.12
'FE/Next' 카테고리의 다른 글
  • 렌더링 전략: SSG, ISR, SSR, CSR
eznnni
eznnni
🍀 성장하는 개발자 연습생 eznnni입니다!
  • eznnni
    개발자 연습생의 기록
    eznnni
  • 전체
    오늘
    어제
  • Eznnni's GitHub
    • 분류 전체보기 (12)
      • WEB (3)
      • FE (5)
        • HTML (1)
        • CSS (1)
        • JavaScript (0)
        • React (1)
        • Next (2)
      • BE (0)
      • 개발 (3)
        • 🛠️ 트러블슈팅 (3)
      • 💡 기타 활동 (1)
        • 텐엑스 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Next.js
    클래스 컴포넌트
    프로젝트
    Sematic Tag
    Server Actions
    Javascript
    트러블슈팅
    react
    부수입 강의
    코드잇 텐엑스
    Hydration
    함수 컴포넌트
    서버 액션
    HTML
    express
    코드잇 10x
    Be
    Web
    PreRendering
    backend
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
eznnni
TanStack Query - TIL
상단으로

티스토리툴바