들어가기
(v4부터 다른 프레임워크도 지원함에 따라 더 이상 React Query가 아니라 TanStack Query가 되었지만 여전히 많은 유저들이 React Query라고 하는 것 같다. 나도 그게 더 손에 붙지만 공식 문서에 TanStack Query로 명명되어 있으므로 앞으로는 TanStack Query라고 하겠다.)
최근 '좋은 상태 관리'에 대한 고민을 하고 있다. 이전에 작업한 곰곰 다이어리를 리팩토링하려고 하는데 상태가 매우 더러워 손을 댈 엄두를 내지 못하고 있기 때문이다. 클라이언트에서 쓰는 상태와 서버 데이터를 받는 상태들이 복잡하게 섞여 있는 게 가장 큰 이유인데, 이걸 어떻게 개선하면 좋을지 알아보다 TanStack Query를 적용해보면 좋겠다는 생각을 하게 됐다.
상태란?
먼저 맨날 이야기 하는 상태에 대해 정확하게 짚고 넘어가자면, 상태란 간단하게 '저장된 데이터'라고 할 수 있다. 내가 모달의 닫기 버튼을 눌러 setIsModalOpen(false)를 만들었다든가, 로그아웃 버튼을 눌러 setIsLogined(false)가 됐다든가... 이처럼 유저가 서비스에서 수행한 모든 동작의 결과가 데이터로 표현된 것을 상태라고 할 수 있다. 즉 무언가를 보여주고, 유저가 액션을 취하고, 그것으로 상태가 바뀌고 바뀐 상태를 보여주고... 이렇게 꼬리를 무는 과정이 바로 상태 관리다.
이런 상태 관리를 도와주는 상태 관리 라이브러리들로 Redux, Zustand, Recoil... 등등이 있는데, TanStack Query와 이 친구들은 약간 결이 다르다. 어떤 게 다르냐면 저 친구들은 '비동기 및 서버 상태 관리에는 적합하지 않다'는 것이다.
서버 상태란 위에서 언급한 예시들처럼 모달이나 로그아웃 버튼처럼 클라이언트에서 제어하는 클라이언트 상태가 아니라, 백엔드에 저장되어 있는 데이터를 나타낸다. 그렇기 때문에 유저의 개인 정보라든지, 상품 정보 같이 DB에 있는 것들을 가져와 제공하기 때문에 fetching이나 updating을 위한 비동기 API가 필요하다. 그냥 정의만 설명했는데도 클라이언트 상태와 서버 상태가 매우 상이하다는 것을 알 수 있다.
이 서버 상태에서는 여러 가지 신경쓸 것(?)이 많다.
단적으로 지금 내가 하고 있는 커피빈 클론 프로젝트만 해도, 메인에 들어가면 서버 쪽으로 받아온 제품 정보를 캐러셀로 보여주는데 메인에 들어갈 때마다 계속 API 요청을 보내 28개의 이미지가 한꺼번에 들어온다. 때문에 속도가 느려지면 그 부분이 텅 비게 된다. 이런 걸 캐싱해서 이미지가 달라지지 않으면 이전 상태를 가져와 보여주는 식으로 개선할 수 있는 것이다. 이는 곧 성능 최적화나 UX 개선에도 도움을 줄 수 있다. 더불어 유저가 개인 정보를 수정했을 때 빠르게 업데이트를 해 반영한다든지, 기존 메모리를 삭제해 가비지 콜렉션을 관리한다든지 등등...
이런 것을 도와주는 게 TanStack Query다.
TanStack Query
TanStack Query는 뭘 어떻게 도와주는지 공식 문서를 잠깐 확인해봤다.
TanStack Query는 TS/JS, React... 등등을 위한 매우 강력한 비동기 상태 관리를 위한 상태 관리 라이브러리다.
구체적으로 복잡한 상태 관리, 수동 refetching, 끝없는 비동기 스파게티 코드에서 벗어나 항상 최신 상태로 유지되는 선언적 자동 관리 쿼리와 mutation을 제공해 DX를 개선할 수 있도록 돕는다.
먼저 선언적이며(Declarative), 자동(Automatic)으로 데이터를 처리해준다. 데이터를 어디서 가져오고 얼마동안 최신 상태를 유지할 것인지 알려주면 TanStack Query가 알아서 처리한다. 또 프로미스나 비동기를 처리할 줄 안다면 비슷하게 사용하면 되기 때문에 사용법도 크게 어렵지 않다(고 한다.) 이외에도 무한 스크롤이나 DevTools 등 확장성 있는 도구를 통해 개발자를 도와준다.
TanStack Query의 장점, 캐싱
TanStack Query에서 가장 많이 쓰이는 게 캐싱(특정 데이터의 복사본을 저장해 이후 동일한 데이터의 접근 속도를 높이는 것)일 거라고 생각되는데, 이건 stale while revalidate라는 개념을 기반으로 동작한다.
HTTP 캐싱에서도 사용되는 이 개념은 '캐싱된 데이터를 유저에게 제공하면서 비동기적으로 콘텐츠를 서버에서 revalidate(재검증)하는 매커니즘'이다.
먼저 fetching을 해서 데이터를 받아온 데이터를 fresh 데이터라고 하겠다.
이 데이터에는 소비기한 같은 게 있다. staleTime이라고 해서 이 시간이 지나면 stale 데이터가 되어 버린다.
이 때 staleTime이 지나 더 이상 fresh 하지 않은 stale 데이터가 다시 fetching 되면 이 데이터는 다시 fresh 데이터가 된다.
하지만 refetching 되지도 않고, 화면에서 사용되지도 않을 경우 이 데이터는 폐기처리 된다.
소비기한, 유통기한이 너무 지나면 폐기처분 하는 것처럼 fresh하지도 않고 inactive한 데이터는 gcTime을 거쳐 캐시에 삭제된다.
즉 TanStack Query는 캐싱을 통해 동일한 데이터에 대한 반복적인 비동기 데이터 호출을 방지하고, 불필요한 API 콜을 줄여 서버 부하를 줄이는 이점을 제공한다.
위에서 refetching 하면 fresh 데이터가 된다고 했는데, TanStack Query는 refetching할 시점도 설정할 수 있다.
화면을 보고 있을 때나, 페이지에 전환이 일어날 때, 이벤트가 발생해 데이터를 요청할 때 데이터를 갱신할 수 있도록 도와주어 유저가 언제나 fresh한 데이터를 받을 수 있도록 한다.
TanStack Query 사용하기
TanStack Query를 사용하기 위해서는 QueryClient를 사용해야 한다.
이것은 모든 쿼리에 대한 상태 및 캐시를 가지고 있는 클래스로, props로 Client를 전달해주면 하위 컴포넌트에서 QueryClient에 접근할 수 있게 된다.
이 QueryClient는 전역적으로 옵션을 선택할 수 있는데 디폴트 옵션을 보면 queries와 mutations를 설정해주고 있다.
이 queries에서는 '서버에서 데이터를 받아올 때 사용하는 기능'으로 위에서 말했던 staleTime(오래된 데이터로 인식하는 시간)을 언제까지 할 지 정할 수도 있고, 재요청을 몇 번할지도 정할 수 있다. 그외 설정도 할 수 있고 mutations도 마찬가지로 자유롭게 설정할 수 있다.
queries랑 mutations가 TanStack Query의 핵심 개념이라 이 2가지만 더 정리해보겠다!
Queries
A query is a declarative dependency on an asynchronous source of data that is tied to a unique key. A query can be used with any Promise based method (including GET and POST methods) to fetch data from a server. If your method modifies data on the server, we recommend using Mutations instead.
공식 문서에서는 "쿼리는 고유 키에 연결된 비동기 데이터 소스에 대한 선언적 종속성이다. 쿼리는 서버에서 데이터를 가져오기 위해 모든 프로미스 기반 메서드와 함께 사용할 수 있다. 메서드가 서버의 데이터를 수정하는 경우 변형(Mutations)을 사용하는 것이 좋다"고 한다.
한 마디로 서버에서 데이터를 받아올 때(GET, POST 포함) 이걸 사용하며, 데이터를 수정하는 경우에는 변형을 쓰라고 한다.
컴포넌트나 사용자 정의 훅에서 쿼리를 사용하려면 useQuery를 사용하면 된다.
import { useQuery } from '@tanstack/react-query'
function App() {
const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}
여기서 queryKey는 쿼리에 대한 고유한 키, queryFn는 프로미스를 반환하는 함수로 데이터를 resolve하거나 에러를 throw한다.
queryKey는 캐시를 관리하기 위한 키값으로 배열 형태로 사용되는데, string 형태로 해쉬해서 key와 data를 매핑해 관리한다.
function Todos() {
const { isPending, isError, data, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
if (isPending) {
return <span>Loading...</span>
}
if (isError) {
return <span>Error: {error.message}</span>
}
// We can assume by this point that `isSuccess === true`
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
이런 식으로 query 객체의 상태에 따라 isPending, isError, isSuccess 같이 경우를 나누어 줄 수도 있다.
isLoading, isFetching을 비롯한 여러 가지 옵션들이 있으므로 필요에 따라 찾아보면 좋을 것 같다.
Mutations
queries와 달리 mutations은 일반적으로 데이터를 CREATE, UPDATE, DELETE 하거나 서버 사이드 이펙트를 수행할 때 사용된다.
사용하기 위해서는 useMutation 훅을 쓰면 된다.
function App() {
const mutation = useMutation({
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo)
},
})
return (
<div>
{mutation.isPending ? (
'Adding todo...'
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>Todo added!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: 'Do Laundry' })
}}
>
Create Todo
</button>
</>
)}
</div>
)
}
이 코드는 /todos로 newTodo를 post하도록 하는 비동기 함수를 호출한다.
isPending으로 변형이 진행중인지 확인하고, isError로 에러가 발생했는지 확인하고 에러 메세지를 보여준다.
만약 isSuccess로 성공된다면 "Todo added!"가 표시되도록 삼항 연산자로 코드를 작성했다.
이처럼 mutations의 상황에 따라 유저에게 진행 상황과 결과를 보여준다.
mutations는 query랑 똑같이 isIdle 상태(idle 상태거나 fresh/reset 상태일 때), isPending(실행중) 상태일 때, isError(에러) 상태일 때, isSuccess(성공) 상태일 때와 같이 특정 순간에 하나의 상태를 갖는다.
이 mutations에서 눈여겨 볼 옵션은 onMutate인데, 이것은 mutate 함수가 실행되기 전에 먼저 실행되는 함수다.
비동기 함수이기 때문에 optimistic update에 유용하게 쓰인다고 하는데, optimistic update란 낙관적 업데이트로 요청을 보내기 전에 UI를 업데이트하는 기술로 예컨대 유저가 좋아요를 눌렀을 때 바로 UI에서 좋아요 수를 증가 시키는 기능을 말한다. 이런 기능은 서버로 요청을 보내기 전에 UI를 빠르게 업데이트 해 UX를 개선하고 빠른 피드백을 유저에게 제공할 수 있다는 장점이 있지만 데이터의 일관성을 해치지 않게 주의해야 하는 점도 있다.
// This will not work in React 16 and earlier
const CreateTodo = () => {
const mutation = useMutation({
mutationFn: (event) => {
event.preventDefault()
return fetch('/api', new FormData(event.target))
},
})
return <form onSubmit={mutation.mutate}>...</form>
}
// This will work
const CreateTodo = () => {
const mutation = useMutation({
mutationFn: (formData) => {
return fetch('/api', formData)
},
})
const onSubmit = (event) => {
event.preventDefault()
mutation.mutate(new FormData(event.target))
}
return <form onSubmit={onSubmit}>...</form>
}
아직 써보지 않았지만 이런 식으로 서버 데이터를 업데이트 하는구나, 이해하고 넘어가도록 한다... 🌛
📌 참고
TanStack | High Quality Open-Source Software for Web Developers
High-quality open-source software for web developers. Headless, type-safe, & powerful utilities for State Management, Routing, Data Visualization, Charts, Tables, and more.
tanstack.com
[React-Query] React-Query 개념잡기
React-Query 알고 사용하기 (v5)
velog.io
'🌺 TanStack Query' 카테고리의 다른 글
[TanStack Query] Type has no properties in common with type invalidateQueries 에러 해결 (2) | 2024.06.20 |
---|---|
[TanStack Query] 낙관적 업데이트 (0) | 2024.05.07 |
[TanStack Query] useMutation으로 데이터 변경/성공 시 동작 및 쿼리 무효화 (0) | 2024.05.03 |
[TanStack Query] 동적 쿼리와 enabled를 사용한 쿼리 활성/비활성화 (0) | 2024.04.30 |
[TanStack Query] useQuery 사용하기 (0) | 2024.04.30 |