본문으로 건너뛰기

React Query 3으로 마이그레이션하기

이전 버전의 React Query는 훌륭했으며 놀라운 새로운 기능과 더 많은 자동화, 전반적으로 개선된 library 경험을 제공했습니다. 또한 대규모 도입을 이끌었고, 그만큼 많은 단련 과정(issues/contributions)을 library에 가져와 library를 더욱 개선하기 위해 다듬어야 할 몇 가지 사항을 드러냈습니다. v3에는 바로 그러한 개선 사항이 포함되어 있습니다.

개요​

  • 확장성과 테스트 용이성이 향상된 캐시 구성
  • 향상된 SSR 지원
  • 어디서나 데이터 지연(이전 명칭: usePaginatedQuery)!
  • 양방향 무한 쿼리
  • 쿼리 데이터 선택기!
  • 사용하기 전에 쿼리 및/또는 뮤테이션의 기본값을 완전히 구성합니다
  • 선택적 렌더링 최적화를 위한 더 세밀한 제어
  • 새로운 useQueries 훅! (가변 길이 병렬 쿼리 실행)
  • useIsFetching() 훅에서 쿼리 필터를 지원합니다!
  • 뮤테이션을 위한 재시도/오프라인/재실행 지원
  • React 외부에서 쿼리/뮤테이션 관찰
  • 원하는 곳 어디에서나 React Query 핵심 로직을 사용하세요!
  • react-query/devtools를 통한 번들형/동일 위치 DevTools
  • 웹 저장소에 캐시 영구 저장(react-query/persistQueryClient-experimental 및 react-query/createWebStoragePersistor-experimental을 통해 실험적으로 제공)

호환성을 깨는 변경 사항​

QueryCache는 QueryClient 인스턴스와 더 낮은 수준의 QueryCache 및 MutationCache 인스턴스로 분리되었습니다.​

QueryCache에는 모든 쿼리가 포함되고, MutationCache에는 모든 뮤테이션이 포함되며, QueryClient는 구성을 설정하고 이들과 상호작용하는 데 사용할 수 있습니다.

여기에는 몇 가지 이점이 있습니다:

  • 다양한 유형의 캐시를 사용할 수 있습니다.
  • 구성이 서로 다른 여러 클라이언트가 동일한 캐시를 사용할 수 있습니다.
  • 클라이언트를 사용하여 쿼리를 추적할 수 있으며, 이를 통해 SSR에서 캐시를 공유할 수 있습니다.
  • 클라이언트 API는 일반적인 사용에 더 중점을 둡니다.
  • 개별 컴포넌트를 더 쉽게 테스트할 수 있습니다.

new QueryClient()를 생성할 때 QueryCache와 MutationCache를 제공하지 않으면 자동으로 생성됩니다.

import { QueryClient } from 'react-query'

const queryClient = new QueryClient()

ReactQueryConfigProvider와 ReactQueryCacheProvider가 모두 QueryClientProvider로 대체되었습니다​

이제 쿼리와 뮤테이션의 기본 옵션을 QueryClient에 지정할 수 있습니다:

이제 defaultConfig가 아니라 defaultOptions이라는 점에 유의하세요

const queryClient = new QueryClient({
defaultOptions: {
queries: {
// query options
},
mutations: {
// mutation options
},
},
})

이제 QueryClientProvider 컴포넌트를 사용하여 QueryClient를 애플리케이션에 연결합니다:

import { QueryClient, QueryClientProvider } from 'react-query'

const queryClient = new QueryClient()

function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}

기본 QueryCache가 사라졌습니다. 이번에는 정말입니다!​

앞서 사용 중단 안내에서 언급했듯이, 이제 기본 패키지에서 생성되거나 내보내지는 기본 QueryCache가 없습니다. new QueryClient() 또는 new QueryCache()를 통해 직접 생성해야 합니다(그런 다음 new QueryClient({ queryCache })에 전달할 수 있습니다)

사용 중단된 makeQueryCache 유틸리티가 제거되었습니다.​

오래 걸렸지만, 드디어 사라졌습니다 :)

QueryCache.prefetchQuery()가 QueryClient.prefetchQuery()로 이동되었습니다​

