우선! React Query가 Tanstack Query 로 이름을 변경했습니다!
React Query는 React Application에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리 입니다. Hook을 사용하여 React Component 내부에서 자연스럽게 서버의 데이터를 사용할 수 있는 방법을 제안합니다.
: 특정 데이터의 복사본을 저장하여 이후 동일한 데이터의 재접근 속도를 높힘
그래서 언제 데이터를 갱신해주는데?
Tanstack-Ouery는 아래의 옵션들을 제공합니다.
refetchOnWindowFocus, //default: true
refetchOnMount, //defualt:true
refetchOnReconnect, //default: true
staleTime, //default:0
cacheTime, //default: 5분 (60 * 5 * 1000)
위의 옵션들을 통해 우리는 Tanstack-Query가 어떤 시점에 데이터를 Refetching 하는지 알 수 있습니다!
staleTime
: 얼마의 시간이 흐른 뒤에 데이터를 stale(신선하지 않은)하다고 취급할 것인가
cacheTime
:inactive 상태로 메모리에 남아있는 시간, 기본 5분
Q. 그럼 각각 어느정도로 설정해줘야 좋은걸까..?
A. 일단..목적에 따라 다를것...
사용자가 자주 업데이트되는 데이터를 보여주는 페이지, 최신 정보를 유지하길 원한다면 짧은 staleTime
자주 변하지 않는 데이터를 사용하는 경우엔 긴 staleTime사용자가 자주 방문하는 페이지라면 긴 cacheTime
근데 둘 다 예측하기 어렵다면..... 적절히 설정해주는 것이 좋을 듯 합니다....!!
: 프로젝트의 규모가 커지고 관리해야할 데이터가 넘치다 보면, Client에서 관리하는 데이터와 Server에서 관리하는 데이터가 분리될 필요성을 느끼게 됩니다.
Client Data: 모달 관련 데이터, 페이지 관련 데이터...
Server Data: 사용자 정보, 비즈니스 로직 관련 정보...
비동기 API 호출을 통해 불러오는 데이터들을 Server 데이터라고 할 수 있습니다!
기존의 Redux,Recoil 등과 같은 전역 상태 관리 라이브러리들은 Client와 Server 데이터를 완벽히 분리해 관리에 용이하도록 충분한 기능이 지원된다고 보기 어렵습니다.
Q. 왜 적합하지 못할까?
A.
- 데이터 동기화의 복잡성
사용자 액션, 네트워크 상태, 다른 사용자의 액션 등 다양한 요인에 따라서 서버의 데이터 상태는 계속 변하게 됩니다. 이런 상황에서 클라이언트의 상태를 항상 최신으로 유지하려면, 실시간으로 서버의 상태 변화를 감지하고 반영하는 로직이 필요하게 되는데, 이는 상당히 복잡한 작업입니다.
- 캐싱과 무효화의 어려움
클라이언트에서 서버 데이터를 캐싱하는 것은 효율적인 데이터 관리를 위해 필수적입니다. 하지만 언제, 어떻게 캐시를 업데이트하거나 무효화할지 결정하는 것은 쉽지 않습니다. 특히 여러 사용자가 동시에 데이터를 변경하는 경우, 캐시된 데이터가 실제 서버의 데이터와 불일치하는 상황이 발생할 수 있습니다.
- 서버와 클라이언트의 데이터 모델 차이
서버와 클라이언트의 데이터 모델이 다를 수 있습니다.
따라서 Tanstack Query 에서는 아래와 같은 로직을 지원합니다.
const { data, isLoading } = useQueries(
['unique-key'],
() => {
return api({
url: URL,
method: 'GET',
});
},
{
onSuccess: (data) => {
// data로 이것저것 하는 로직
}
},
{
onError: (error) => {
// error로 이것저것 하는 로직
}
}
)
onSuccess
와 onError
함수를 통해 fetch 성공과 실패에 대한 분기를 간단하게 구현할 수 있게 됩니다!
비동기 데이터를 가져오고 관리하는 작업을 쉽게 처리할 수 있게 됩니다.
const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
'todos'라는 Query Key를 가진 비동기 쿼리를 수행합니다.
또한 result
에는 생산성을 높이기 위해 몇 가지 상태가 포함되어 있습니다.
isPending
혹은 status === 'pending'
: 쿼리에 아직 데이터가 없습니다.isError
혹은 status === 'error'
: 쿼리에 오류가 발생했습니다.isSuccess
혹은 status === 'suceess'
: 쿼리가 성공했고 데이터를 사용할 수 있습니다.이 외에도 많은 상태가 존재하나 공식문서를 참조하시길 바라겠습니다!
대부분의 경우는 isPending
상태를 확인한 후 isError
상태를 확인한 다음 마지막으로 데이터가 사용 가능하다고 가정하고 성공적인 상태를 렌더링 하는 것으로 충분합니다.
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>
)
}
Q. cacheTime과 staleTime은 어디서 적용?
A. useQuery에서 해당 내용을 적용시킬 수 있습니다!
export const useGetSpeechGuideList = () => {
return useQuery(['getSpeechGuideList'], () => api.homeService.getSpeechGuideData(), {
cacheTime: Infinity,
staleTime: Infinity,
});
};
queries와 달리 mutations 는 보통 데이터를 생성/수정/삭제 혹은 서버 사이드 이펙트를 수행 하는데 사용됩니다.
즉 useMutations 는 데이터 변경 작업을 쉽게 처리할 수 있도록 해줍니다.
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>
)
}
useMutation 훅을 사용해 mutation을 생성합니다. mutation은 newTodo 객체를 인자로 받아, axios.post 함수를 사용해 '/todos' 경로에 POST 요청을 보냅니다.
mutation 상태에 따라 다른 UI를 렌더링합니다.
isPending
isError
<div>An error occurred: {mutation.error.message}</div>
isSuccess
'Create Todo' 버튼을 클릭하면, mutation의 mutate 메서드를 호출해 새로운 할 일을 추가합니다.
id로 현재시간, title로 'Do Laundry'인 새로운 할 일을 추가해주게 됩니다.
쿼리의 데이터가 오래되었다는 사실을 알게 되었을 경우, 이 데이터는 유효하지 않을 수 있습니다. 따라서 쿼리를 오래되었다고 표시해주고 다시 가져오게 해줍니다.
// 캐시에 있는 모든 쿼리를 무효화
queryClient.invalidateQueries()
// 'todos' 라는 쿼리 키를 가진 모든 데이터를 무효화
queryClient.invalidateQueries({ queryKey: ['todos'] })
Q. 그럼 어떤 시간에 따라서 쿼리를 무효화 시켜주는거지?
A. 어떤 시간을 따르지 않고 즉시 무효화 시키고 새로운 데이터를 가져옵니다.
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { getTodos, postTodo } from '../my-api'
// Create a client
const queryClient = new QueryClient()
function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
// Access the client
const queryClient = useQueryClient()
// Queries
const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })
// Mutations
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<div>
<ul>{query.data?.map((todo) => <li key={todo.id}>{todo.title}</li>)}</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
render(<App />, document.getElementById('root'))
QueryClient
를 생성, QueryProvider
를 사용해 감싸 앱에 제공합니다.useMutation
으로 postTodo 함수를 호출하는 mutation 생성, 해당 mutation은 새로운 할 일을 추가하는 작업을 수행onSuccess
콜백이 실행, 해당 콜백에서는 queryClient.invalidateQueries
를 사용하여 'todos' 쿼리를 무효화하고 다시 가져옵니다. 이렇게 하면 최신 데이터로 UI가 즉시 업데이트됩니다.Q. 만약 서버에서 내려주는 응답 데이터가 갱신된 새로운 데이터라면 추가적인 GET 요청없이(네트워크 낭비 없이)도 업데이트가 가능하지 않을까요?
A. setQuery()를 이용해보자!
쿼리의 캐시된 데이터를 즉시 업데이트하는데 사용할 수 있는 동기식 함수입니다.
첫번째 인자로는 변경시키고자 하는 쿼리의 키를 입력받습니다.
두번째 인자로는 업데이트 함수를 입력합니다.
const queryClient = useQueryClient();
queryClient.setQueryData('todos', updatedTodos);
'todos' 쿼리를 updateTodos
라는 업데이트 함수를 이용해 변경시키게 됩니다.
const updateVideoData = (data: VideoData, clickedTitleIndex: number) => {
queryClient.setQueryData<VideoData>(['getVideoData', data.id, clickedTitleIndex, false], (oldData) => {
return { ...oldData, ...data };
});
};
export const usePostMemoData = () => {
return useMutation(api.learnDetailService.postMemoData, {
onSuccess: (data, { clickedTitleIndex }) => updateVideoData(data, clickedTitleIndex),
});
};
(코드출처: https://heycoding.tistory.com/128)
서버로부터 응답받은 데이터는 onSuccess
의 첫번째 인자인 data
를 통해서 들어오게 됩니다.
oldData(입력한 쿼리키와 일치하는 쿼리가 가지고 있던 데이터를 의미, 해당 값은 불변성을 지켜야 합니다)와 data
에 모두 스프레드 연산자를 사용했으므로 중복되는 필드는 뒤에 위치한 data
가 가지고 있는 값으로 덮어 씌워집니다.
Q. 둘 중 무엇을 사용?
A. TkDodo(이 라이브러리를 만들진 않았지만.. 많은 기여를 하신 분!)의 블로그에서는 invalidation이 더 안전한 접근 방식이라고 명시되어 있습니다.
상황에 맞게 쓰는 것이 좋을 것 같습니다.
onSuccess
, onError
, isFetching
등 ErrorFlag를 지원해줘 편리하게 사용 가능[Next] TanStack Query 소개, 설치 및 셋팅
React Query(Tanstack Query)
카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유
공식문서
[React Query] 리액트 쿼리 '잘' 사용해보자 - 네트워크 비용 감소 / UX 개선