공식문서 좀 읽어보기. 많은 내용이 빠져있습니다 :)
프론트에서 상태를 다루는 일은 매우 흔한일! 그 상태는 크게 두가지로 나눠볼 수 있다.
유저와의 인터렉셕에 의해서 발생하는 상태를 의미한다. 예를들면 모달을 연다던가?
api 통신을 통해서 서버에서 받아온 서버 데이터를 의미한다.
전통전인 상태관리 라이브러리들은 클라이언트 서버를 다루는 것에는 훌륭했지만, 서버 상태나 async와 작업하기에는 충분치 못했다.
This is because server state is totally different
서버 상태는 다음과 같은 특징을 가진다.
서버의 특성을 살펴보면 해결해야 할 문제는 산개해 있다.
이런 다양한 문제를 모두 해결했으면 이 라이브러리를 사용하지 않아도 될듯..?
React Query는 서버 상태를 관리하기 위한 최고의 라이브러리 중 하나이다.
React Query를 사용함으로써 클라이언트 상태와 서버 상태를 분리시킬 수 있다.
React Query의 3가지 핵심 컨셉
Query Devtools는 process.env.NODE_ENV === 'development'
일 때만 번들에 포함되므로 프로덕션 빌드 중에 제외하는 것에 대해 걱정할 필요가 없다.
Devtools는 프로덕션 빌드에서 제외된다. 그러나 프로덕션에서 devtools를 지연 로드하는 것이 바람직할 수 있다.
code
기본적으로 useQuery 또는 useInfiniteQuery를 통한 Query instance는 캐시된 데이터를 오래된 것으로 간주한다.
이러한 동작을 변경하려면 staleTime 옵션을 사용하여 쿼리를 전역적으로 구성하고 쿼리별로 구성할 수 있다. 더 긴 staleTime을 지정하면 쿼리가 데이터를 자주 다시 가져오지 않음을 의미한다.
상한 queries는 다음과 같은 상황에 자동적으로 백그라운드에서 refetch 된다.
예상치 못한 refetch가 발생하면, 창에 포커스가 되어있고, 탄스택 쿼리가 refetchOnWindowFocus를 수행하고 있기 때문일 수 있다.
이런 기능을 변경하려면 refetchOnMount, refetchOnWindowFocus, refetchOnReconnect 및 refetchInterval과 같은 옵션을 사용할 수 있다.
더 이상 사용되지 않는 인스턴스인 useQuery, useInfiniteQuery 또는 query observers들의 Query는 비활성 레이블로 지정되며 나중에 다시 사용될 경우에도 캐시에 남아있다.
기본적으로 비활성 쿼리들은 5분 후에 가비지 콜렉터로부터 회수당한다.
이를 바꾸고 싶다면, cacheTime의 값을 변경할 수 있다.
이를 바꾸고 싶다면 retry 와 retryDelay의 값을 변경할 수 있다.
query는 프로미스 기반의 method(GET, POST)로 서버로부터 data를 fetch할 수 있다. 만약 서버의 데이터를 수정해야 한다면, Mutations를 대신 사용하는 것을 권장한다.
당신의 컴포넌트나 커스텀 훅에서 query를 구독하기 위해서는 최소한 다음과 같이 useQuery를 호출해라.
당신이 제공하는 unique key는 당신의 쿼리들을 app 전체에서 refetching, caching, sharing하기 위해 내부적으로 사용된다.
import { useQuery } from '@tanstack/react-query'
function App() {
const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}
result
객체에는 몇가지 생산성을 위한 아주 중요한 sates가 포함되어 있다. 쿼리는 주어진 순간(given moment)에 다음 중 하나의 상태만 가질 수있다.
이런 기본 상태 말고도, 쿼리 상태에 따라 더 많은 정보를 사용할 수 있다.
function Todos() {
//const { isLoading, isError, data, error } = useQuery({
// queryKey: ['todos'],
// queryFn: fetchTodoList,
//})
const { status, data, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
if (status === 'loading') {
return <span>Loading...</span>
}
if (status === 'error') {
return <span>Error: {error.message}</span>
}
// also status === 'success', but "else" logic works, too
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
추가적으로 status나 result object 말고도 다음 옵션이 있는 fetchStatus 프로퍼티를 얻을 수 있다.
백그라운드 refetches와 오래된 검증 로직은 모든 status
와 fetchStatus
조합을 만들 수 있다.
success
상태의 쿼리는 일반적으론 idle
fetchStatus이지만, 백그라운드 refetch가 발생했을때 fetching
에도 있을 수 있다.loading
과 fetching
fetchStatus 상태지만 네트워크 연결이 없는 경우 pause
될 수도 있습니다.따라서 실제로 데이터를 가져오지 않고도 쿼리가 로드 상태에 있을 수 있음을 명심하자!
status
는 data
에 대한 정보를 제공한다. 데이터가 있는지 없는지?fetchStatus
는 queryFn
의 정보를 제공한다. 지금 실행되고 있는지 아닌지?본질적으로 TanStack Query는 쿼리 키를 기반으로 쿼리 캐싱을 관리한다. 쿼리 키는 top level의 배열이어야 하며 단일 문자열이 포함된 배열처럼 단순하거나 많은 문자열 및 중첩 개체의 배열처럼 복잡할 수 있다. 쿼리 키가 직렬화 가능하고 쿼리 데이터에 고유한 한 사용할 수 있다!!
// A list of todos
useQuery({ queryKey: ['todos'], ... })
// Something else, whatever!
useQuery({ queryKey: ['something', 'special'], ... })
// An individual todo
useQuery({ queryKey: ['todo', 5], ... })
// An individual todo in a "preview" format
useQuery({ queryKey: ['todo', 5, { preview: true }], ...})
// A list of todos that are "done"
useQuery({ queryKey: ['todos', { type: 'done' }], ...
객체의 키 순서에 관계없이 다음 쿼리는 모두 동일하게 간주된다.
useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})
useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... })
다음과 같은 경우는 같지 않다.
useQuery({ queryKey: ['todos', status, page], ... })
useQuery({ queryKey: ['todos', page, status], ...})
useQuery({ queryKey: ['todos', undefined, page, status], ...})
쿼리 키는 가져오는 데이터를 uniquely하게 설명하므로 변경되는 쿼리 기능에 사용하는 변수를 포함해야 한다.
function Todos({ todoId }) {
const result = useQuery({
queryKey: ['todos', todoId],
queryFn: () => fetchTodoById(todoId),
})
}
쿼리 함수는 promise를 반환하는 모든 함수가 될 수 있다.
다음과 같이 작성 가능하다.
useQuery({ queryKey: ['todos'], queryFn: fetchAllTodos })
useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) })
useQuery({
queryKey: ['todos', todoId],
queryFn: async () => {
const data = await fetchTodoById(todoId)
return data
},
})
useQuery({
queryKey: ['todos', todoId],
queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]),
})
parallel
쿼리는 병렬로 실행되거나, 동시에 maximize fetching concurrency 하는 쿼리이다.
병렬 쿼리수가 변경되지 않는다면(?), parallel 쿼리를 사용할 필요가 없다.
function App () {
// The following queries will execute in parallel
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams })
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })
...
}
const results = useQueries({
queries: [
{ queryKey: ['movieList', 1], queryFn: () => getMovieList({}) },
{ queryKey: ['movieList', 2], queryFn: () => getMovieList({ limit: 20, sort_by: 'peers'}) },
{ queryKey: ['movieList', 1], queryFn: () => getMovieList({ limit: 15, sort_by: 'rating', order_by: 'asc'}) }
]
})
// results는 결과 배열을 반환 받는다.
병렬 처리 문서를 읽다 문득 의문점이 생겼다. 이 동작은 병렬이라 그랬으니까... 누가 먼저 올지는 모르겠네?
const { movieList, isLoading } = useGetMoviList({
limit: 10,
sort_by: 'rating',
order_by: 'asc'
})
const { movieList: movieList2, isLoading: isLoading2 } = useGetMoviList({
limit: 20,
sort_by: 'peers',
order_by: 'asc'
})
const { movieList: movieList3, isLoading: isLoading3 } = useGetMoviList({
limit: 15,
sort_by: 'title',
order_by: 'desc'
})
const { movieList: movieList4, isLoading: isLoading4 } = useGetMoviList({
limit: 35,
sort_by: 'year',
order_by: 'desc'
})
// 누가 먼저 결과를 받아올지 모름
console.log(movieList, movieList2, movieList3, movieList4);
// [결과1], undefined, undefined, undefined
// [결과1], undefined, [결과2], undefined
// 뭐 이런식으로 뒤죽박죽
위 같은 경우 누가 먼저 올지 알 수 없었다. 말 그대로 병렬 처리되면서, 먼저 오는대로 새롭게 앱을 렌더링하고 있었다.자, 그렇다면 useQueries를 보자.
const results = useQueries({
queries: [
{ queryKey: ['movieList', 1], queryFn: () => getMovieList({}) },
{ queryKey: ['movieList', 2], queryFn: () => getMovieList({ limit: 20, sort_by: 'peers' }) },
{ queryKey: ['movieList', 1], queryFn: () => getMovieList({ limit: 15, sort_by: 'rating', order_by: 'asc' }) }
]
})
역시 누가 먼저 올지 알 수 없다.
찾아보니 enabled 옵션이 있다.
const { movieList, isLoading } = useGetMoviList({
limit: 10,
sort_by: 'rating',
order_by: 'asc',
})
const { movieList: movieList2, isLoading: isLoading2 } = useGetMoviList({
limit: 15,
sort_by: 'peers',
order_by: 'asc',
option: {
enabled: !!movieList
}
})
const { movieList: movieList3, isLoading: isLoading3 } = useGetMoviList({
limit: 20,
sort_by: 'title',
order_by: 'desc',
option: {
enabled: !!movieList2
}
})
const { movieList: movieList4, isLoading: isLoading4 } = useGetMoviList({
limit: 35,
sort_by: 'year',
order_by: 'desc',
option: {
enabled: !!movieList3
}
})
// [결과1], undefined, undefined, undefined
// [결과1], [결과2], undefined, undefined
// [결과1], [결과2], [결과3], undefined
// [결과1], [결과2], [결과3], [결과4]
확실히 받아오는 속도가 느려졌지만, 앞의 데이터를 의존함으로써, 앞의 데이터가 있어야만 다음 호출이 일어났다.
그럼 useQueries를 사용해서 위와 같은 결과를 얻고 시퍼! 어떡해?
const results = useQueries({
queries: [
{ queryKey: ['movieList', 1], queryFn: () => getMovieList({}), enabled: true },
{ queryKey: ['movieList', 2], queryFn: () => getMovieList({ limit: 20, sort_by: 'peers' }), enabled: true },
{ queryKey: ['movieList', 3], queryFn: () => getMovieList({ limit: 15, sort_by: 'rating', order_by: 'asc' }), enabled: true }
]
})
const loading = results.some(result => result.isLoading)
if(loading) {
return <>Loading...</>
}
console.log(results); // loading을 통과해야만 log가 찍힌다!
some통해 loading이 하나라도 있는지 체크하다가 loading이 없어지면, 즉, 모두 호출되면 log가 찍히는 것이다.
잠깐 다른 길로 샜다. 다시 돌아가자.
아니! 바로 다음장에 enabled가 나왔다. 히히
쿼리의 status
=== loading
상태는 쿼리의 초기 하드로딩 상태를 표시하기에 충분하지만, 쿼리가 백그라운드에서 refetch 중임을 나타내는 추가 표시기를 표시할 수도 있다. 이를 위해 쿼리는 상태 변수의 상태에 관계없이 fetching 상태임을 표시하는 데 사용할 수 있는 isFetching 부울을 제공한다.
개별적인 쿼리 로드 상태 외에도, 어떠한 query든지 fetching될 때 전역적으로 loading 표시기를 보여주고 싶다면, useIsFetching
hook을 사용할 수 있다.
import { useIsFetching } from '@tanstack/react-query'
// ...
const isFetching = useIsFetching()
if(isFetching) {
return <h1>Queries are fetching in the background...</h1>
}
유저가 앱에서 벗어났다가 돌아왔고, 쿼리 데이터가 stale이라면, 백그라운드에서 자동으로 fresh data를 request한다. 이를 글로벌적으로, 혹은 개별적 쿼리에서 disable 할 수 있다.
위의 예제를 다시 재활용 해보자.
const { movieList, isLoading } = useGetMoviList({
limit: 10,
sort_by: 'rating',
order_by: 'asc',
})
const { movieList: movieList2, isLoading: isLoading2 } = useGetMoviList({
limit: 15,
sort_by: 'peers',
order_by: 'asc',
option: {
enabled: !!movieList
}
})
const { movieList: movieList3, isLoading: isLoading3 } = useGetMoviList({
limit: 20,
sort_by: 'title',
order_by: 'desc',
option: {
enabled: !!movieList2,
refetchOnWindowFocus: false
}
})
const { movieList: movieList4, isLoading: isLoading4 } = useGetMoviList({
limit: 35,
sort_by: 'year',
order_by: 'desc',
option: {
enabled: !!movieList3,
refetchOnWindowFocus: false
}
})
window를 focus 했을 때 네트워크 탭에는 두개만 호출되는 것을 확인할 수 있다.
커스텀 하고 싶다면 focusManager
를 사용하면 된다.
쿼리가 자동으로 실행되지 않도록 하려면 enabled = false
옵션을 사용할 수 있다.
마찬가지로 위 예제에서 option으로 enabled = false
를 부여하면, 첫 fetch조차 일어나지 않는것을 확인할 수 있었다.
status === success
또는 isSuccess
상태로 초기화된다.status === loading
및 fetchStatus === loading
상태에서 시작된다.활성화 옵션은 쿼리를 영구적으로 비활성화할 뿐만 아니라 나중에 활성화/비활성화할 수도 있다. 사용자가 필터 값을 입력한 후 첫 번째 요청만 실행하려는 필터 양식이 좋은 예이다.
function Todos() {
const [filter, setFilter] = React.useState('')
const { data } = useQuery({
queryKey: ['todos', filter],
queryFn: () => fetchTodos(filter),
// ⬇️ disabled as long as the filter is empty
enabled: !!filter
})
return (
<div>
// 🚀 applying the filter will enable and execute the query
<FiltersForm onApply={setFilter} />
{data && <TodosTable data={data}} />
</div>
)
}
Lazy queries는 loading
이 의미하는 바가 데이터가 아직 없다는 뜻이니까 status: 'loading'
이 된다. 기술적으로는 맞는 말이지만, 현재 데이터를 가져오지 않았기 때문에(비활성화 이기 때문에) 이 flag로는 로딩 스피너 여부를 표시할 수 없다.
만약 disabled나 lazy queries를 사용하는 경우에는 isInitialLoading
flag를 대신 사용할 수 있다. 이는 isLoading && isFetching
이다.
따라서 쿼리가 현재 처음으로 가져오는 경우에만 해당된다.
제목처럼 쿼리를 재시도 하는 것이다. 당연~히 실패했을때 재시도 여부를 설정할 수 있다.
제목만 봐도 바로 알 수 있을것이다.
기본 retryDelay는 시도할 때마다 두 배(1000ms부터 시작)로 설정되지만 30초를 초과할 수 없다.
일반적으로 useQuery
를 사용해서 페이지네이트를 하게되면 각각의 새로운 페이지가 새로운 쿼리처럼 생성되기 때문에 UI 는 success
와 loading
상태에서 점프한다(?)
아, 영어 못하니까 역시 직접 써봤다.
const Paginate = () => {
const [page, setPage] = useState(1)
const { data, isLoading} = useMovieListQuery({
limit: 10,
page,
option: {
// keepPreviousData: true,
staleTime: 1 * 6000
}
})
const clickPrevious = () => {
setPage(cur => {
if(cur === 1) return 1
return cur - 1
})
}
const clickNext = () => {
setPage(cur => cur + 1)
}
if(isLoading) return <h1>Loading...</h1>
return (
<div style={{ textAlign: 'center'}}>
<ol style={{ padding: '100px'}}>
{data?.data.data.movies?.map(movie => {
return (
<li style={{ display: 'inline-block'}} key={movie.id}>
<Image src={movie.large_cover_image} alt={movie.description_full} width={200} height={350} priority />
</li>
)
})}
</ol>
<span>Current Page: { page }</span>
<button onClick={clickPrevious}>이전 페이지</button>
<button onClick={clickNext}>다음 페이지</button>
</div>
)
}
export default Paginate
keepPreviousData를 적용하면 페이지를 넘길때, 로딩대신 이전 데이터를 유지시키기 때문에 이전 페이지 결과값이 화면에 남아있다.
keepPreviousData를 빼버리면 페이지를 넘길때<h1>Loading...</h1>
이 출력된다.
스크롤을 내릴때, 특정 지점에서 query로 nextFetch를 한다.
// useMovieListInfiniteQuery.ts
import { getMovieList } from '@/api/movie'
import { useInfiniteQuery } from '@tanstack/react-query'
import { getMovieParmI } from './useMovieListQuery'
const useMovieListInfiniteQuery = (param: getMovieParmI) => {
const result = useInfiniteQuery({
queryKey: ['movieListInf'],
queryFn: ({ pageParam = 1}) => getMovieList({
page: pageParam
}),
getNextPageParam: (lastPage) => lastPage.data.data.page_number + 1,
...param.option
})
return result
}
export default useMovieListInfiniteQuery
// useIntersectionObserver.ts
import { useEffect, useState } from 'react';
interface useIntersectionObserverProps {
onIntersect: IntersectionObserverCallback;
}
const useIntersectionObserver = ({
onIntersect,
}: useIntersectionObserverProps) => {
const [target, setTarget] = useState<HTMLElement | null>(null);
useEffect(() => {
if (!target) return;
const observer: IntersectionObserver = new IntersectionObserver(onIntersect);
observer.observe(target);
return () => observer.unobserve(target);
}, [onIntersect, target]);
return { setTarget };
};
export default useIntersectionObserver;
import useIntersectionObserver from '@/hooks/intersectionObserver/useIntersectionObserver'
import useMovieListInfiniteQuery from '@/hooks/query/useMovieListInfiniteQuery'
import Image from 'next/image'
import React from 'react'
const InfiniteScroll = () => {
const { data, isLoading, isError, fetchNextPage } = useMovieListInfiniteQuery({
page: 0
})
const onIntersect: IntersectionObserverCallback = ([{ isIntersecting }]) => {
if(isIntersecting) {
fetchNextPage()
}
}
const { setTarget } = useIntersectionObserver({onIntersect})
if (isLoading) return <h1>Loading...</h1>
if (isError) return <h1>Error...</h1>
return (
<main style={{ width: '100%', border: '1px solid white', minHeight: '700px', position: 'relative'}}>
{data.pages.map((item, i) => (
<React.Fragment key={i}>
{item.data.data.movies.map(movie => (
<Image key={movie.id} src={movie.large_cover_image} alt={movie.description_full} width={200} height={350} priority />
))}
</React.Fragment>
))}
<button onClick={() => fetchNextPage()}>버튼</button>
<div ref={setTarget} style={{ border: '1px solid green', width: '100%', height: '100px', bottom: '100px', position: 'absolute' }}></div>
</main>
)
}
export default InfiniteScroll
잘 된다!
무한 쿼리가 오래되어 다시 페치해야 할 경우 각 그룹은 첫 번째 그룹부터 순차적으로 페치된다. 무한 쿼리의 결과가 queryCache에서 제거되면 초기 그룹만 요청된 상태에서 pagination이 초기 상태에서 다시 시작된다.
캐시에 쿼리에 대한 초기 데이터를 제공하는 방법은 여러가지가 있다.
initialData
는 캐시에 남는다. 그렇기에 placeholder, 부분적 또는 불완전한 데이터에 이 옵션을 부여하지 말고, 대신 placeholderData를 사용해라!!
공식문서를 봐도 뽝! 느낌이 안와서 걍 사용해봤다.
const InitialData = () => {
const result = useQuery({
queryKey: ["movieList"],
queryFn: () => getMovieList({}),
initialData: {
config: {},
data: {
'@meta': {},
data: {
limit: 0,
movie_count: 0,
movies: [{
id: 1,
url: '',
large_cover_image: 'https://yts.torrentbay.to/assets/images/movies/fours_a_crowd_2022/large-cover.jpg',
title: '',
description_full: ''
}],
page_number: 1,
},
status: '',
status_message: ''
},
headers: {},
request: {},
status: 1,
statusText: ''
}
})
if(result.isLoading) return <h1>Loading...</h1>
return (
<ol>
{result.data?.data.data.movies.map(result => {
return (
<li key={result.id} style={{display: 'inline-block'}}>
<Image src={result.large_cover_image} alt={result.description_full} width={200} height={350} priority></Image>
</li>
)
})}
</ol>
)
}
export default InitialData
로딩을 거치지 않는것을 확인했다.
기존에 초기값, 그러니까 내가 억지로 넣은 initialData값으로 사진한장이 덜렁 나오다가 data를 fetch 해 오면, 해당 결과 바뀌는 것을 확인했다. 말 그대로 initialData였다.
그 이후 나오는 여러 예제가 있는데 이중 initialDataUpdatedAt은 솔직히 이해가 안간다. 관련해서 tkdodo 씨의 블로그 글을 봐야겠다.
initialData와 비슷하지만 캐시에 남지 않는다. 이 기능은 실제 데이터가 백그라운드에서 가져와지는 동안 쿼리를 성공적으로 렌더링하기에 충분한 부분(또는 가짜) 데이터가 있는 상황에서 유용하다.
위 initialData예제에서 placeholderData로 바꾸고 해봐도 결과가 같다.
이하, chat gpt의 답변이다.
React Query의 initial query data와 placeholder query data는 비슷해 보일 수 있지만 목적이 다릅니다.
initial query data는 쿼리가 처음 실행될 때 UI에 표시될 데이터입니다. 이 데이터는 일반적으로 캐시에서 가져온 데이터이거나, 서버에서 가져온 데이터가 없을 때 사용되는 기본값입니다.
반면, placeholder query data는 새로운 데이터를 가져오기 전에 UI에 표시할 데이터입니다. 이 데이터는 일반적으로 실제 서버에서 가져올 데이터와 유사한 형식으로 구성됩니다. placeholder query data는 쿼리가 해결되기 전에 사용자에게 로딩 중임을 알리기 위해 UI에 사용됩니다.
따라서, initial query data와 placeholder query data는 모두 UI에 표시할 데이터이지만, initial query data는 쿼리 결과를 받기 전에 캐시나 기본값으로 사용되고, placeholder query data는 로딩 중인 상태에서 사용되는 일시적인 데이터입니다.
알거 같으면서 모르겠다~
유저가 어떤 데이터를 필요로 하기전에 해당 데이터를 미리 받아놨다면, 데이터를 가져오는데 걸리는 시간을 줄일 수 있다. Prefetching으로 데이터를 캐싱하는것이 그 방법이 될 수 있다.
staleTime
을 부여했고, 데이터가 특정 staleTime보다 오래되었으면, 쿼리는 fetched한다.쿼리와 달리 mutations는 create/update/delete data 또는 server side-effects를 수행한다.
mutaion은 주어진 순간에 다음 중 하나의 상태에 있다.
isIdle
or status === 'idle'
- mutation은 idle 또는 fresh/reset 상태이다.isLoading
or status === 'loading'
- mutation은 running 상태이다.isError
or status === 'error'
- mutation은 error를 맞닥뜨렸다.isSuccess
or status ==='success'
- mutation은 성공했고 mutation data는 사용 가능하다.외에도 많은 정보를 사용 가능하다.
error
- mutation은 error
상태이고, error
프로퍼티를 통해 사용 가능하다.data
- mutation은 success
상태이고, data
프로퍼티를 통해 사용 가능하다.가끔 mutation 요청의 error
와 data
를 비워줘야 할 때가 있다. reset
함수를 사용하자.
mutation.reset()
현재 캐시된 쿼리를 무효화하며, 해당 쿼리를 다시 가져오게 한다. 해당 함수는 주로! 데이터 업데이트 이후에 사용하게 된다. 사용자가 어떠한 작업을 해서, 데이터를 업데이트 시켰다면, 해당 데이터에 관한 모든 쿼리를 무효화한 다음, 업데이트된 데이터를 다시 가져와야 할 것이다. 쿼리 무효화를 하는 방법은 매우 다양하다.
앞선 invalidation과 유사해 보이는 setQueryData가 소개되고 있다.
서버에게 객체를 업데이트하는 mutation을 다룰때, 새로운 객체는 mutation의 응답으로 자동으로 반환되는게 일반적이다. 해당 항목에 대한 쿼리를 다시 가져와서 이미 가지고 있는 데이터에 대한 네트워크 호출을 낭비하는 대신에 mutation 함수에 의해 반환된 객체를 활용하고 Query Client's setQueryData 메서드를 사용해서 즉시 기존 쿼리를 새 데이터로 업데이트 할 수 있다.
setQueryData를 통한 업데이트는 immutable 방식으로 수행되어야 한다.
queryClient.setQueryData(
['posts', { id }],
(oldData) => {
if (oldData) {
// ❌ do not try this
oldData.title = 'my new post title'
}
return oldData
})
queryClient.setQueryData(
['posts', { id }],
// ✅ this is the way
(oldData) => oldData ? {
...oldData,
title: 'my new post title'
} : oldData
)
이후 공식문서는 봐도 무슨 의미인지 솔직히? 모르겠다. 실전에서 써먹다가 의문이 생기면 그때 다시 와서 읽어야지.
React Query는 서버에서 데이터를 미리 가져와 queryClient에 전달하는 두 가지 방법을 지원합니다.
두가지 형태의 pre-rendering을 지원하는 Next.js와 시작해보자!
React Query는 플랫폼에 관계없이 이러한 형태의 사전 렌더링을 모두 지원한다.
Next.js의 getStaticProps 또는 getServerSideProps에서, 둘 중 하나의 메서드에서 가져오는 data를 useQuery의 initialData 옵션에 전달할 수 있다. React Query의 관점에서 이들은 동일한 방식으로 integrate(통합)된다.
export async function getStaticProps() {
const posts = await getPosts()
return { props: { posts } }
}
function Posts(props) {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: props.posts,
})
// ...
}
설정이 쉽고, 경우에 따라 빠른 솔루션이 될 수 있지만 전체 접근 방식과 비교할때 고려해야 할 몇가지 장단점이 있다.
React query는 Next.js 서버에서 prefetching multiple queries를 지원한다. 그리고 해당 쿼리를 queryClient로 dehydrating한다. 이것은 서버가 페이지 로드 시 즉시 사용할 수 있는 마크업을 미리 렌더링할 수 있다는 것을 의미하며, js가 사용 가능한 즉시 리액트 쿼리는 라이브러리의 전체 기능으로 이러한 쿼리를 업그레이드하거나 hydrate시킬 수 있다. 이 작업은 서버에서 렌더링된 이후로 쿼리가 오래된 경우 클라이언트에서 쿼리를 다시 페치하는 작업이 포함된다.
서버에서 캐싱 쿼리를 지원하고 hydration을 set up 하려면
// _app.jsx
import {
Hydrate,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
export default function MyApp({ Component, pageProps }) {
const [queryClient] = React.useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
)
}
이제 우린 getStaticProps 또는 getServerSideProps로 페이지의 일부 데이터를 prefetch 할 준비가 되었다.
React Query의 관점에서 이들은 동일한 방식으로 integrate(통합)된다.
// pages/posts.jsx
import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query'
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery(['posts'], getPosts)
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
function Posts() {
// 이러한 useQuery는 게시물 페이지의 더욱 깊은 하위 자식에서 발생할 수 있으며,
// 데이터는 어느 쪽이든지 즉시 사용이 가능하다.
const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
// 이 쿼리는 서버에서 prefetch 되지 않았으며, 클라이언트에서 불러올때까지 시작하지 않는다.
// 이렇게 두 패턴을 혼합해도 괜찮다.
const { data: otherData } = useQuery({
queryKey: ['posts-2'],
queryFn: getPosts,
})
// ...
}
Next.js의 rewrites 기능을 Automatic Static Optimization과 함께 사용하거나 StaticProps와 함께 사용하면 다음과 같은 단점이 있습니다.
바로 React Query에 의한 두번째 hydration이 발생한다.
그 이유는 Next.js가 router.query에서 제공될 수 있도록 클라이언트에서 다시 쓰기를 구문 분석하고 hydration 후 매개 변수를 수집해야 하기 때문이다.
그 결과, 모든 hydration data의 참조 동일성이 누락된다. 이러한 예로는 컴포넌트의 props로 사용되는 data, useEffect/useMemo의 의존성 배열에 사용되는 data가 있다.
React Query v3과 v4의 주요 차이점은 다음과 같습니다.
React Query v4에서는 QueryKey를 사용하여 쿼리의 종류와 관련된 데이터를 쉽게 식별할 수 있습니다. 이전 버전의 React Query에서는 각 쿼리에 고유한 문자열 식별자를 제공해야 했습니다. 그러나 v4에서는 객체, 배열 또는 함수를 사용하여 QueryKey를 구성할 수 있으므로 쿼리 구성이 훨씬 유연해졌습니다.
React Query v4에서는 새로운 Query 및 Mutation 컴포넌트가 추가되었습니다. 이 컴포넌트들은 useQuery 및 useMutation 훅을 래핑하며, 컴포넌트를 사용하여 리액트 애플리케이션의 상태를 더욱 쉽게 관리할 수 있습니다.
React Query v4에서는 타입스크립트를 지원하기 위해 많은 개선이 이루어졌습니다. 예를 들어, QueryKey 및 MutationOptions에 대한 타입 지원이 추가되었습니다.
React Query v4에서는 멀티캐시라는 개념이 도입되었습니다. 이는 캐시된 데이터를 여러 위치에서 사용할 수 있도록 허용하며, 이전 버전에서는 캐시 데이터를 변경하기 위해서는 쿼리를 다시 실행해야 했습니다.
React Query v4에서는 쿼리 결과 데이터의 크기를 줄이고 캐시 불일치를 방지하는 등의 성능 개선이 이루어졌습니다. 또한 v4에서는 쿼리 결과 데이터를 직렬화하여 네트워크 전송 속도를 높이는 기능도 추가되었습니다.
이러한 변경 사항을 통해 React Query v4는 더욱 유연하고 강력한 캐싱 및 데이터 관리 라이브러리가 되었습니다.
와 리액트 쿼리!