새로운 QueryClient.prefetchQuery() 함수는 async이지만, 쿼리의 데이터를 반환하지 않습니다. 데이터가 필요하다면 새로운 QueryClient.fetchQuery() 함수를 사용합니다

// Prefetch a query:
await queryClient.prefetchQuery('posts', fetchPosts)

// Fetch a query:
try {
const data = await queryClient.fetchQuery('posts', fetchPosts)
} catch (error) {
// Error handling
}

ReactQueryErrorResetBoundary 및 QueryCache.resetErrorBoundaries()가 QueryErrorResetBoundary 및 useQueryErrorResetBoundary()로 대체되었습니다.​

이들을 함께 사용하면 이전과 동일한 경험을 제공하면서 재설정할 컴포넌트 트리를 선택할 수 있는 제어 기능이 추가됩니다. 자세한 내용은 다음을 참조하세요:

QueryCache.getQuery()가 QueryCache.find()로 대체되었습니다.​

이제 캐시에서 개별 쿼리를 조회하려면 QueryCache.find()를 사용해야 합니다

QueryCache.getQueries()가 QueryCache.findAll()로 이동되었습니다.​

이제 캐시에서 여러 쿼리를 조회하려면 QueryCache.findAll()을 사용해야 합니다.

QueryCache.isFetching이 QueryClient.isFetching()으로 이동되었습니다.​

이제 속성 대신 함수라는 점에 유의합니다

useQueryCache 훅이 useQueryClient 훅으로 대체되었습니다.​

컴포넌트 트리에 제공된 queryClient를 반환하며, 이름 변경 외에는 크게 조정할 필요가 없습니다.

쿼리 키의 부분/조각은 더 이상 쿼리 함수에 자동으로 펼쳐져 전달되지 않습니다.​

이제 인라인 함수는 쿼리 함수에 매개변수를 전달하는 권장 방식입니다:

// Old
useQuery(['post', id], (_key, id) => fetchPost(id))

// New
useQuery(['post', id], () => fetchPost(id))

그래도 인라인 함수를 사용하지 않으려면 새로 전달된 QueryFunctionContext를 사용할 수 있습니다:

useQuery(['post', id], (context) => fetchPost(context.queryKey[1]))

이제 무한 쿼리 페이지 매개변수는 QueryFunctionContext.pageParam을 통해 전달됩니다​

이전에는 쿼리 함수에서 마지막 쿼리 키 매개변수로 추가되었지만, 일부 패턴에서는 사용하기 어려운 것으로 드러났습니다

// Old
useInfiniteQuery(['posts'], (_key, pageParam = 0) => fetchPosts(pageParam))

// New
useInfiniteQuery(['posts'], ({ pageParam = 0 }) => fetchPosts(pageParam))

usePaginatedQuery()는 제거되고 keepPreviousData 옵션으로 대체되었습니다​

새로운 keepPreviousData 옵션은 useQuery와 useInfiniteQuery 모두에서 사용할 수 있으며 데이터에 동일한 "지연" 효과를 줍니다:

import { useQuery } from 'react-query'

function Page({ page }) {
const { data } = useQuery(['page', page], fetchPage, {
keepPreviousData: true,
})
}

useInfiniteQuery()가 이제 양방향으로 작동합니다​

양방향 무한 목록을 완전히 지원하도록 useInfiniteQuery() interface가 변경되었습니다.

  • options.getFetchMore의 이름이 options.getNextPageParam으로 변경되었습니다.
  • queryResult.canFetchMore의 이름이 queryResult.hasNextPage로 변경되었습니다.
  • queryResult.fetchMore의 이름이 queryResult.fetchNextPage로 변경되었습니다.
  • queryResult.isFetchingMore의 이름이 queryResult.isFetchingNextPage로 변경되었습니다.
  • options.getPreviousPageParam 옵션 추가
  • queryResult.hasPreviousPage 속성 추가
  • queryResult.fetchPreviousPage 속성 추가
  • queryResult.isFetchingPreviousPage 추가됨
  • 이제 무한 쿼리의 data는 페이지를 가져오는 데 사용된 pages 및 pageParams를 포함하는 객체입니다: { pages: [data, data, data], pageParams: [...]}

