Docs
GUIDES & CONCEPTS
Migrating to v4

Migrating to React Query 4

Breaking Changes

v4는 주요 버전 업데이트로, 몇 가지 주요 변경 사항이 있습니다:

react-query is now @tanstack/react-query

이제 react-query@tanstack/react-query로 변경되었습니다. 따라서 종속성을 제거하고 새로운 종속성을 설치해야 합니다:

npm uninstall react-query
npm install @tanstack/react-query
npm install @tanstack/react-query-devtools
- import { useQuery } from 'react-query' // [!code --]
- import { ReactQueryDevtools } from 'react-query/devtools' // [!code --]
 
+ import { useQuery } from '@tanstack/react-query' // [!code ++]
+ import { ReactQueryDevtools } from '@tanstack/react-query-devtools' // [!code ++]

Codemod

이전의 import를 새 버전에 맞게 자동으로 변경하는 codemod가 제공됩니다. 이를 통해 import 변경 작업을 더 쉽게 할 수 있습니다.

코드 생성 후에는 반드시 검토해 주시고, 로그 출력을 주의 깊게 살펴보세요.

.js 또는 .jsx 파일에 적용하려면 다음 명령어를 사용하세요:

npx jscodeshift ./path/to/src/ \
  --extensions=js,jsx \
  --transform=./node_modules/@tanstack/react-query/codemods/v4/replace-import-specifier.js

.ts 또는 .tsx 파일에 적용하려면 다음 명령어를 사용하세요:

npx jscodeshift ./path/to/src/ \
  --extensions=ts,tsx \
  --parser=tsx \
  --transform=./node_modules/@tanstack/react-query/codemods/v4/replace-import-specifier.js

TypeScript의 경우 tsx를 파서로 지정해야 합니다. 그렇지 않으면 codemod가 제대로 적용되지 않습니다.

Note: codemod를 적용한 후에는 코드 포맷팅이 깨질 수 있으므로 prettier 및/또는 eslint를 실행하는 것을 잊지 마세요!

Note: codemod는 오직 import만 변경하며, devtools 패키지는 별도로 수동 설치해야 합니다.

Query Keys (and Mutation Keys) need to be an Array

v3에서는 Query 및 Mutation Keys를 문자열 또는 배열로 사용할 수 있었지만, React Query는 내부적으로 배열만 사용합니다. 모든 API에서 일관성을 유지하기 위해, 이제 모든 키는 배열만 허용됩니다:

-useQuery("todos", fetchTodos) + // [!code --]
  useQuery(["todos"], fetchTodos); // [!code ++]

Codemod

이 변경을 쉽게 적용할 수 있도록 codemod가 제공됩니다. 이를 통해 배열로 변환할 수 있습니다.

.js 또는 .jsx 파일에 적용하려면 다음 명령어를 사용하세요:

npx jscodeshift ./path/to/src/ \
  --extensions=js,jsx \
  --transform=./node_modules/@tanstack/react-query/codemods/v4/key-transformation.js

.ts 또는 .tsx 파일에 적용하려면 다음 명령어를 사용하세요:

npx jscodeshift ./path/to/src/ \
  --extensions=ts,tsx \
  --parser=tsx \
  --transform=./node_modules/@tanstack/react-query/codemods/v4/key-transformation.js

TypeScript의 경우 tsx를 파서로 지정해야 합니다. 그렇지 않으면 codemod가 제대로 적용되지 않습니다.

Note: codemod를 적용한 후에는 코드 포맷팅이 깨질 수 있으므로 prettier 및/또는 eslint를 실행하는 것을 잊지 마세요!

The idle state has been removed

새로운 fetchStatus가 도입되면서 idle 상태는 더 이상 필요하지 않게 되었습니다. 이제 fetchStatus: 'idle'로 동일한 상태를 더 잘 표현할 수 있습니다.

주로 disabled 쿼리에 영향을 미치며, 이전에는 idle 상태였던 쿼리는 이제 loading 상태로 표시됩니다:

- status: 'idle' // [!code --]
+ status: 'loading'  // [!code ++]
+ fetchStatus: 'idle' // [!code ++]

