본문으로 건너뛰기

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-experimentalreact-query/createWebStoragePersistor-experimental을 통해 실험적으로 제공)

호환성을 깨는 변경 사항

QueryCacheQueryClient 인스턴스와 더 낮은 수준의 QueryCacheMutationCache 인스턴스로 분리되었습니다.

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

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

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

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

import { QueryClient } from 'react-query'

const queryClient = new QueryClient()

ReactQueryConfigProviderReactQueryCacheProvider가 모두 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
}

ReactQueryErrorResetBoundaryQueryCache.resetErrorBoundaries()QueryErrorResetBoundaryuseQueryErrorResetBoundary()로 대체되었습니다.

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

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

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

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

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

QueryCache.isFetchingQueryClient.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 옵션은 useQueryuseInfiniteQuery 모두에서 사용할 수 있으며 데이터에 동일한 "지연" 효과를 줍니다:

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는 페이지를 가져오는 데 사용된 pagespageParams를 포함하는 객체입니다: { 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 옵션은 모든 쿼리 옵저버가 아니라 상위 컴포넌트에만 적용됩니다

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

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

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

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

QueryOptions.notifyOnStatusChange 옵션은 새로운 notifyOnChangePropsnotifyOnChangePropsExclusions 옵션으로 대체되었습니다.

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

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.dataUpdatedAtQueryResult.errorUpdatedAt 속성으로 분리되었습니다

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

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

QueryStatusenum에서 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 ++]
...
}

새로운 기능

쿼리 데이터 선택기

이제 useQueryuseInfiniteQuery 훅에는 쿼리 결과의 일부를 선택하거나 변환하는 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로 교체하기만 하면 됩니다