참고자료
본래 React Query 라는 이름으로 시작됐으나, v4부터 Vue나 Svelte 등 다른 프레임워크에서도 활용할 수 있도록 기능이 확장되면서 Tanstack Query 라는 이름으로 변경되었다.
Tanstack Query의 대표적인 기능
- 데이터 가져오기 및 캐싱
- 동일 요청의 중복 제거
- 신선한 데이터 유지
- 무한 스크롤, 페이지네이션 등의 성능 최적화
- 네트워크 재연결, 요청 실패 등의 자동 갱신
Tanstack Query를 활용해서 데이터를 가져올 때는 항상 쿼리 키(queryKey)를 지정하게 된다.
쿼리 키는 캐시된 데이터와 비교해 새로운 데이터를 가져올 지, 캐시된 데이터를 사용할 지 결정하는 기준이 된다.
import { useQuery } from '@tanstack/react-query'
export default function DelayedData() {
const { data } = useQuery({
queryKey: ['delay'], // 쿼리 키
queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json()
})
return <div>{JSON.stringify(data)}</div>
}
Tanstack Query는 캐시한 데이터를 신선(fresh)하거나 상한(stale) 상태로 구분해 관리한다.
데이터가 상하는 데까지 걸리는 시간은 staleTime 옵션으로 지정할 수 있으며, 신선한지 상했는지 여부는 isStale로 확인할 수 있다.
import { useQuery } from '@tanstack/react-query'
export default function DelayedData() {
const { data, isStale } = useQuery({
queryKey: ['delay'],
queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json(),
staleTime: 1000 * 10 // 10초 후 상함. 즉, 10초 동안 신선함.
})
return (
<>
<div>데이터가 {isStale ? '상했어요..' : '신선해요!'}</div>
<div>{JSON.stringify(data)}</div>
</>
)
}
가장 기본적인 쿼리 훅으로, 컴포넌트에서 데이터를 가져올 때 사용한다.
const 반환 = useQuery<데이터타입>(옵션)
쿼리 키는 쿼리를 식별하는 고유한 값으로, 배열 형태로 지정한다.
다중 아이템 쿼리 키를 사용할 때에는 아이템의 순서가 중요하다.
// 단일 아이템 쿼리 키
useQuery({ queryKey: ['hello'] })
// 다중 아이템 쿼리 키
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2 }] })
// 서로 같은 쿼리
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2 }] })
useQuery({ queryKey: ['hello', 'world', 123, { b: 2, c: undefined, a: 1 }] })
// 서로 다른 쿼리
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2 }] })
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2, c: 3 }] })
useQuery({ queryKey: ['hello', 'world'] })
useQuery({ queryKey: [123, 'world', { a: 1, b: 2, c: 3 }, 'hello'] }) // 순서 다른 다중 아이템 쿼리 키
다음 예시의 wait prop의 값이 다르면, 각각 별개의 요청을 전송한다.
import { useQuery } from '@tanstack/react-query'
type ResponseValue = {
message: string
time: string
}
export default function DelayedData({ wait = 1000 }: { wait: number }) {
const { data } = useQuery<ResponseValue>({
queryKey: ['delay', wait],
queryFn: async () => (await fetch(`https://api.heropy.dev/v0/delay?t=${wait}`)).json(),
staleTime: 1000 * 10
})
return <div>{data?.time}</div>
}
기본적으로 쿼리 함수(
queryFn)에서 사용하는 변수는 쿼리 키에 포함되어야 한다.
- 그러면 변수가 변경될 때마다 자동으로 다시 가져올 수 있다.
쿼리 함수(queryFn)는 데이터를 가져오는 비동기 함수로, 꼭 데이터를 반환하거나 오류를 던져야 한다.
error 객체로 확인할 수 있으며, 기본적으로 null 값이다.import { useQuery } from '@tanstack/react-query'
type ResponseValue = {
message: string
time: string
}
export default function DelayedData() {
const { data, error } = useQuery<ResponseValue>({
queryKey: ['delay'],
queryFn: async () => {
const res = await fetch('https://api.heropy.dev/v0/delay?t=1000')
const data = await res.json()
if (!data.time) {
throw new Error('문제가 발생했습니다!')
}
return data
},
staleTime: 1000 * 10,
retry: 1
})
return (
<>
{data && <div>{JSON.stringify(data)}</div>}
{error && <div>{error.message}</div>}
</>
)
}
선택 함수(select)를 사용하면 가져온 데이터를 변형(선택)할 수 있다.
쿼리 함수가 반환하는 데이터를 인수로 받아 선택 함수에서 처리하고 반환하면 최종 데이터가 된다.
새로운 데이터를 가져오는 과정에서는 쿼리가 무효화되어 일시적으로 데이터가 없는 상태(undefined) 가 된다.
이 때 데이터 출력 화면이 깜빡일 수 있다.
이런 현상을 방지하기 위해 placeholderData 옵션을 사용하면, 쿼리 함수가 호출되는 대기 상태(pending)에서 임시로 표시할 데이터를 미리 지정할 수 있다.
placeholderData 옵션에는 함수를 지정할 수 있으며, 이 함수는 새로운 데이터를 가져오기 직전의 이전(previous) 데이터를 받아올 수 있어서, 이를 반환해 임시 데이터로 사용할 수 있다.
const { data: movies } = useQuery<Movie[]>({
queryKey: ['movies', searchText], // 검색어
queryFn: async () => {
const res = await fetch(`https://omdbapi.com?apikey=7035c60c&s=${searchText}`)
const { Search: movies } = await res.json()
return movies
},
placeholderData: prev => prev
})
새로운 데이터를 가져올 때 이전 데이터와 비교해 변경되지 않은 부분은 이전 데이터를 재사용하도록 지정할 수 있다.
structuralSharing 이 true인 경우, 변경된 부분만 새롭게 업데이트하고 변경되지 않은 부분은 이전 데이터의 참조를 재사용한다.// 이전 데이터
const prevUser = {
id: 'abc123',
name: 'Neo',
age: 22,
contact: {
email: 'neo@gmail.com',
address: {
country: 'Korea',
city: 'Seoul'
}
}
}
// 새로운 데이터
const newUser = {
id: 'abc123',
name: 'Neo',
age: 22,
contact: {
email: 'neo@gmail.com',
address: {
country: 'Korea',
city: 'Suwon' // 변경된 부분!
}
}
}
하지만, 때로는
structuralSharing옵션을false로 지정하는 것이 더 유리할 때도 있다.
- 매우 큰 중첩 객체를 다루는 경우, 구조적인 비교 자체가 성능에 부담될 수 있다.
- 데이터가 항상 새로운 참조여야 하거나, 데이터가 단순해 깊은 비교가 필요하지 않은 경우에도
false가 낫다.
meta 속성은 쿼리에 대한 추가 정보를 제공할 수 있다.
쿼리 클라이언트 생성의 queryCache 옵션에서 호출 쿼리의 추가 정보인 meta 를 얻을 수 있다.
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (_error, query) => {
alert(query.meta?.myErrorMessage) // 오류 메시지 출력!
}
})
})
const { data: movies } = useQuery<Movie[]>({
queryKey: ['movies', searchText], // 검색어
queryFn: async () => {
const res = await fetch(`https://omdbapi.com?apikey=7035c60c&s=${searchText}`)
const { Search: movies } = await res.json()
return movies
},
meta: {
myErrorMessage: '영화를 검색할 수 없어요!'
}
})
isFetching은 쿼리 함수(queryFn)가 실행 중인지의 여부로, 데이터를 가져오는 중임을 나타낸다.isPending은 캐시된 데이터가 없고, 쿼리가 아직 완료되지 않은 상태의 여부를 나타낸다. enabled 옵션이 없으면 쿼리가 대기 상태로 시작하므로 true를 반환initialData 혹은 placeholderData 옵션으로 데이터를 제공하면 pending 상태가 필요하지 않으므로 false를 반환isLoading은 isFetching && isPending 과 같은 의미로, 쿼리의 첫 번째 가져오기가 진행 중인 경우를 나타낸다.refetch 함수를 사용하면, 데이터를 항상 새롭게 다시 가져올 수 있다.
queryClient.fetchQuery() 메소드를 사용하면, 신선도(staleTime) 기반으로 데이터를 가져올 수 있다.
- 주의할 점은, queryKey와 staleTime을 기존 쿼리와 동일하게 제공해야 한다.
queryClient.getQueryData() 메소드를 사용하면, 캐시된 데이터를 가져올 수 있다.
- 데이터가 상해도 새로 가져오지 않고 캐시된 데이터만 반환한다.
- 캐시된 데이터가 없는 경우, undefined를 반환한다.
queryClient.ensureQueryData() 메소드를 사용하면, 캐시된 데이터가 없는 경우에도 undefined가 아니라 자동으로 queryClient.fetchQuery() 메소드를 호출해서 데이터를 가져온다.
const 반환 = useInfiniteQuery<페이지타입>(옵션)
추가 데이터를 더 가져오거나, 무한 스크롤 기능을 구현하기 위해 사용되는 훅.
const 반환 = useMutation(옵션)
Tanstack Query는 데이터 변경 작업(생성, 수정, 삭제 등)을 위한 useMutation 훅을 제공한다.
이를 통해 데이터 변경 작업을 처리하고 다양한 성공, 실패, 로딩 등의 상태를 얻을 수 있다
그리고 요청 실패 시의 자동 재시도나 낙관적 업데이트 같은 고급 기능도 쉽게 처리할 수 있다.
낙관적 업데이트(optimistic update)?
- 서버 요청의 응답을 기다리지 않고 먼저 UI를 업데이트하는 기능을 일컫는다.
- 서버 응답이 느린 상황에서도 빠른 인터페이스를 제공할 수 있어 사용자 경험을 크게 향상시킬 수 있다.
onMutate: async newUser => { // 낙관적 업데이트 전에 사용자 목록 쿼리를 취소해 잠재적인 충돌 방지! await queryClient.cancelQueries({ queryKey: ['users'] }) // 캐시된 데이터(사용자 목록) 가져오기! const previousUsers = queryClient.getQueryData<Users>(['users']) // 낙관적 업데이트 if (previousUsers) { queryClient.setQueryData<Users>(['users'], [...previousUsers, newUser]) } // 각 콜백의 context로 전달할 데이터 반환! return { previousUsers } }, }
변이(Mutation)는 조회(Read)를 제외한 데이터 생성(Create), 수정(Update), 삭제(Delete) 작업을 수행한다.