disabled 쿼리는 이제 loading 상태로 시작합니다. 따라서 isLoading 대신 isInitialLoading을 사용하는 것이 좋습니다:

-isLoading + // [!code --]
  isInitialLoading; // [!code ++]

new API for useQueries

useQueries 훅은 이제 queries prop이 있는 객체를 입력으로 받습니다. queries prop의 값은 쿼리 배열입니다:

-useQueries([
  { queryKey1, queryFn1, options1 },
  { queryKey2, queryFn2, options2 },
]) + // [!code --]
  useQueries({
    queries: [
      { queryKey1, queryFn1, options1 },
      { queryKey2, queryFn2, options2 },
    ],
  }); // [!code ++]

Undefined is an illegal cache value for successful queries

업데이트를 중단할 수 있도록 undefined를 반환할 수 있지만, undefined는 이제 유효한 캐시 값이 아닙니다. undefined를 반환하는 것은 이제 허용되지 않으며, 런타임에서 실패한 Promise로 변환되어 에러가 발생하고, 개발 모드에서는 콘솔에 기록됩니다.

Queries and mutations, per default, need network connection to run

기본적으로 쿼리와 뮤테이션은 네트워크 연결이 없으면 paused 상태가 됩니다. 이전 동작을 유지하려면 networkMode: offlineFirst를 글로벌로 설정할 수 있습니다:

new QueryClient({
  defaultOptions: {
    queries: {
      networkMode: "offlineFirst",
    },
    mutations: {
      networkMode: "offlineFirst",
    },
  },
});

notifyOnChangeProps property no longer accepts "tracked" as a value

notifyOnChangeProps 옵션이 더 이상 "tracked" 값을 허용하지 않습니다. 대신, useQuery는 기본적으로 속성을 추적합니다. notifyOnChangeProps: "tracked"을 사용하는 모든 쿼리는 이 옵션을 제거해야 합니다.

notifyOnChangePropsExclusion has been removed

v4에서 notifyOnChangeProps는 기본적으로 "tracked" 동작을 따릅니다. 따라서 더 이상 notifyOnChangePropsExclusion 옵션을 사용할 필요가 없습니다.

Consistent behavior for cancelRefetch

cancelRefetch 옵션은 이제 모든 함수에서 사용되며, 기본값이 true로 설정됩니다. refetchQueriesinvalidateQueries를 연속으로 호출하면 첫 번째 호출이 취소되고 두 번째 호출이 시작됩니다:

queryClient.refetchQueries({ queryKey: ['todos'] })
// 이 호출은 이전 refetch를 취소하고 새 fetch를 시작합니다
queryClient.refetchQueries({ queryKey: ['todos'] })

이 동작을 원하지 않으면 cancelRefetch: false를 명시적으로 설정할 수 있습니다:

queryClient.refetchQueries({ queryKey: ['todos'] })
// 이 호출은 이전 refetch를 취소하지 않습니다
queryClient.refetchQueries({ queryKey: ['todos'] }, { cancelRefetch: false })

Query Filters

쿼리 필터는 이제 단일 필터로 통합되었습니다. 이 필터는 'active', 'inactive', 'all' 중 하나를 선택할 수 있습니다:

- active?: boolean // [!code --]
- inactive?: boolean // [!code --]
+ type?: 'active' | 'inactive' | 'all' // [!code ++]

이 필터는 기본값으로 all을 사용하며, active 또는 inactive 쿼리만 선택할 수도 있습니다.

refetchActive / refetchInactive

queryClient.invalidateQueriesrefetchActiverefetchInactive 플래그도 통합되었습니다:

- refetchActive?: boolean // [!code --]
- refetchInactive?: boolean // [!code --]
+ refetchType?: 'active' | 'inactive' | 'all' | 'none' // [!code ++]

이 플래그의 기본값은 active이며, 모든 쿼리를 리페치하지 않으려면 none 옵션을 사용할 수 있습니다.

onSuccess is no longer called from setQueryData

setQueryData에서 onSuccess가 호출되지 않습니다. 이제 onSuccess는 요청이 있을 때만 호출됩니다. 데이터 필드의 변경 사항을 감지하려면 useEffect를 사용할 수 있습니다:

