Tanstack Query: 리액트 친화적인 비동기 상태 관리 툴에 대한 집중 분석

Tanstack Query 의 철학

Powerful asynchronous state management for TS/JS, React, Solid, Vue, Svelte and Angular
저자는 자신의 블로그에서 Tanstack Query 는 데이터 패치 라이브러리가 아닌, 비동기 상태 관리 툴이라고 소개합니다.
그리고 Tanstack Query는 다음과 같은 문제를 해결하기 위해 만들어졌습니다.
  1. 지저분한 상태 관리
  1. 수동 리패칭
  1. 비동기 스파게티 코드

Tanstack Query 의 특징

그렇다면 위 문제들을 어떻게 해결했을까요? Tanstack 이 내세우는 세가지 특징을 살펴봅시다.

DECLARATIVE & AUTOMATIC

사용자는 단순히 Tanstack Query에게 “어떤 데이터를 얻고 싶은지”, “얼마나 신선한 상태로 유지할 지”만 알려주면 나머지는 알아서 해줍니다. 캐싱, 백그라운드 업그레이드, 낡은 데이터 처리를 복잡한 설정 없이 알아서 수행합니다.
💡 과정을 표현하는 절차형이 아니라, 원하는 결과를 표현하는 선언형 패러다임과 더불어 상태 관리에 필요한 각종 부수작업들을 알아서 처리해줍니다.

SIMPLE & FAMILIAR

만약 사용자가 프로미스나 async/await 에 알고있다면, 바로 Tanstack Query를 사용할 수 있습니다. 관리해야 할 전역 상태와 리듀서, 정규화 시스템이나 무거운 설정등이 전혀 없습니다. 단순히 패칭 함수만 전달하면 끝입니다!
💡 비동기를 처리할 수 있는 최신 JS 문법에 잘 어우러지고, 무겁고 복잡한 설정 코드를 작성할 필요가 없으므로 코드 표현이 간결해집니다.

EXTENSIBLE

Tanstack Query는 각 옵저버별로 세부적인 설정이 가능하며, 다양한 용례에 맞춰 세부 설정이 가능합니다. 개발자 전용 도구와 무한 로딩 API, 일급 변이 도구까지 함께 제공됩니다.
💡 격리된 블랙박스로 인해 다른 영역에 영향을 끼치지 않으므로 모듈성이 좋으며, 비동기 상태 관리를 위한 여러가지 세부 설정이 가능해서 다양한 경우에 맞춰 유연하게 사용할 수 있습니다.

그 외 특징들

너무 많으니 공식 홈페이지를 참고해주세요.

Tanstack Query의 동기

