tanstack query v4 -> v5

March 03, 2025

전체적인 변화

매개변수를 객체 하나로 통일

// v4
useQuery(key, fn, options);
useInfiniteQuery(key, fn, options);
useMutation(fn, options);
// ...

// v5
useQuery({ queryKey, queryFn, ...options });
useInfiniteQuery({ queryKey, queryFn, ...options });
useMutation({ mutationFn, ...options });
// ...

useQuery

onSuccess, onError, onSettled 제거

https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose 인상 깊었던 부분 일부 번역

추가 렌더링 사이클

많은 사람들이 상태를 동기화하는 데, 해당 콜백을 쓰곤 합니다. 제발 이렇게 하지마세요!

export function useTodos() {
  const [todoCount, setTodoCount] = React.useState(0);
  const { data: todos } = useQuery({
    queryKey: ["todos", "list"],
    queryFn: fetchTodos,
    //😭 please don't
    onSuccess: (data) => {
      setTodoCount(data.length);
    },
  });

  return { todos, todoCount };
}

이러한 코드는 불필요한 렌더링을 유발합니다. 렌더링 과정은 다음과 같습니다.

  1. todosundefinedlength가 0일 것입니다. 이는 기본 상태값으로, 옳습니다.
  2. todoslength가 5인 배열이 되고, todoCount는 0일 것입니다. useQuery는 이미 돌았으나, setTodoCount는 실행되지 않은 사이클에 속해있기 때문입니다. 이는 값이 동기화되지 않았으므로 잘못됐습니다.
  3. todoslength가 5인 배열이 되고, todoCount는 5가 될 것입니다. 이것이 최종 상태이며 다시 옳습니다.

간단히 수정하자면, 상태를 사용하는 것 대신 계산할 수 있습니다. https://tkdodo.eu/blog/dont-over-use-state (주제 관련된 글)

export function useTodos() {
  const { data: todos } = useQuery({
    queryKey: ["todos", "list"],
    queryFn: fetchTodos,
  });

  const todoCount = todos?.length ?? 0;

  return { todos, todoCount };
}

todoCount가 동기화되지 않을 경우는 없습니다.

그렇다면 어떻게 상태를 관리해야할까

상태 동기화가 불가피하거나, 성공 이후에 처리해야하는 로직의 경우

→ 제 생각이므로 참고만 부탁드려요

// Cursor AI가 만들어준 코드
const { data, isSuccess } = useQuery({
  queryKey: ["user"],
  queryFn: fetchUser,
});

useEffect(() => {
  if (isSuccess && data) {
    // Simple side effects
    updateLastLoginTime();
    showWelcomeMessage();
  }
}, [isSuccess, data]);

가져온 data를 가공해 사용하는 경우(무거운 연산을 실행할 때 권장)

// Cursor AI가 만들어준 코드
const { data } = useQuery({
  queryKey: ["analytics"],
  queryFn: fetchAnalytics,
  select: (data) => {
    // Complex data reshaping/aggregation
    return data.map((item) => ({
      ...item,
      metrics: processMetrics(item.rawData),
      trends: calculateTrends(item.historicalData),
    }));
  },
});

remove 제거

앞으로는 remove 대신 이러한 방식으로 사용

const queryClient = useQueryClient();
const query = useQuery({ queryKey, queryFn });

queryClient.removeQueries({ queryKey });

cacheTime

gcTime으로 이름 변경

useInfiniteQuery

  • initialPageParam이 필수값으로 추가됨
  • queryFn의 pageParam의 기본값으로 설정됨

그 외에도 많은 변화가 있으나, 작업하며 느낀 큰 변화를 중점으로 작성해보았습니다.

현재 고민인 부분

  • 성공 시(onSuccess)에 같은 처리를 하는 같은 Query의 경우 useEffect에 dependency 걸어서 데이터 처리 시, 각각의 컴포넌트에 중복 코드가 발생하는데 이걸 어떻게 처리하면 좋을지.. 커스텀 훅이 좋을지 애초에 더 좋은 방법이 있을지 고민

  • remove 제거할 때 UI단 코드에서 아래 코드를 사용하게 되는데, 이게 과연 최선일지

    queryClient.removeQueries({ queryKey });

-> 해결하게 되면 또 새로운 글로 올릴 예정!

출처