const { data } = useQuery({ queryKey, queryFn });
React.useEffect(() => mySideEffectHere(data), [data]);

persistQueryClient and the corresponding persister plugins are no longer experimental and have been renamed

createWebStoragePersistorcreateAsyncStoragePersistor는 각각 createSyncStoragePersistercreateAsyncStoragePersister로 이름이 변경되었습니다. persistQueryClient는 이제 실험적이지 않으며 PersistQueryClient로 리팩토링되었습니다:

- import { persistQueryClient } from 'react-query/persistQueryClient-experimental' // [!code --]
- import { createWebStoragePersistor } from 'react-query/createWebStoragePersistor-experimental' // [!code --]
- import { createAsyncStoragePersistor } from 'react-query/createAsyncStoragePersistor-experimental' // [!code --]
 
+ import { persistQueryClient } from '@tanstack/react-query-persist-client' // [!code ++]
+ import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' // [!code ++]
+ import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'  // [!code ++]

The cancel method on promises is no longer supported

기존의 cancel 메서드(구버전 cancel 메서드)는 더 이상 지원되지 않습니다. 이 메서드는 Promise에 cancel 함수를 정의하여 라이브러리가 쿼리 취소를 지원하도록 했으나 제거되었습니다. 이제는 AbortController API (opens in a new tab)를 내부적으로 사용하여 AbortSignal 인스턴스 (opens in a new tab)를 제공하는 새로운 API (v3.30.0에서 도입됨)를 사용하는 것이 좋습니다.

TypeScript

타입은 이제 TypeScript v4.1 이상을 요구합니다.

Supported Browsers

v4부터 React Query는 최신 브라우저에 최적화되었습니다. 브라우저 리스트를 업데이트하여 더 현대적이고 성능이 뛰어나며 더 작은 번들을 생성하도록 했습니다. 요구 사항에 대해서는 여기에서 확인하실 수 있습니다.

setLogger is removed

이전에는 setLogger를 호출하여 전역적으로 로거를 변경할 수 있었습니다. v4에서는 이 함수가 QueryClient를 생성할 때 선택적 필드로 대체되었습니다.

- import { QueryClient, setLogger } from 'react-query'; // [!code --]
+ import { QueryClient } from '@tanstack/react-query'; // [!code ++]
 
- setLogger(customLogger) // [!code --]
- const queryClient = new QueryClient(); // [!code --]
+ const queryClient = new QueryClient({ logger: customLogger }) // [!code ++]

No default manual Garbage Collection server-side

v3에서는 React Query가 쿼리 결과를 기본적으로 5분 동안 캐시한 후, 수동으로 가비지 컬렉션을 수행했습니다. 이 기본 설정은 서버 측 React Query에도 적용되었습니다.

이로 인해 높은 메모리 소비와 수동 가비지 컬렉션 완료를 기다리는 과정에서 프로세스가 멈추는 문제가 발생했습니다. v4에서는 기본적으로 서버 측의 cacheTimeInfinity로 설정되어 수동 가비지 컬렉션이 비활성화되었습니다 (NodeJS 프로세스는 요청이 완료되면 모든 것을 자동으로 정리합니다).

이 변경 사항은 Next.js와 같은 서버 측 React Query 사용자의 경우에만 영향을 미칩니다. cacheTime을 수동으로 설정하고 있다면 영향을 받지 않지만 (동작을 맞추는 것이 좋을 수 있습니다).

Logging in production

v4부터 react-query는 프로덕션 모드에서 콘솔에 오류(예: 실패한 요청)를 더 이상 기록하지 않습니다. 이는 많은 사용자에게 혼란을 초래했기 때문입니다. 오류는 여전히 개발 모드에서 표시됩니다.

ESM Support

React Query는 이제 package.json "exports" (opens in a new tab)를 지원하며 Node의 기본 CommonJS 및 ESM 해상도와 완전히 호환됩니다. 대부분의 사용자에게는 주요 변경 사항이 없겠지만, 프로젝트에 가져올 수 있는 파일이 공식적으로 지원하는 진입점으로 제한됩니다.

Streamlined NotifyEvents

