[React.js] React Query
Next.js 에 React Query 를 도입하며, 관련된 사용법과 옵션을 기록하기 위한 글입니다.
💁 React Query의 장점
- ✅ 서버 데이터 캐싱
- ✅ 데이터 패칭 시 로딩, 에러 처리를 한 곳에서 처리 가능
- ✅ prefetching, retry 등 다양한 옵션
- ✅ 쉬운 상태관리
♻️ React Query 의 라이프 사이클
라이프 사이클 별 상태 개념
- fetching : 데이터 요청 상태
- fresh : 데이터가 프레시한 (만료되지 않은 상태)
- 컴포넌트의 상태가 변경되더라도, 데이터를 다시 요청하지 않는다.
- 새로고침하면 다시 fetching 한다.
- stale : 데이터가 만료된 상태.
- 한번 프론트로 내려준 서버 데이터는, 최신화가 필요한 데이터라고 볼 수 있다.
- 그 사이에 다른 유저가 데이터를 추가, 수정, 삭제 등을 할 수 있기 때문에
- 컴포넌트가 마운트, 업데이트되면 데이터를 다시 요청한다.
- fresh 에서 stale 로 넘어가는 시간의 디폴트는 0이다.
- inactive : 사용하지 않는 상태
- 일정 시간이 지나면 가비지 콜렉터가 캐시에서 제거한다.
- 기본값 5분
- delete : 가비지 콜렉터에 의해 캐시에서 제거된 상태.
라이프 사이클
- A 쿼리 인스턴스가 mount 됨
- 네트워크에서 데이터를 fetch 하고 A 라는 query key 로 캐싱함
- 이 데이터는 fresh 상태에서 staleTime (기본값 0) 이후 stale 상태로 변경됨
- A 쿼리 인스턴스가 unmount 됨
- 캐시는 cacheTime (기본값 5분) 만큼 유지되다가 가비지 콜렉터로 수집됨
- 만일 cacheTime 이 지나기 전에 A 쿼리 인스턴스가 새롭게 mount 되면, fetch 가 실행되고, fresh 한 값을 가져오는 동안 캐시 데이터를 보여줌.
staleTime
- 데이터가 fresh → stale 상태로 변경되는데 걸리는 시간
- fresh 상태일때는 쿼리 인스턴스가 새롭게 mount 되어도, 네트워크 fetch 가 일어나지 않는다.
- 데이터가 한번 fetch 되고 나서 staleTime 이 지나지 않았다면, unmount 후 mount 되어도 fetch 가 일어나지 않는다.
- 디폴트는 0
cacheTime
- 데이터가 inactive 상태일 때, 캐싱된 상태로 남아있는 시간
- 쿼리 인스턴스가 unmount 되면, 데이터는 inactive 상태로 변경 된다.
- 캐시는 cacheTime 만큼 유지된다.
- cacheTime 이 지나면 가비지 콜렉터로 수집된다.
- cacheTime 이 지나기 전에 쿼리 인스턴스가 다시 마운트 되면, 데이터를 fetch 하는 동안 캐시 데이터를 보여준다.
🤩 useQuery
데이터 fetching 에 쓰이는 Hook. GET 메소드 사용시에 자주 쓰인다.
QueryKey
QueryKey
를 기반으로 데이터 캐싱을 관리한다.- 문자열 또는 배열로 저장할 수 있다.
// 문자열
useQuery('todos', ...)
// 배열
useQuery(['todos'], ...)
- 쿼리가 변수에 의존하는 경우에는 QueryKey 에도 해당 변수를 추가해줘야한다.
const { data, isLoading, error } = useQuery(['todos', id], () => axios.get(`http://.../${id}`));
Query Functions
- useQuery 의 두번째 인자에는 promise를 반환하는 함수를 넣어주어야 한다.
// 방법 1
useQuery('todos', fetchTodos);
// 방법 2
useQuery(['todos', todoId], () => fetchTodoById(todoId));
Query Options
- enable
- false 시 자동으로 데이터를 불러오는 것을 막는다.
- default : true ( 디폴트 : 자동으로 데이터 불러옴)
- ex) 클릭 이벤트 시에만, 데이터 패칭해오길 바란다면 enable false 로 줘야함.
- retry
- false : 데이터 fetch 에 실패해도 재요청하지 않는다.
- true : 데이터 fetch 시 실패해도 무한으로 재요청한다.
- number : 데이터 fetch 에 실패하면 number 번 까지만 재요청한다.
- staleTime : 캐시가 fresh 하다고 인정하는 시간.
- number :
number
밀리세컨즈 후 stale 상태로 처리할 것인지 설정한다. (default : 0) - Infinity : 데이터를 영원히 fresh 상태로 취급한다.
- number :
- cacheTime : 메모리에 살아있는 시간
- number:
number
millisecond 동안 캐시 데이터가 메모리에 남아있게 됩니다. 이 이후 가비지 컬렉션에서 이 데이터를 처리합니다. (default: 5 60 1000 => 5 min) - Infinity: 영원히 메모리에 데이터를 보관합니다.
- number:
- onSuccess (data : TDdata) ⇒ void
- 데이터 fetch 성공 시 실행되는 콜백
- 매개변수 data 에 요청받은 데이터가 들어온다.
- onError : (error : TError) ⇒ void
- 데이터 fetch 실패시 실행되는 콜백
- 매개변수 error 에 실패 정보가 담긴 error 가 들어온다.
- onSettled : (data? TData, error?: TError) ⇒ void
- 데이터 fetch 성공, 실패와 관계없이 무조건 동작하는 콜백입니다.
- select : (data : TData) ⇒ unknown
- 데이터를 가공할 때에 쓰이는 함수.
- 이 함수에서 리턴한 형태가 응답받은 데이터의 형태가 됩니다.
- keepPreviousData
- 새로 fetch 한 데이터를 화면에 나타내기 전까지 기존에 있던 데이터를 계속 화면에 유지할 지 여부를 결정.
- initialData
- 캐시된 데이터가 없을 때, 표시할 초기값.
- 브라우저 로컬 스토리지에 저장해둔 값으로 데이터를 초기화할 때 사용할 수 있을 것.
그외 리턴 값.
- status
- idle : 초기 상태
- loading : 데이터 fetching 중일 때 상태
- error : 데이터 fetch에 실패한 상태
- success : 데이터 fetch 에 성공한 상태
- isFetching : 데이터가 fetch 상태일 때 true, 캐싱 데이터가 있어서 백그라운드에서 fetch 되더라도 true
- isLoading 캐싱된 데이터가 없을 때 fetch 중에 true
- data : 응답 받은 데이터
- error : 실패 정보
- refetch : 수동으로 데이터 refetch 를 실행하는 함수. stale 이나 cache 같은 설정들이 무시되고, 무조건 다시 데이터를 fetching 한다.
예시
- 게시물 목록의 만료 시간을 1분으로 설정해서, 유저가 페이지 번호를 1에서 2로 반복해서 바꾸는 등의 행동을 취할 때, API 중복 호출을 방지할 수 있다.
- 게시물 목록의 만료 시간이 1분으로 설정되어 있는데 어떤 사용자는 게시글을 그 시간 안에 작성하거나, 수정할 수도 있다. 그래서 게시글 작성 후에는 캐시를 강제로 무효화(invalidate) 하여, 목록을 새로고침한다.
const { data, isFetching } = useQuery([page, searchValue], fetcher, {
staleTime: 60 * 1000,
keepPreviousData: true,
})
- stateTime 1분
- 1분 안에 호출했던 쿼리 키로 다시 호출한 경우에는 API 를 호출하지 않고, 캐시에 있는 데이터를 다시 가져와 사용
⚙️ useMuation
서버 데이터 업데이트할 때 쓰이는 Hook
데이터 생성/수정/삭제 시 자주 쓰인다.
다음과 같이 post, put 등 서버 데이터에 변경이 발생할 때 사용되는 Hook 이 useMutation 이다.
const mutation = useMutation(newTodo => axios.post('/todos', newTodo))
const handleSubmit = useCallback(
(newTodo) => {
mutation.mutate(newTodo)
},
[mutation],
)
하지만, useMutation 의 경우 API 콜 후에 서버의 response 를 data 에서 받아올 수 없다는 단점이 있다.
공식문서에서도 useMutation 의 return 값은 undefined | unknown
으로 명시되어 있다. 만약 useMutation 사용 시 reponse 가 필요한 경우라면, mutateAsync
로 얻어올 수 있다.
mutateAsycn 는 Promise 를 return 하게 되고, Promise result 로 response 를 가져올 수 있다.
mutateAsync 로 서버 response 가져오는 방법.
const postTodo = (todo) => {
axios.post('/api/data', { todo })
}
const createTodo = useMutation(postTodo);
createTodo.mutateAsync(todo).then((data) => {
console.log(data);
// console로 찍은 data가 서버의 response 값입니다.
});
🎈SSR
React Query는 SSR 을 두가지 방식으로 구현할 수 있다. initalData 를 주는 방법과, Hydration 을 하는 방법.
initialData 를 통한 방법에서는 데이터를 명시해주기만 하면 되기 때문에 훨씬 간단하지만, 만약 여러 컴포넌트에서 해당 데이터를 SSR 을 통해 사용자에게 보여준다고 하면 모든 컴포넌트에 initialData 를 넘겨줘야 하는 문제가 있다. 컴포넌트의 뎁스가 깊어질 수록 비효율적이다.
반면 Hydration 을 통한 방법은 SSR 을 할 때, 원하는 쿼리를 prefetch 하고 해당 쿼리를 사용하는 컴포넌트에서는 동일한 키로 useQuery 훅을 호출하기만 하면 ****
// pages/poke.tsx
import Pokemon from '../components/Pokemon'
import { getPoke } from '../api'
import { QueryClient } from 'react-query'
import { dehydrate } from 'react-query/hydration'
const Poke = () => {
return (
<Pokemon />
)
}
export async function getServerSideProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery('poke',
async() => {
// Next.js 에서 data 안꺼내주면 serializable 에러 뜸.
const {data} =getPoke();
return data;
},
{ staleTime: 1000 })
return {
props: {
dehydratedState: JSON.parse(JSON.stringify(dehydrate(queryClient))),
}
}
}
export default Poke
이후로 getServerSideProps 에서 props 로 넘겨준, dehydratedState를 _app 에서 받아 Hydration 으로 내려줄 것이다. 이제 컴포넌트에서 prefetch 에서 사용된 쿼리와 같은 키인 poke 를 사용해, useQuery 훅을 호출하면 된다.
const { data, isFetching } = useQuery('poke',
() => getPoke(),
{
staleTime: 1000,
}
)
⭐ 만약 custom hook 을 만든다면?
export function useGetWorst10 (startDate:string, endDate:string, workingHour:string){
return useQuery(["worst10", startDate, endDate, workingHour],
async()=> {
const {data}= await Worst10API.yulkok( startDate, endDate, workingHour);
return data.results;
},
{
staleTime: 600*1000,
keepPreviousData:true
}
)
}
👌 useInfiniteQuery
useInfiniteQuery 란 파라미터 값만 변경해, 동일한 useQuery 를 무한정 호출할 때 사용됩니다.
보통 Infinite Scroll 을 구현할 때, 많이 사용된다.
const {
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
...result
} = useInfiniteQuery(queryKey, ({ pageParam = 1 }) => fetchPage(pageParam), {
...options,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})
Option
- pageParam
- page 를 지정해준다. 기본값을 1로 한 상태에서 다음 데이터를 불러온다.
- getNextPageParam (getPreviousPageParam)
- page 를 1 증가시키는 역할을 한다.
예시코드
const useBlacklistQuery = () => {
// useInfiniteQuery에서 쓸 함수
const fetchBlacklist = async ({ pageParam = 1 }) => {
const response = await axiosInstance.get(
`/api/---/${pageParam}`,
);
const result = response.data;
// axios로 받아온 데이터를 다음과 같이 변경!
return {
result: result.blacklist,
nextPage: pageParam + 1,
isLast: result.is_last,
};
};
const query = useInfiniteQuery('[blacklist]', fetchBlacklist, {
getNextPageParam: (lastPage, pages) => {
if (!lastPage.isLast) return lastPage.nextPage;
return undefined;
},
refetchOnWindowFocus: false,
refetchOnMount: true,
refetchOnReconnect: true,
retry: 1,
});
return query;
};
result 의 구조 예시
result : {
blacklist : {
user1 : {
},
user2 : {
},
},
is_last : true, // 마지막 여부
}
👩💻결론
🟢 React Query 는 기존 상태관리 라이브러리에서 요구하는 boilerplate 코드를 제거할 수 있다.
- 보통 Redux를 사용하고 있다면 후속처릴 위해 redux-thunk, redux-observable, redux-saga 등의 미들웨어를 사용해,서버 데이터 요청 액션이 들어왔을 경우, API 를 호출해 redux 상태를 업데이트 하는 방식을 사용한다.
- 하지만 React Query 는 기본적으로 함수형 컴포넌트 안에서 훅 형태로 사용하며, 굳이 서버 상태를 다른 장소에 저장할 필요가 없다. 전역적으로 가져오는 것 또한 당연히 가능하고 후속처리 또한 편리하다.
🟢 캐싱 & 리프레시
- React Query 는 staleTime, cacheTime 등 옵션 파라미터를 통해 API 데이터의 만료 시간, 리프레시 간격, 데이터를 캐시에서 유지할 기간, 브라우저 포커스시 데이터 리프레시 여부, 성공 or 에러 콜백 등 다양한 기능을 제어할 수 있다.
- 클라이언트에서 사용하는 서버 데이터의 종류와 양이 늘어날 경우, 작업 처리량을 줄이고 사용자 경험을 개선하기위해 캐싱은 유용하게 사용될 수 있다.
🔴 코드 구조를 잘 고민해야 한다.
- Redux에 비해 보일러플레이트가 적고, 기존의 개발 패턴, 선언적인 구문들이 없어졌다.
- 비동기 로직들이 컴포넌트 별로 분산되어, 프로젝트 설계에 신경쓰지 않았을 경우 추후 관리나 확장이 오히려 어려워지고, Component 에 유착된다던지 아니면 어디서 쓰이고 있는지 파악이 더 힘들어 질 수 도 있다.
- 따라서, React-Query 사용 방법에 대해 깊이 고민해볼 필요가 있다.
참고
- https://thinkforthink.tistory.com/34
- https://kdinner.tistory.com/113
- https://velog.io/@kimhyo_0218/React-Query-리액트-쿼리-시작하기-useQuery
- https://blog.rhostem.com/posts/2021-02-01T00:00:00.000Z
- https://gingerkang.tistory.com/123
- 리액트 쿼리 공식문서
Author And Source
이 문제에 관하여([React.js] React Query), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@aeong98/React.js-React-Query저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)