TanStack Query (FKA React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your web applications a breeze.
대부분의 웹 프레임워크는 자체적인 데이터 패칭 & 업데이트 방법을 제공하지 않기 때문에, 개발자들은 스스로 이러한 기능을 구축해야합니다.
대부분의 전통적인 상태 관리 라이브러리는 클라이언트 상태와는 잘 어울리지만, 비동기 혹은 서버 상태와 작업하기에는 썩 좋지 않습니다. 이것은 클라이언트 상태와 서버 상태가 완전히 다르기 때문입니다.
서버 상태는 보통 다음과 같은 특징을 가지고 있습니다.
  1. 당신이 통제할 수 없는 원격에서 존재합니다.
  1. 패칭과 업데이트를 위해서는 비동기 API가 필요합니다.
  1. 공동 소유로 인해, 다른 사람에 의해 암시적으로 변경될 수 있습니다.
  1. 조심하지 않으면 잠재적으로 오래된 데이터가 되기 십상입니다.
이러한 서버 상태를 잘 다루기 위해서는 다음과 같은 작업들이 필요합니다.
  1. 캐싱
  1. 요청 최적화 (동일한 요청이 동시다발적으로 발생하는 경우라던지)
  1. 오래된 데이터를 백그라운드에서 업데이트
  1. 데이터가 언제 낡아지는지 판단하기
  1. 데이터 갱신을 최대한 빠르게 반영하기
  1. 페이지네이션 및 레이지 로딩과 같은 성능 최적화
  1. 메모리 관리와 가비지 컬렉팅
  1. 구조적 공유를 통해 쿼리를 메모이징
이러한 수많은 작업들을 일일이 다루기 힘들기 때문에, Tanstack Query는 이러한 작업들을 알아서 잘 해줍니다.
  1. 복잡하고 오해하기 쉬운 코드들을 간단한 몇줄의 코드로 대체할 수 있어 코드가 간결해집니다.
  1. 유지보수성을 높여주고, 새로운 서버 상태를 클라이언트 상태와 연결할 때 걱정을 하지 않아도 됩니다.
  1. 사용자로 하여금 어플리케이션이 더욱 빠르다고 느끼게 해주며, 더 반응성이 좋아집니다.
  1. 대역폭을 절약하고 메모리 성능을 향상시킬 가능성을 제공합니다.

React와의 호환성

훅의 형태로 기능을 제공

Hook을 통해 서로 비슷한 것을 하는 작은 함수의 묶음으로 컴포넌트를 나누는 방법을 사용할 수 있습니다.
Tanstack Query가 제공하는 대부분의 기능은 훅의 형태로 제공됩니다. 이러한 훅은 React가 원하는 “재사용 가능한, 비슷한 것을 하는 작은 함수의 묶음을 선언적으로 표현”이 가능합니다.
한 예로 useQuery 내부 구현을 보면, 쿼리 클라이언트와 옵저버를 이용하는 커스텀 훅임을 알 수 있습니다.

컴포넌트 구조에 맞춰짐

스스로 상태를 관리하는 캡슐화된 컴포넌트를 만드세요. 그리고 이를 조합해 복잡한 UI를 만들어보세요.
Tanstack Query는 컴포넌트에 서버 상태를 연동시킬 수 있습니다. 컴포넌트라는 격리된 공간 내에서 연동된 서버 상태를 간편하게 관리할 수 있습니다.
// 여기서는 글 목록만! function PostList() { const { data: postList } = useQuery({ queryKey: ['post'], queryFn: fetchPostList, }); } // 여기서는 글만! function Post(props) { const { id, img } = props; const { data: post } = useQuery({ queryKey: ['Post', id], queryFn: fetchPost, }); }

React의 Suspense 와 함께 사용이 가능

일부 기능들은 Suspense와 함께 사용하도록 설계되어있습니다. 이를 통해 조금 더 선언적으로 UI를 표현할 수 있습니다.
실제로 구현체를 보면, 프로미스를 던지는 것으로 Suspense와 어우러지게 되어있음을 알 수 있습니다.
function useBaseQuery(...) { // Handle suspense if (shouldSuspend(defaultedOptions, result)) { throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary) } }

구조적 공유

Tanstack Query는 구조적 공유(Structural sharing) 기법을 이용해 렌더링을 최적화합니다. (사실 React 뿐만 아니라, 반응형 프레임워크에서 다 통용되는 원칙이긴 하지만..)
❓ <a href="https://tanstack.com/query/latest/docs/framework/react/guides/render-optimizations#structural-sharing">구조적 공유</a>란? 낡은 서버 상태와 새로운 서버 상태를 비교할 때, 이전 상태를 최대한 많이 유지하려고 하는 전략입니다. JSON 형태로 직렬화 가능한 데이터에 대해, 변경이 없는 부분은 얕은 복사를 통해 레퍼런스를 유지하여 리렌더링을 방지합니다.

Context API와의 통합

QueryClientProvider 를 통해, 리액트 컴포넌트 트리 내에서 손쉽게 QueryClient 를 공유할 수 있습니다. 물론, 이 또한 React 내장 Context API를 이용해 만들어져있습니다.
🚨 QueryClient 는 참조-안전하게 사용하길 권장합니다. 가장 좋은 방법은 App 외부에 쿼리 클라이언트 인스턴스를 생성하는 것입니다. (<a href="https://tkdodo.eu/blog/react-query-fa-qs#2-the-queryclient-is-not-stable">참고</a>)

간단하게 쓰기 쉽지만 잘 쓰긴 쉽지 않음 (개인적인 생각)

React 처럼 간단하게 사용하기엔 좋지만 잘 쓰려면 알아야 할 것이 많습니다. (당장 TkDodo 의 블로그 아티클만 봐도 28개나 됩니다. 😨)

Deep Dive

❗ 공식 문서에 나와있는 예제는 여기서 설명하지 않도록 하겠습니다.

1. 쿼리 키를 대하는 자세

Treat the query key like a dependency array
쿼리 키를 훅의 의존성 배열처럼 대하라고 합니다. 즉, 다음 두가지가 유사합니다.
  • 의존성 배열이 바뀌면 훅이 동작하는 것 처럼, 쿼리 키가 바뀌면 리패치 함수가 불립니다.
  • 로컬 상태에 의존하는 쿼리 함수에 대해, 둘을 동기화해줍니다. 마치 useEffect 내부의 로직이 의존성 배열의 상태와 동기화되는 것 처럼요.

2. initialData를 이용해 매끄럽게 화면 바꾸기

아래 예제에서는 모든 할 일 목록을 불러온 다음 필터된 목록을 불러오려고 할 때, initialData 내부에서 getQueryData 를 사용하여, 화면의 깜빡거림을 방지하는 기술을 보여주고 있습니다.
type State = 'all' | 'open' | 'done'; type Todo = { id: number state: State }; type Todos = ReadonlyArray<Todo>; const fetchTodos = async (state: State): Promise<Todos> => { const response = await axios.get(`todos/${state}`) return response.data }; const useTodosQuery = (state: State) => useQuery({ queryKey: ['todos', state], queryFn: () => fetchTodos(state), initialData: () => { const allTodos = queryClient.getQueryData<Todos>([ 'todos', 'all', ]) const filteredData = allTodos?.filter((todo) => todo.state === state) ?? [] return filteredData.length > 0 ? filteredData : undefined }, });

3. 서버 상태와 클라이언트 상태를 분리하라

TanStack Query 가 뱉어주는 서버 상태를 로컬 상태에 저장하는 경우, 이 로컬 상태는 서버 상태의 변경을 따라가지 못합니다. 즉, 둘이 동기화가 되지 않는 부작용이 있습니다.
의도적으로 둘의 동기화를 막고 싶은 경우가 아니라면, 서버 상태의 복사본인 로컬 상태를 사용하지 않는 것이 좋습니다.
다음 코드는 의도적으로 둘의 동기화를 막은 사례인데요, 서버 상태를 가져와 폼의 초기 상태로 설정하는 코드입니다.
const App = () => { const { data } = useQuery({ queryKey: ['key'], queryFn, staleTime: Infinity, }) return data ? <MyForm initialData={data} /> : null } const MyForm = ({ initialData }) => { const [data, setData] = React.useState(initialData) ... }
이 경우, 불필요하게 패치 함수가 불리는것을 막기 위해 staleTime 을 Infinity 로 설정해주는 것을 잊지 마세요!

4. 쿼리 캐시를 로컬 상태 매니저로 사용하지 마라

setQueryData 를 이용해 쿼리 데이터를 변경하는 것은 일반적으로 낙관적 업데이트 혹은 서버 상태를 받아온 직후에만 사용하기를 권장하고 있습니다.
암묵적인 백그라운드 패치가 의도적으로 설정한 서버 상태를 덮어씌울 수 있으므로, 패치 함수가 트리거되지 않도록 주의를 기울여야 합니다.
🚨 추가로, setQueryData 를 사용하는 경우 쿼리 키가 반드시 정확히 일치해야 합니다. (<a href="https://tkdodo.eu/blog/react-query-fa-qs#1-query-keys-are-not-matching">참고</a>)

5. 단순한 래핑 함수라도 유용할 수 있다

// 2번에 나온 예제 코드 const useTodosQuery = (state: State) => useQuery({ queryKey: ['todos', state], queryFn: () => fetchTodos(state), });
단순히 tanstack 훅을 래핑하는 커스텀 훅을 만들어 쓰더라도, 거기엔 여러가지 이점이 있습니다.
  • 데이터 패칭 로직을 UI와 분리할 수 있습니다.
  • 쿼리를 다루는 로직들을 하나의 파일에서 관리할 수 있습니다.
  • 변경이 필요할 때, 수고가 덜 듭니다.

6. 서버 상태를 변형하기

쿼리 함수에서 데이터를 변형하는 경우, 네트워크 탭의 응답 값과 TanStack 디버거에 표시되는 값이 달라 혼란을 겪을 수 있습니다. 그리고 패치 함수가 호출될 때 마다 변형이 일어납니다.
// 패치 함수 내에서 서버 상태를 변형하는 경우 const fetchTodos = async (): Promise<Todos> => { const response = await axios.get('todos') const data: Todos = response.data return data.map((todo) => todo.name.toUpperCase()) }; const useTodosQuery = () => useQuery({ queryKey: ['todos'], queryFn: fetchTodos, });
이런저런 이유로 응답값을 변형해야 하지만 근본적으로 API를 수정할 수 없는 경우, select 옵션이 좋은 해결책이 됩니다. 이는 온전히 클라이언트 사이드에서 변형 로직을 최적화하기 좋은 곳입니다.
셀렉터 함수는 순수 함수로 만들거나 useCallback 을 이용해서 메모이징을 하는 것을 권장합니다.
또한, 5번과 결합해서 유용한 커스텀 훅을 만들어 사용할 수도 있습니다.
// select 옵션을 이용해 서버 상태를 변형하는 경우 const useTodosQuery = (select) => useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select, }); const useTodosCount = () => useTodosQuery((data) => data.length); const useTodo = (id) => useTodosQuery((data) => data.find((todo) => todo.id === id));

7. 극한으로 리렌더링 최적화하기

💡 저자는 “불필요한 리렌더링” 보다, “필요한 리렌더링을 놓치는 것”을 더 경계라하고 조언합니다.
종종 TanStack Query 를 사용하다보면 리렌더링이 여러번 일어나는 경우가 있습니다. 이는 TanStack Query에 존재하는 여러 메타데이터가 변경되기 때문입니다.
{ status: 'success', data: 2, isFetching: true } { status: 'success', data: 2, isFetching: false }
대부분의 경우 이는 필요한 리렌더링이지만, 만약 극한으로 최적화를 하고 싶다면 notifyOnChangeProps 속성을 이용할 수 있습니다.
다만 이는 정말로 필요한 일부 경우에만 사용하는 것이 좋습니다. 지정한 필드 외에는 상태가 동기화되지 않고, 변경을 감지하지 않기 때문에 필요한 리렌더링을 놓치는 경우가 발생할 수 있기 때문입니다.
사용자가 필요한 값을 일일이 지정하기 귀찮을 수 있으므로, "tracked" 라는 값을 지정할 수 있습니다. 다만 이는 몇가지 한계가 있으므로 조심해서 사용해야합니다. (Tracked Query 의 주의점)

8. 쿼리 상태 확인하기

주요한 몇가지 쿼리 상태를 표현하는 메타데이터들에 대해 알아보겠습니다.
  • success : 쿼리가 성공해서 data 가 존재하는 경우
  • error : 쿼리가 실패해서 error 가 존재하는 경우
  • pending : 캐시된 데이터가 없고, 쿼리가 아직 완료되지 않은 경우
  • isStale : 캐싱된 데이터가 invalidate 하거나, staleTime 이 지나버린 경우 true
  • fetching : 초기 pending 상태를 포함해서, 패칭 함수가 실행중이거나 백그라운드 리패칭이 실행중인 경우 true
  • paused : 네트워크 오프라인등의 이유로 쿼리 함수를 진행할 수 없는 경우 true. 만약 네트워크가 연결되면 자동으로 패칭을 재시도함.
  • idle : 쿼리가 일시정지되지 않았으면서, 패칭중이 아닐 때 true
  • isRefetching : 초기 pending 상태를 제외하고, 백그라운드 리패칭이 실행중인 경우 true
🚨 success 상태와 error 상태는 상호배타적이라고 생각하기 쉽지만, 일부 경우에는 공존이 가능합니다. (<a href="https://tkdodo.eu/blog/status-checks-in-react-query#background-errors">참고</a>) 저자는 이런 경우 재시도 메커니즘에 의해 안좋은 유저 경험을 제공할 수 있으므로, 데이터 이용가능성을 우선적으로 확인하는 것을 권장합니다.

8. 기본 에러 타입 지정하기

module augmentation 을 이용해 쿼리 에러의 기본 타입을 지정할 수 있습니다.
declare module '@tanstack/react-query' { interface Register { defaultError: AxiosError } }

9. Typescript 에서 안전하게 쿼리 비활성화하기

enabled 옵션 대신, skipToken 이라는 함수를 이용해서 type-safe하게 쿼리를 비활성화 할 수 있습니다.
import { useQuery, skipToken } from '@tanstack/query' // id가 유효한 경우에만 패치 함수를 실행 function useGroup(id: number | undefined) { return useQuery({ queryKey: ['group', id], queryFn: () => fetchGroup(id), enabled: Boolean(id), }) } // 마찬가지로 id가 유효한 경우에만 패치 함수를 실행 function useGroup(id: number | undefined) { return useQuery({ queryKey: ['group', id], queryFn: id ? () => fetchGroup(id) : skipToken, }) }
개인적으로는 enabled 옵션을 사용하는 것이 더 좋지만, TS에 더 잘 맞는 부분은 후자인 것 같네요.

10. 효과적인 쿼리 키

Use Query Key factories 목차를 보면, 쿼리 키를 일일이 선언하는 방식에 대한 부작용을 언급하고 있습니다.
This is not only error-prone, but it also makes changes harder in the future
그래서 저자는 쿼리 키 팩토리를 사용하는 것을 권장합니다. (필수는 아니지만, 확장가능한 앱을 위해 권장한다고 합니다.)
const todoKeys = { all: ['todos'] as const, lists: () => [...todoKeys.all, 'list'] as const, list: (filters: string) => [...todoKeys.lists(), { filters }] as const, details: () => [...todoKeys.all, 'detail'] as const, detail: (id: number) => [...todoKeys.details(), id] as const, } // 🕺 remove everything related to the todos feature queryClient.removeQueries({ queryKey: todoKeys.all }) // 🚀 invalidate all the lists queryClient.invalidateQueries({ queryKey: todoKeys.lists() }) // 🙌 prefetch a single todo queryClient.prefetchQueries({ queryKey: todoKeys.detail(id), queryFn: () => fetchTodo(id), })

11. 동기적으로 쿼리 캐시 미리 채워놓기

쿼리 캐시에 데이터를 미리 채워놓는 두가지 방법이 있습니다.
  • placeholderData
  • initialData
function Component() { // ✅ status will be success even if we have not yet fetched data const { data, status } = useQuery({ queryKey: ['number'], queryFn: fetchNumber, placeholderData: 23, }) // ✅ same goes for initialData const { data, status } = useQuery({ queryKey: ['number'], queryFn: fetchNumber, initialData: () => 42, }) }
두 방법 모두 pending 상태 없이 바로 success 상태가 됩니다. 또한 처음부터 캐시 데이터가 존재하므로, 이펙트가 없습니다.
하지만 두 방법 간의 차이가 전혀 없는 것은 아닙니다. initialData 는 캐시 레벨에서 동작하며, placeholderData는 옵저버 레벨에서 동작합니다. 이로인해, 다음과 같은 차이점을 만들어냅니다.
  1. initialData 는 캐시에 영속적으로 존재하며, placeholderData 는 캐시에 영속적으로 존재하지 않습니다.
    1. 하나의 쿼리 클라이언트는 하나의 캐시 엔트리를 가지므로, 특정 쿼리 키에 initialData 가 설정된다면, 이후 설정값은 무시됩니다.
    2. 반면 placeholderData 는 (컴포넌트 별로) 하나의 쿼리 키에 여러 값을 지닐 수 있습니다.
  1. placeholderData 는 가짜 값이기 때문에 항상 백그라운드 리패칭이 수행되며, 실제 쿼리 데이터가 받아와지면 교체됩니다. 반면에 initialData 는 진짜 값이기 때문에 staleTime 설정에 따라 백그라운드 리패칭이 수행되지 않을 수 있습니다.
💡 저자의 추천 방식! 다른 쿼리로부터 초기 값을 설정해야 할 땐 <code>initialData</code> 를, 그 외엔 <code>placeholderData</code> 를 쓴다고 합니다.
🚨 <code>initialData</code> 는 기본적으로 신선한 데이터로 판단하기 때문에, 낡아지는 시점을 잘 지정해주는게 좋습니다. 만약 쿼리 캐시로부터 <code>initialData</code> 를 설정한다면, <code>initialDataUpdatedAt</code> 속성을 같이 지정해주세요.

12. 쿼리 키 변경 시, 매끄럽게 화면 바꾸기

첫번째로는 React 에서 제공하는 useDeferredValue 를 사용하는 방법입니다. 이 방법은 React 환경에서 모든 비동기 작업에 적용할 수 있습니다.
function App() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); return ( <> <label> Search albums: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Loading...</h2>}> <SearchResults query={deferredQuery} /> </Suspense> </> ); } function SearchResults({ query }) { const { data: albums } = useSuspenseQuery({ queryKey: [...], queryFn: () => fetchAlbums(query), }); return ( <ul> {albums.map(album => ( <li key={album.id}> {album.title} ({album.year}) </li> ))} </ul> ); }
두번째 방법은 TanstackQuery 에서 제공하는 keepPreviousData 를 사용하는 방법입니다.
import { keepPreviousData } from '@tanstack/react-query' const { data, isPlaceholderData } = useQuery({ queryKey: ['item', id], queryFn: () => fetchItem({ id }), // ⬇️ like this️ placeholderData: keepPreviousData, })

13. 에러를 핸들링하는 세가지 방법

TanStack Query 에서 에러를 핸들링하는 방법에는 세가지가 있습니다.
  1. 훅을 호출하는 주체가 에러를 보고 처리하는 방법
  1. 에러를 던지는 방법
  1. 에러 콜백을 이용하는 방법
// 훅을 호출하는 주체가 에러를 직접 처리하는 방법 function TodoList() { const todos = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // isError 를 이용해 판단할 수도 있지만, 8번의 "데이터 이용가능성"을 확인하는게 더 좋습니다. if (todos.isError) { return 'An error occurred' } } // 에러를 던지는 방법 function TodoList() { const todos = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, // 해당 옵션에 true 를 주거나, throwOnError: true, // 함수를 줄 수 있습니다. throwOnError: (error) => error.response?.status >= 500, }) } // 에러 콜백을 이용하는 방법 const useTodos = () => useQuery({ queryKey: ['todos'], queryFn: fetchTodos, // ⚠️ looks good, but is maybe _not_ what you want onError: (error) => toast.error(`Something went wrong: ${error.message}`), })
다만 세번째 방법은 useTodos 를 호출하는 모든 컴포넌트가 onError 함수를 호출하게되므로, 토스트가 여러개 뜰 수 있습니다. 단 한번만 에러 핸들링 동작이 수행되기를 원한다면, QueryCache 를 이용하면 됩니다.
const queryClient = new QueryClient({ queryCache: new QueryCache({ onError: (error) => toast.error(`Something went wrong: ${error.message}`), }), })
이 방식을 이용하면 하나의 쿼리 키에 한번의 에러 핸들링이 보장됩니다. 그래서 Sentry 등의 에러 로깅 도구를 넣기 좋은 위치입니다.

14. mutation 하는 방법

useQuery is declarative, useMutation is imperative.
function AddComment({ id }) { const addComment = useMutation({ mutationFn: (newComment) => axios.post(`/posts/${id}/comments`, newComment), }) return ( <form onSubmit={(event) => { event.preventDefault() addComment.mutate( new FormData(event.currentTarget).get('comment') ) }} > <textarea name="comment" /> <button type="submit">Comment</button> </form> ) }
위와 같이 변이 함수를 호출하면 됩니다.
여기서 더 나아가서, 변이된 서버 상태를 다시 동기화해주는 두가지 방법이 있습니다.
// 캐시 무효화 const useAddComment = (id) => { const queryClient = useQueryClient() return useMutation({ mutationFn: ..., onSuccess: () => { // ✅ refetch the comments list for our blog post queryClient.invalidateQueries({ queryKey: ['posts', id, 'comments'] }) }, }) } // 직접 캐시 업데이트 const useUpdateTitle = (id) => { const queryClient = useQueryClient() return useMutation({ mutationFn: ..., // 💡 response of the mutation is passed to onSuccess onSuccess: (newPost) => { // ✅ update detail view directly queryClient.setQueryData(['posts', id], newPost) }, }) }
만약 변이 함수가 필요한 모든 서버 상태를 반환하는 등, 당신이 리패칭을 하기 원하지 않는다면, 두번째 방법을 추천드립니다.
💡 onSuccess 함수는 async 함수를 받을 수 있습니다. 만약 쿼리 무효화를 기다리게 하고 싶다면, invalidateQueries가 반환하는 프로미스를 다시 반환하세요.
💡 프로미스를 반환하는 mutateAsync 함수도 있습니다.

15. Tanstack Query의 생명주기

개인적으로 Tanstack Query 와 React 컴포넌트가 어떻게 상호작용을 하며 생명주기가 흘러가는지 이해하는게 중요하다고 생각합니다.
notion image
위 이미지는 컴포넌트가 마운트되는 시점부터 리렌더링까지의 흐름을 간략하게 도식화한 그림입니다. 간략한 과정은 다음과 같습니다.
  1. 컴포넌트가 마운트되면 컴포넌트 함수가 호출되며, 훅이 호출됩니다.
  1. 훅은 쿼리 옵저버를 생성하고, 옵저버는 쿼리 캐시 내부에 존재하는 쿼리를 구독합니다. (만약 쿼리가 없다면 생성합니다.)
  1. 쿼리는 패치 함수를 호출하고, 이 과정에서 쿼리의 메타데이터 및 상태가 변합니다. 쿼리 옵저버는 이 변화를 감지합니다.
  1. 변화를 감지한 옵저버는 컴포넌트를 리렌더링시킵니다.
  1. 쿼리가 패칭을 완료하면, 옵저버에게 변경을 알려주고, 옵저버는 컴포넌트를 리렌더링합니다.

참고