QueryCache에 수동으로 구독할 때 QueryCacheNotifyEvent를 받을 수 있었지만, MutationCache에는 적용되지 않았습니다. 이제는 동작을 간소화하고 이벤트 이름도 이에 맞게 조정했습니다.

QueryCacheNotifyEvent

- type: 'queryAdded' // [!code --]
+ type: 'added' // [!code ++]
- type: 'queryRemoved' // [!code --]
+ type: 'removed' // [!code ++]
- type: 'queryUpdated' // [!code --]
+ type: 'updated' // [!code ++]

MutationCacheNotifyEvent

MutationCacheNotifyEventQueryCacheNotifyEvent와 동일한 타입을 사용합니다.

참고: 이 내용은 queryCache.subscribe 또는 mutationCache.subscribe를 통해 캐시에 수동으로 구독하는 경우에만 관련이 있습니다.

Separate hydration exports have been removed

버전 3.22.0 (opens in a new tab)부터, hydration 유틸리티는 React Query 코어로 이동했습니다. v3에서는 여전히 react-query/hydration에서 구버전 내보내기를 사용할 수 있었지만, v4에서는 이러한 내보내기가 제거되었습니다.

- import { dehydrate, hydrate, useHydrate, Hydrate } from 'react-query/hydration' // [!code --]
+ import { dehydrate, hydrate, useHydrate, Hydrate } from '@tanstack/react-query' // [!code ++]

Removed undocumented methods from the queryClient, query and mutation

QueryClientcancelMutatationsexecuteMutation 메서드는 문서화되지 않았고 내부적으로 사용되지 않았기 때문에 제거되었습니다. 이 메서드는 mutationCache에서 사용할 수 있는 메서드의 래퍼에 불과하므로, executeMutation의 기능은 여전히 사용할 수 있습니다.

- executeMutation< // [!code --]
-   TData = unknown, // [!code --]
-   TError = unknown, // [!code --]
-   TVariables = void, // [!code --]
-   TContext = unknown // [!code --]
- >( // [!code --]
-   options: MutationOptions<TData, TError, TVariables, TContext> // [!code --]
- ): Promise<TData> { // [!code --]
-   return this.mutationCache.build(this, options).execute() // [!code --]
- } // [!code --]

추가로, query.setDefaultOptions가 사용되지 않아 제거되었습니다. mutation.cancel은 실제로 요청을 취소하지 않았기 때문에 제거되었습니다.

The src/react directory was renamed to src/reactjs

이전에는 React Query가 react라는 디렉토리를 가지고 있었고, 이는 react 모듈에서 가져왔습니다. 이는 일부 Jest 구성과 충돌을 일으킬 수 있어, 다음과 같은 테스트 오류를 초래할 수 있었습니다:

TypeError: Cannot read property 'createContext' of undefined

디렉토리 이름이 변경됨에 따라 더 이상 문제가 발생하지 않습니다.

프로젝트에서 'react-query/react'에서 직접 가져오던 경우 (단순히 'react-query'가 아닌 경우), 가져오기를 업데이트해야 합니다:

- import { QueryClientProvider } from 'react-query/react'; // [!code --]
+ import { QueryClientProvider } from '@tanstack/react-query/reactjs'; // [!code ++]

New Features 🚀

v4는 다음과 같은 멋진 새로운 기능들을 제공합니다:

Support for React 18

React 18이 올해 초에 출시되었으며, v4는 이제 이를 완벽히 지원하며 새로운 동시성 기능도 제공합니다.

Proper offline support

v3에서는 React Query가 쿼리와 변이를 실행한 후 인터넷에 연결되어 있어야 재시도할 수 있다고 가정했습니다. 이로 인해 여러 가지 혼란스러운 상황이 발생했습니다:

  • 오프라인 상태에서 쿼리를 마운트하면 로딩 상태로 전환되고 요청이 실패하며, 오프라인 상태에서 다시 온라인 상태가 될 때까지 로딩 상태로 유지됩니다.
  • 비슷하게, 오프라인 상태에서 재시도가 꺼져 있는 경우, 쿼리가 실행되었다가 실패하며 오류 상태로 전환됩니다.
  • 네트워크 연결이 필요 없는 쿼리를 오프라인 상태에서 실행하고 싶을 때, 쿼리가 다른 이유로 실패하면 이제는 온라인 상태가 될 때까지 일시 중지됩니다.
  • 창 포커스 재요청이 오프라인 상태에서는 아무런 동작을 하지 않았습니다.

