카테고리 없음
react-query
eumjo_o
2023. 7. 31. 22:52
react-query
- https://react-query.tanstack.com/comparison
- https://github.com/tannerlinsley/react-query/tree/master/examples
참고 블로그
- https://dev.to/g_abud/why-i-quit-redux-1knl
- https://kdinner.tistory.com/113
- https://velog.io/@velopert/using-redux-in-2021#recoil
- https://velog.io/@velopert/using-redux-in-2021#api요청은-이제-react-query-swr에게-맡기자
우아한형제들
카카오
react-query
Fetch, cache and update data in your React and React Native applications all without touching any "global state".
global state를 사용하지 않고 react 애플리케이션에서 서버 상태를 가져오고 캐시하고 동기화하고 업데이트를 쉽게 하는 라이브러리
- 전역 상태가 아닌 가벼운 캐싱 레이어에 해당하는 상태 관리 라이브러리 (data fetching 라이브러리)
리액트 프로젝트에서 일반적으로 사용하는 상태(state) 세 가지
- Local State 리액트 컴포넌트 안에서만 사용되는 state
- Global State Global Store에 정의되어 프로젝트 어디에서나 접근할 수 있는 state
- Server State 서버로부터 받아오는 state
- client에서 제어되거나 소유되는 것이 아닌 remote 공간에서 관리되는 값
- fetching 혹은 updating 시 비동기 API가 필요
- 사용자가 모르는 사이에 다른 사람들에 의해 변경될 수 있는 값
- 잠재적으로 out of date가 될 가능성이 있음
리덕스는 어떠한 액션이 발생했을 때, 액션에 따라 데이터 상태가 어떻게 변경되는 지를 리듀서에 정의한다. 동기적인 로직(global state) 에서는 액션과 리듀서를 직관적으로 작성 가능하지만, 비동기 로직(server state) 를 처리하기 위해서는 미들웨어를 사용해야 한다. 리덕스 미들웨어는 발생한 액션을 가로채서 다른 로직을 실행할 수 있도록 하고, 비동기 로직을 처리하는 미들웨어에는 redux-thunk와 redux-sage가 있다.
기타 전역 상태 관리자에서 server상태를 관리하는 문제점
- 보일러 플레이트
- 관리하는 state가 많아지면서, 관심사(client, server data) 분리가 어려운 스토어
- 비동기 요청
- 특정 시점의 스냅샷(최신을 보장할 수 없음)
- 예를 들어서 어떤 동영상의 좋아요 숫자를 실시간으로 반영하기 위해서는 주기적으로 서버 데이터를 폴링해서 리덕스 스토어의 데이트를 업데이트해 주어야 하고, 다른 사람에 의해서 변경된 데이터가 반영될 수 있도록 적절한 타이밍에 개발자가 업데이트 해주어야 한다.
- loading, error
- 여러 컴포넌트에서 각 정보를 얻어오는 API가 동일할 때, API의 중복 요청
- pagination, infinity Queries 를 구현해야 함
react-query는 redux나 전역 상태 관리자를 대체할 순 없다.
BUT, react query를 사용하면 Store에서 비동기 통신을 걷어내면 관심사가 분리되고 선언적으로 프로그래밍할 수 있게 된다. 즉 온전한 Client Side 전역 상태 관리가 가능할 수 있고, 각종 API 통신과 Sync를 맞추며 Store 밖에서 서버와 관련된 상태가 마치 전역 상태처럼 관리하는 것이 가능하다.
react-query 장점
- react-query를 사용함으로 서버, 클라이언트 데이터를 분리한다.
- 비동기적 과정을 선언적으로 관리 (선언적 프로그래밍 , 장황하지 않은 코드)
- 리덕스에서 비동기 로직을 사용하기 위해 액션이나 리듀서 등 장황한 코드가 필요했지만, React Query를 사용할 경우 어떤 데이터를 언제 fetch하면 되는지 목표만 기술하면 되기 때문에 선언적으로 프로그래밍 할 수 있다.
- react hook과 사용하는 구조가 비슷 (useQuery)
- useQuery 훅의 파라미터를 통해 API 데이터의 만료시간, refresh 간격, 브라우저를 focus하거나 네트워크 재연결 시 refresh 여부, 성공/에러 콜백 등 다양한 기능 제어 가능
- useQuery 훅은 자체적으로 result라는 객체를 반환하고, 해당 객체 안에 다양한 데이터들이 존재
- 반환 데이터
- unique key와 비동기 동작이 매핑되어 있으며, 여기서 설정한 unique key를 통해 다른 곳에서도 해당 query의 결과를 꺼내올 수 있다.
- 반환 데이터
- Data의 life cycle (캐싱)
- 데이터의 fetching, 동기화, 업데이트, 캐싱을 도와주면서 2개의 훅과 하나의 utility 함수로 작성할 코드 수를 줄여주고, Rest API, GraphQL API 등에 구애받지 않고 사용할 수 있다.
- https://kdinner.tistory.com/113
- fetching -> fresh -> stale -> inactive -> delete
- get을 한 데이터에 대해 update를 하면 자동으로 get을 다시 수행 (예를 들면 게시판의 글을 가져왔을 때 게시판의 글을 생성하면 게시판 글을 get하는 api를 자동으로 실행 )
- 데이터가 오래 되었다고 판단되면 다시 get (invalidateQueries)
- 동일 데이터 여러 번 요청하면 한번만 요청 (옵션에 따라 중복 호출 허용 시간 조절 가능)
- 무한 스크롤 (Infinite Queries (opens new window))
- react query devtools를 활용하여 React Query 에서 사용하는 데이터의 흐름과 캐쉬된 데이터를 실시간으로 확인 가능
- react query는 서버 데이터를 요청하는 api 관련 action, reducer, middleware를 작성하지 않아도 됨.
- 컴포넌트 안에서 Hook 상태로 사용하며 따로 상태를 다른 곳에 저장할 필요가 없다.
개념
리액트 프로젝트에서 일반적으로 사용하는 상태(state) 세 가지
- Local State 리액트 컴포넌트 안에서만 사용되는 state
- Global State Global Store에 정의되어 프로젝트 어디에서나 접근할 수 있는 state
- Server State 서버로부터 받아오는 state
Life Cycle
- 쿼리 데이터의 5가지 상태
- fresh - 만료되지 않은 쿼리, 최신 상태로 컴포넌트가 업데이트되어도 다시 요청하지 않는다.
- fetching - 요청을 수행하는 중인 쿼리
- stale - 만료된 쿼리로, 컴포넌트가 업데이트되면 다시 요청된다. useQuery로 가져온 데이터는 기본적으로 stale 상태이다.
- inactive - active 인스턴스가 없는 쿼리로 cacheTime이 지나면 garbage collection
- 예를 들어, 다른 컴포넌트가 렌더링 되었는데, 해당 컴포넌트에 전 컴포넌트에서 마운트되었던 쿼리를 똑같이 마운트하는 useQuery나 useMutation 선언문이 없을 때 이전 컴포넌트의 stale 쿼리들은 inactive 상태가 된다.
- deleted - cacheTime이 지나 삭제된 상태
- 활성화된 useQuery, useInfiniteQuery 인스턴스가 없는 쿼리나 inactive 상태로 캐시에 남아있는 경우 5분 후에 메모리에서 제거된다.
- 옵션
- enabled - 특정 조건에만 쿼리를 요청하고 싶을 때
- staleTime - 쿼리가 fresh에서 stale로 전환될 때까지의 시간으로 기본값은 0
- cacheTime - inactive 쿼리가 캐시에서 제거될 때 까지의 시간 즉 캐시 데이터가 메모리에서 유지될 시간을 의미 기본값은 5분
- onSuccess - 성공적으로 데이터를 가져왔을 때 호출되는 함수
- onError - 쿼리 함수에서 오류가 발생했을 때
- keepPreviousData - 쿼리 키가 변경되어 새로운 데이터를 요청하는 동안에도 이전 데이터를 사용할 수 있도, 캐시되지 않은 페이지를 가져올 때 화면에서 목록이 사라지는 깜빡임 현상을 방지할 수 있다.
- placeholderData - mock 데이터 (캐시 x)
- initialData - 캐시된 데이터가 없을 때 나타낼 초기값
- refetchOnWindowFocus - 윈도우가 다시 포커스 됐을 때 데이터 재호출 여부로 기본값은 true
- retry - query가 실패했을 때 해당 숫자만큼 다시 쿼리를 요청, false 일 경우는 실패한 쿼리의 경우 다시 요청을 하지 않고, true일 경우는 계속 반복해서 쿼리 요청
- retryDelay - 쿼리를 재요청하는 간격을 나타냄
- // react-query Polling export const useFetchMultipleDeliveryStatus = useQuery( 'fetchMultpleDeliveryStatus', (): AxiosPromise<ServerResponse<FetchMultipleDeliveryStatusResponse>> => fetchMultipleDeliveryStatus(...), { enabled: ..., refetchInterval: REFETCH_INTERVAL, onSuccess: data => { ... }, ... } );
- 오래된 쿼리를 백그라운드에서 refetching하는 경우
- 특정 쿼리 인스턴스가 다시 만들어졌을 때,
- 네트워크가 다시 연결됐을 때,
- refetch interval이 있을 때,
- 백그라운드에서 기본 3번 요청하게 되는데 그 이상 실패한 쿼리는 에러처리가 된다.
- retry, retryDelay 옵션으로 제어가 가능
- react query는 캐시를 관리하기 위해 QueryClient 인스턴스를 사용
- QueryClientProvider 를 컴포넌트 트리 상위에 추가시켜준다.
- 문자열과 배열을 넣을 수 있다.
- react query는 이 쿼리 key에 기반해서 cache를 관리한다.
- 해당 쿼리 key가 달라질 경우 캐싱도 다르게 관리한다.
- 배열 요소가 다르거나, 요소의 순서가 달라도 다르다고 인식한다.
- 서버에서 데이터를 요청하고, 프로미스를 리턴하는 함수를 전달한다.
- isLoading
- isSuccess
- isError
- isIdle - 쿼리 캐시된 데이터가 없는 상태
- 서버 데이터를 업데이트할 때는 useMutation 훅을 사용
- useMutation으로 mutation 객체를 정의하고, mutate 메서드를 사용하면 요청함수를 호출해 요청이 보내진다.
const mutation = useMutation(newTodo => axios.post('/todos', newTodo)) const handleSubmit = useCallback( (newTodo) => { mutation.mutate(newTodo) }, [mutation], )
- Redux를 사용하면 미들웨어를 사용하여 추가 액션을 실행할 것이고, useMutation을 사용한다면 성공/에러 콜백을 사용하여 처리할 수 있다.
- function App() { const mutation = useMutation(newTodo => axios.post('/todos', newTodo)) return ( <div> {mutation.isLoading ? ( '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> ) }
- 한 페이지 혹은 컴포넌트에서 여러 API에서 데이터를 Fetching 해야 하는 경우에는, useEffect가 용도에 따라 1개씩 추가하는 것처럼 useQuery도 필요한 만큼 추가해, 병렬처리가 가능하고 동시성이 극대화되는 것을 알 수 있다.
const results = useQueries([ { queryKey: ['post', 1], queryFn: fetchPost }, { queryKey: ['post', 2], queryFn: fetchPost }, ])
- useQuries ← useQuery 훅과 동일한 query option object 들을 array로 받는다
의존 쿼리예시
- refetch가 된 후 loading 상태는 false이기 때문에, isFetching을 써야한다.
import React from "react";
import ReactDOM from "react-dom";
import { QueryClient, QueryClientProvider, useQuery } from "react-query";
import { ReactQueryDevtools } from 'react-query/devtools'
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
{/* ... */}
<Todos/>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
function Todos() {
const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList)
if (isLoading) {
return <span>Loading...</span>
}
if (isError) {
return <span>Error: {error.message}</span>
}
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
쿼리 무효화
- 쿼리 데이터가 stale 상태로 바뀌기 기다리지 못하는 경우에 지정한 staleTime이 지나기 전에 쿼리를 무효화시켜 데이터를 새로 가져오게 할 수 있다.
import { useMutation, useQueryClient } from 'react-query'
const queryClient = useQueryClient()
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos')
queryClient.invalidateQueries('reminders')
},
})
- predicate 옵션을 사용하면 더 자세하게 대상 쿼리를 설정할 수 있다. 이를 이용해 Mutation이 일어난 후, 상태 업데이트를 원하는 쿼리를 재요청 할 수 있다.
import { useMutation, useQueryClient } from 'react-query'
const queryClient = useQueryClient()
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos')
queryClient.invalidateQueries('reminders')
},
})
SSR
- initialData 방법
- 컴포넌트 깊이를 따라서 계속 props로 initialData를 전달해야한다.
- 여러 위치에서 동일한 key 값을 가진 query를 호출하는 경우 모두 initialData를 전달한다.
- 서버에서 query를 가져온 시간을 알 수 없어서 시간 기준으로 interval refetch를 할 경우 페이지 로딩 시간을 기준으로 한다.
export async function getStaticProps() {
const posts = await getPosts();
return { props: { posts } };
}
function Posts(props) {
const { data } = useQuery("posts", getPosts, { initialData: props.posts });
// ...
}
Infinite Queries
- data.pages - 가져온 페이지를 포함하는 배열
- data.pageParams - 페이지를 가져오는데 사용되는 pageParams 배열
- fetchNextPage, fetchPreviousPage - 이전, 다음 페이지 가져올 때
- getNextPageParam, getPreviousPage - 새로고침과 로딩 상태를 구분할 때
- hasNextPage - getNavePageParam의 반환값이 undefined가 아니면 true
- isFetchingNextPage, isFetchingPreviousPage - 새로고침과 로딩 상태를 구분할 때
- https://react-query.tanstack.com/comparison
- https://github.com/tannerlinsley/react-query/tree/master/examples
import { useInfiniteQuery } from "react-query";
function Projects() {
const fetchProjects = ({ pageParam = 0 }) =>
fetch("/api/projects?cursor=" + pageParam);
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery("projects", fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
});
return status === "loading" ? (
<p>Loading...</p>
) : status === "error" ? (
<p>Error: {error.message}</p>
) : (
<>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.projects.map((project) => (
<p key={project.id}>{project.name}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? "Loading more..."
: hasNextPage
? "Load More"
: "Nothing more to load"}
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? "Fetching..." : null}</div>
</>
);
}
export async function getServerSideProps() {
const queryClient = new QueryClient();
await queryClient.prefetchInfiniteQuery("projects", () => fetchProjects());
return {
props: {
dehydratedState: JSON.parse(JSON.stringify(dehydrate(queryClient))),
},
};
}