한 방향:

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery(
'projects',
({ pageParam = 0 }) => fetchProjects(pageParam),
{
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
},
)

양방향:

const {
data,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
} = useInfiniteQuery(
'projects',
({ pageParam = 0 }) => fetchProjects(pageParam),
{
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor,
},
)

한 방향이 반전됨:

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery(
'projects',
({ pageParam = 0 }) => fetchProjects(pageParam),
{
select: (data) => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
},
)

이제 Infinite Query 데이터에는 페이지 배열과 해당 페이지를 가져오는 데 사용된 pageParams가 포함됩니다.​

이를 통해 데이터와 페이지 매개변수를 더 쉽게 조작할 수 있습니다. 예를 들어 첫 번째 데이터 페이지를 해당 매개변수와 함께 제거할 수 있습니다:

queryClient.setQueryData(['projects'], (data) => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}))

이제 useMutation은 배열 대신 객체를 반환합니다​

이전 방식은 처음 useState를 발견했을 때의 따뜻하고 포근한 기분을 되살려 주었지만, 그 기분은 오래가지 않았습니다. 이제 뮤테이션 반환값은 단일 객체입니다.

// Old:
const [mutate, { status, reset }] = useMutation()

// New:
const { mutate, status, reset } = useMutation()

mutation.mutate는 더 이상 Promise를 반환하지 않습니다.​

  • [mutate] 변수가 mutation.mutate 함수로 변경되었습니다
  • mutation.mutateAsync 함수 추가

사용자들이 Promise가 일반적인 Promise처럼 동작할 것으로 기대했기 때문에 이 동작에 관한 질문을 많이 받았습니다.

이로 인해 mutate 함수는 이제 mutate 함수와 mutateAsync 함수로 분리되었습니다.

콜백을 사용할 때 mutate 함수를 사용할 수 있습니다:

const { mutate } = useMutation({ mutationFn: addTodo })

mutate('todo', {
onSuccess: (data) => {
console.log(data)
},
onError: (error) => {
console.error(error)
},
onSettled: () => {
console.log('settled')
},
})

async/await를 사용할 때 mutateAsync 함수를 사용할 수 있습니다:

const { mutateAsync } = useMutation({ mutationFn: addTodo })

try {
const data = await mutateAsync('todo')
console.log(data)
} catch (error) {
console.error(error)
} finally {
console.log('settled')
}

이제 useQuery의 객체 구문은 통합된 설정을 사용합니다:​

// Old:
useQuery({
queryKey: 'posts',
queryFn: fetchPosts,
config: { staleTime: Infinity },
})

// New:
useQuery({
queryKey: 'posts',
queryFn: fetchPosts,
staleTime: Infinity,
})

설정하는 경우 QueryOptions.enabled 옵션은 boolean(true/false)이어야 합니다​

이제 enabled 쿼리 옵션은 값이 false일 때만 쿼리를 비활성화합니다. 필요한 경우 값을 !!userId 또는 Boolean(userId)으로 캐스팅할 수 있으며 boolean이 아닌 값이 전달되면 유용한 오류가 발생합니다.

QueryOptions.initialStale 옵션이 제거되었습니다​

initialStale 쿼리 옵션이 제거되었으며 이제 초기 데이터는 일반 데이터로 취급됩니다. 즉, initialData가 제공되면 기본적으로 마운트 시 쿼리를 다시 가져옵니다. 즉시 다시 가져오지 않으려면 staleTime을 정의할 수 있습니다.

QueryOptions.forceFetchOnMount 옵션은 refetchOnMount: 'always'로 대체되었습니다.​

솔직히 refetchOn____ 옵션이 지나치게 많이 누적되고 있었으므로, 이를 통해 깔끔하게 정리할 수 있습니다.

이제 QueryOptions.refetchOnMount 옵션은 모든 쿼리 옵저버가 아니라 상위 컴포넌트에만 적용됩니다​

refetchOnMount가 false로 설정되면 추가 컴포넌트가 마운트 시 다시 가져오기를 수행하지 못했습니다. 버전 3에서는 옵션이 설정된 컴포넌트만 마운트할 때 다시 가져오지 않습니다.