v4에서는 이러한 문제를 해결하기 위해 새로운 networkMode를 도입했습니다. 새로운 Network mode에 대한 페이지를 참조하여 더 많은 정보를 확인해 보세요.

Tracked Queries per default

React Query는 기본적으로 쿼리 속성을 "추적"하여 렌더링 최적화를 개선합니다. 이 기능은 v3.6.0 (opens in a new tab)부터 존재해왔으며, v4에서 기본 동작이 되었습니다.

Bailing out of updates with setQueryData

setQueryData의 함수형 업데이트자를 사용할 때, undefined를 반환하여 업데이트를 중단할 수 있습니다. 이는 undefinedpreviousValue로 제공되어 현재 캐시된 항목이 없고 생성할 수 없을 때 유용합니다. 예를 들어, 할 일을 토글하는 경우 다음과 같습니다:

queryClient.setQueryData(["todo", id], (previousTodo) =>
  previousTodo ? { ...previousTodo, done: true } : undefined
);

Mutation Cache Garbage Collection

이제 mutation도 쿼리와 마찬가지로 자동으로 가비지 컬렉션에 의해 사라질 수 있습니다. 변이의 기본 cacheTime은 5분으로 설정되어 있습니다.

Custom Contexts for Multiple Providers

여러 React Query Provider 인스턴스가 컴포넌트 트리에 있을 때, 훅과 맞는 Provider를 지정할 수 있는 사용자 지정 컨텍스트를 설정할 수 있습니다. 이는 훅이 올바른 Provider 인스턴스를 사용하도록 보장하는 데 중요합니다.

예시:

  1. 데이터 패키지 생성.
// 첫 번째 데이터 패키지: @my-scope/container-data
 
const context = React.createContext<QueryClient | undefined>(undefined);
const queryClient = new QueryClient();
 
export const useUser = () => {
  return useQuery(USER_KEY, USER_FETCHER, {
    context,
  });
};
 
export const ContainerDataProvider = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  return (
    <QueryClientProvider client={queryClient} context={context}>
      {children}
    </QueryClientProvider>
  );
};
  1. 두 번째 데이터 패키지 생성.
// 두 번째 데이터 패키지: @my-scope/my-component-data
 
const context = React.createContext<QueryClient | undefined>(undefined);
const queryClient = new QueryClient();
 
export const useItems = () => {
  return useQuery(ITEMS_KEY, ITEMS_FETCHER, {
    context,
  });
};
 
export const MyComponentDataProvider = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  return (
    <QueryClientProvider client={queryClient} context={context}>
      {children}
    </QueryClientProvider>
  );
};
  1. 어플리케이션에서 두 데이터 패키지 사용.
// 어플리케이션
 
import { ContainerDataProvider, useUser } from "@my-scope/container-data";
import { AppDataProvider } from "@my-scope/app-data";
import { MyComponentDataProvider, useItems } from "@my-scope/my-component-data";
 
<ContainerDataProvider> // <-- 자신의 React Query provider를 사용하여 컨테이너 데이터(예: "user") 제공
  ...
  <AppDataProvider> // <-- 자신의 React Query provider를 사용하여 어플리케이션 데이터 제공 (이 예제에서는 사용되지 않음)
    ...
      <MyComponentDataProvider> // <-- 자신의 React Query provider를 사용하여 컴포넌트 데이터(예: "items") 제공
        <MyComponent />
      </MyComponentDataProvider>
    ...
  </AppDataProvider>
  ...
</ContainerDataProvider>
 
// 위 "DataProvider" 컴포넌트에서 제공하는 훅의 예:
const MyComponent = () => {
  const user = useUser() // <-- ContainerDataProvider에서 지정한 컨텍스트를 사용합니다.
  const items = useItems() // <-- MyComponentDataProvider에서 지정한 컨텍스트를 사용합니다.
  ...
}