새로운 QueryFunctionContext 객체를 사용하도록 QueryOptions.queryFnParamsFilter가 제거되었습니다.​

이제 쿼리 함수가 쿼리 키 대신 QueryFunctionContext 객체를 받으므로 queryFnParamsFilter 옵션이 제거되었습니다.

QueryFunctionContext에도 쿼리 키가 포함되어 있으므로 쿼리 함수 자체 내에서 매개변수를 계속 필터링할 수 있습니다.

QueryOptions.notifyOnStatusChange 옵션은 새로운 notifyOnChangeProps 및 notifyOnChangePropsExclusions 옵션으로 대체되었습니다.​

이러한 새로운 옵션을 사용하면 컴포넌트가 다시 렌더링되어야 하는 시점을 세부적으로 구성할 수 있습니다.

data 또는 error 속성이 변경될 때만 다시 렌더링합니다:

import { useQuery } from 'react-query'

function User() {
const { data } = useQuery(['user'], fetchUser, {
notifyOnChangeProps: ['data', 'error'],
})
return <div>Username: {data.username}</div>
}

isStale 속성이 변경될 때 다시 렌더링되지 않도록 합니다:

import { useQuery } from 'react-query'

function User() {
const { data } = useQuery(['user'], fetchUser, {
notifyOnChangePropsExclusions: ['isStale'],
})
return <div>Username: {data.username}</div>
}

QueryResult.clear() 함수의 이름이 QueryResult.remove()로 변경되었습니다​

clear이라고 불렸지만, 실제로는 캐시에서 쿼리를 제거하기만 했습니다. 이제 이름이 기능과 일치합니다.

QueryResult.updatedAt 속성이 QueryResult.dataUpdatedAt 및 QueryResult.errorUpdatedAt 속성으로 분리되었습니다​

데이터와 오류가 동시에 존재할 수 있으므로 updatedAt 속성이 dataUpdatedAt 및 errorUpdatedAt으로 분리되었습니다.

setConsole()이 새로운 setLogger() 함수로 대체되었습니다​

import { setLogger } from 'react-query'

// Log with Sentry
setLogger({
error: (error) => {
Sentry.captureException(error)
},
})

// Log with Winston
setLogger(winston.createLogger())

React Native에는 더 이상 로거 재정의가 필요하지 않습니다​

쿼리가 실패할 때 React Native에 오류 화면이 표시되지 않도록 하려면 Console을 수동으로 변경해야 했습니다:

import { setConsole } from 'react-query'

setConsole({
log: console.log,
warn: console.warn,
error: console.warn,
})

버전 3에서는 React Native에서 React Query를 사용하면 이 작업이 자동으로 수행됩니다.

TypeScript​

QueryStatus가 enum에서 union type으로 변경되었습니다​

따라서 쿼리 또는 뮤테이션의 status 속성을 QueryStatus enum 속성과 비교하고 있었다면, 이제 각 속성에 대해 enum이 이전에 보유하던 문자열 리터럴과 비교해야 합니다.

따라서 다음과 같이 enum 속성을 이에 상응하는 문자열 리터럴로 변경해야 합니다:

  • QueryStatus.Idle -> 'idle'
  • QueryStatus.Loading -> 'loading'
  • QueryStatus.Error -> 'error'
  • QueryStatus.Success -> 'success'

다음은 변경해야 할 사항의 예입니다:

- import { useQuery, QueryStatus } from 'react-query'; // [!code --]
+ import { useQuery } from 'react-query'; // [!code ++]

const { data, status } = useQuery(['post', id], () => fetchPost(id))

- if (status === QueryStatus.Loading) { // [!code --]
+ if (status === 'loading') { // [!code ++]
...
}

- if (status === QueryStatus.Error) { // [!code --]
+ if (status === 'error') { // [!code ++]
...
}

새로운 기능​

쿼리 데이터 선택기​

이제 useQuery 및 useInfiniteQuery 훅에는 쿼리 결과의 일부를 선택하거나 변환하는 select 옵션이 있습니다.

import { useQuery } from 'react-query'

function User() {
const { data } = useQuery(['user'], fetchUser, {
select: (user) => user.username,
})
return <div>Username: {data}</div>
}

선택된 데이터가 변경될 때만 다시 렌더링하도록 notifyOnChangeProps 옵션을 ['data', 'error']로 설정합니다.

가변 길이 병렬 쿼리 실행을 위한 useQueries() 훅​

useQuery를 루프에서 실행하고 싶으신가요? 훅 규칙상 불가능하지만, 새로운 useQueries() 훅을 사용하면 가능합니다!

import { useQueries } from 'react-query'

function Overview() {
const results = useQueries([
{ queryKey: ['post', 1], queryFn: fetchPost },
{ queryKey: ['post', 2], queryFn: fetchPost },
])
return (
<ul>
{results.map(({ data }) => data && <li key={data.id}>{data.title})</li>)}
</ul>
)
}

뮤테이션 재시도/오프라인 처리​

기본적으로 React Query는 오류 발생 시 뮤테이션을 재시도하지 않지만, retry 옵션을 사용하면 재시도할 수 있습니다:

const mutation = useMutation({
mutationFn: addTodo,
retry: 3,
})

기기가 오프라인이어서 뮤테이션이 실패하면 기기가 다시 연결될 때 동일한 순서로 재시도됩니다.

뮤테이션 영구 저장​

이제 뮤테이션을 저장소에 영구 저장하고 나중에 재개할 수 있습니다. 자세한 내용은 뮤테이션 문서에서 확인할 수 있습니다.

QueryObserver​

QueryObserver를 사용하여 쿼리를 생성하거나 감시할 수 있습니다:

const observer = new QueryObserver(queryClient, { queryKey: 'posts' })

const unsubscribe = observer.subscribe((result) => {
console.log(result)
unsubscribe()
})

InfiniteQueryObserver​

InfiniteQueryObserver를 사용하여 무한 쿼리를 생성하거나 감시할 수 있습니다:

const observer = new InfiniteQueryObserver(queryClient, {
queryKey: 'posts',
queryFn: fetchPosts,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})

const unsubscribe = observer.subscribe((result) => {
console.log(result)
unsubscribe()
})

QueriesObserver​

QueriesObserver를 사용하여 여러 쿼리를 생성하거나 감시할 수 있습니다:

const observer = new QueriesObserver(queryClient, [
{ queryKey: ['post', 1], queryFn: fetchPost },
{ queryKey: ['post', 2], queryFn: fetchPost },
])

const unsubscribe = observer.subscribe((result) => {
console.log(result)
unsubscribe()
})

특정 쿼리의 기본 옵션 설정​

QueryClient.setQueryDefaults() 메서드를 사용하면 특정 쿼리의 기본 옵션을 설정할 수 있습니다:

queryClient.setQueryDefaults(['posts'], { queryFn: fetchPosts })

function Component() {
const { data } = useQuery(['posts'])
}

특정 뮤테이션의 기본 옵션 설정​

QueryClient.setMutationDefaults() 메서드를 사용하여 특정 뮤테이션의 기본 옵션을 설정할 수 있습니다:

queryClient.setMutationDefaults(['addPost'], { mutationFn: addPost })

function Component() {
const { mutate } = useMutation({ mutationKey: ['addPost'] })
}

useIsFetching()​

이제 useIsFetching() 훅은 예를 들어 특정 유형의 쿼리에 대해서만 spinner를 표시하는 데 사용할 수 있는 필터를 받습니다:

const fetches = useIsFetching({ queryKey: ['posts'] })

Core 분리​

이제 React Query의 코어가 React에서 완전히 분리되어 독립 실행형으로 또는 다른 프레임워크에서도 사용할 수 있습니다. 코어 기능만 가져오려면 react-query/core 진입점을 사용하세요:

import { QueryClient } from 'react-query/core'

이제 Devtools는 기본 저장소와 npm 패키지에 포함됩니다​

이제 DevTools는 react-query 패키지 자체에 react-query/devtools import로 포함됩니다. react-query-devtools import를 react-query/devtools로 교체하기만 하면 됩니다