: 처음에 동료 개발자가 이걸 쓰는걸 보고 뭐지,, 겐조 마크인가?
했던 그 마크가 바로 react-query에서 제공하는(사실 그냥 리액트 쿼리 마크다) devtools 버튼이다.
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} position="bottom-right" />
</QueryClientProvider>
이런식으로 써주면 된다. devtools의 기능은 사용하면서 익혀보고자 한다.
const {
isFetching,
data: videos,
refetch,
} = useQuery({
queryKey: ["videos", videoId],
queryFn: () => getVideos(videoId),
select: (data) => data.data,
});
위와 같이 queryKey: ["videos", videoId],
여기에 써준 videoId가 바뀔 때마다 쿼리를 재실행 (queryFn)한다는 말이 된다.
** 써본 결과 useQuery는 api가 실패했을 때 디폴트로 3번까지 재요청을 한다(결과적으로 기본 세팅이 계속 에러가 발생해쓸 경우 총 네번의 api를 실행하는 것이라고 할 수 있다).
const { data, isLoading, isError, error } = useQuery(
["posts", currentPage],
() => fetchPosts(currentPage),
{ staleTime: 10000, keepPreviousData: true },
);
이렇게 staleTime을 10초로 해두면 페이지네이션 상태라고 했을 때 해당 페이지 넘버(currentPage state)로 이동을 해도 10초가 지나지 않았다면(처음 api 호출 이후) 캐싱된 데이터를 사용한다(api를 호출하지 않는다).
export function usePrefetchPost(): void {
const queryClient = useQueryClient();
queryClient.prefetchQuery(queryKeys.post, getPosts);
}
위와 같이 prefetchQuery를 커스텀 훅으로 만들어서 특정 컴포넌트에 진입하기 전에 미리 데이터를 불러와 캐싱해놓을 수 있다. 이렇게하면 예전에 최적화 공부할 때 했던 것처럼 특정 페이지 혹은 모달창 등을 열기전에 미리 컨텐츠를 받아놓고, 지연시간 없이 바로 콘텐츠를 렌더링해서 보여줄 수 있다는 장점이 있다. 이에 더해서, 커스텀 훅으로 만들지 않고, 예를 들어, pagination
을 해놓은 부분이 있다고 할 때
const queryClient = useQueryClient();
useEffect(() => {
if (currentPage < MAX_POST_PAGE) {
const nextPage = currentPage + 1;
queryClient.prefetchQuery(["posts", nextPage], () =>
fetchPosts(nextPage),
);
}
}, [currentPage, queryClient]);
이런식으로 현재 페이지네이션 내의 페이지가 변할 때마다 prefetchQuery
를 통해 다음 페이지의 데이터를 페칭해와서 캐싱할 수 있다. 이 때, prefetch를 해온 데이터도 staleTime이 지나면 결국 해당 페이지가 됐을 때 다시 fetching 한다.
이 떄, { staleTime: 1000000000000000000, keepPreviousData: true },
이런식으로 staleTime을 사실상 무한대로 주면 어떻게될까? 당연하게도 데이터는 처음 api를 호출 한번을 한 뒤로는 더이상 fetching을 하지 않는다(신선도를 유지할 필요가 없는 데이터는 이런식으로 쓰면 될 것 같다 => 한번 받으면 갱신할 필요가 없는 데이터들).
const { data = [] } = useQuery(queryKeys.comments, getComments,{
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
})
: refetchOnMount
는 데이터가 stale 상태일 경우 마운트 시 마다 refetch를 실행하는 옵션이다. default는 true이고, 만약 always 로 설정하면 마운트 시 마다 매번 refetch 를 실행한다.
: refetchOnWindowFocus
는 데이터가 stale 상태일 경우 윈도우 포커싱 될 때 마다 refetch를 실행하는 옵션이다. 예를 들어, 크롬에서 다른 탭을 눌렀다가 다시 원래 보던 중인 탭을 눌렀을 때도 이 경우에 해당한다. 심지어 F12로 개발자 도구 창을 켜서 네트워크 탭이든, 콘솔 탭이든 개발자 도구 창에서 놀다가 페이지 내부를 다시 클릭했을 때도 이 경우에 해당한다.
default true이고, always 로 설정하면 항상 윈도우 포커싱 될 때 마다 refetch를 실행한다.
: 네트워크 연결이 다시 설정될 때 데이터를 다시 가져와야 하는지 여부를 결정한다. 'false'로 설정되면 네트워크 재연결이 발생할 때 데이터를 다시 가져오지 않음을 나타내고, 'true'로 설정하면 구성요소가 인터넷 재연결을 감지하면 자동으로 데이터 다시 가져오기를 트리거한다. 근데 개인적으로,, 네트워크 연결이 다시 설정될 일이 얼마나 있을까 싶기는 하지만 그래도 뭐,,!
: 추가로 아예 이렇게 refetch 주기를 개발자가 설정해서 쓸 수 있다.
const { data, isLoading, isError, error } = useQuery(
["posts", currentPage],
() => fetchPosts(currentPage),
{
refetchInterval: 60000 // 60초(매분마다)마다 리페치 하도록!
}
);
: 두 메서드는 모드 현재 진행중인 request의 개수를 정수로 나타낸다. 그렇다면 이걸 어떻게 사용해볼 수 있을까?. 간단하게 전역적으로 혹은 React.createPortal 등으로 사용하는 root depth의 스피너를 on, off 할 때 필요한 flag로 유용할 것 같다. 예를 들어,
function SpinnerContainer () {
const isFetching = useIsFetching();
const isMutating = useIsMutating();
const display = isFetching || isMutating ? 'inherit' : 'none';
return <Spinner display={display} />
}
위와 같이 해놓으면 현재 request중인 것이 한개라도 있으면 Spinner를 보여줄 것이고, 그렇지 않으면 보여주지 않을 것이다.
const {
isFetching,
data: comments = [],
refetch,
} = useQuery({
queryKey: ["comments", id],
queryFn: () => getComments(id),
select: (data) => data.data,
});
명시적으로 fallback을 쓰려면
const fallback = []
const {
isFetching,
data: comments = fallback,
refetch,
} = useQuery({
queryKey: ["comments", id],
queryFn: () => getComments(id),
select: (data) => data.data,
});
위와 같이 써줄 수 있겠다. fallback은 사실 별건 아니고, useState()로 치면 기본값이다. 본래
{comments?.map((comment) => {
return <Comment key={comment.id} comment={comment} refetch={refetch} />;
})}
이렇게 comments가 undefined 인 케이스를 comments?.map
이런식으로 커버했다면, fallback을 쓰면 그럴 필요가 없는 것이다.
: 작업을 할 때 예를 들어, useMutation
의 경우
const mutation = useMutation({
mutationFn: deleteComment,
onSuccess: () => {
dispatch(
addToast({
text: "Successfully deleted",
variant: "success",
}),
);
refetch();
},
onError: () => {
dispatch(
addToast({
text: "failed to delete",
variant: "error",
}),
);
},
});
이런식으로 onSuccess, onError 콜백을 둬서 썼었는데, 이렇게하면 해당 mutation이 아닌 것에서는 재정의를 해줬어야했다. 물론 위의 케이스는 delete를 하는 mutation인데, 이 메시지에 특화해서 작성을 했다고 생각할 수 있지만, queryClient쪽에 공통적으로 디폴트 옵션을 걸어놓으면 매번 이렇게 생성해야하는 수고를 덜 수 있다.
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: queryErrorHandler,
staleTime: 600000, // 10 minutes
cacheTime: 900000, // 15 minutes
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
},
mutations: {
onError: queryErrorHandler,
}
},
});
위와 같이 mutation, query 등에 디폴트 옵션을 해주면 모든 useMutation, useQuery에 적용이 된다. 이 때, queryErrorHandler
를
function queryErrorHandler(error: unknown): void {
const id = 'react-query-error';
const textMessage = error instanceof Error ? error.message : 'error connecting to server';
dispatch(
addToast({
text: textMessage,
variant: "error",
}),
);
}
이런식으로 써주면 매번 delete, update 등의 콜백에 따로 onError or onSuccess 콜백을 등록하지 않아도 된다.
: 예전에 레코일에서도 이런게 있었던 것 같은데(물론 useSelector에도 있다), 받아온 데이터를 컨버팅 해주는 로직을 넣어주는 곳이다. 정확히는 useQuery의 3번째 인자인 options 객체에 컨버팅 로직을 넣어주면 useQuery를 통해 받아오는 data를 원하는 형식에 맞게 바꿔준다.
const commentMapFn = useCallback(
(unfilteredStaff) => comments.map((comment) => {
if(!comment.visible) {
comment.display = "none"
return comment
}
return comment;
})
,[comments])
const {data: comments = [] } = useQuery(queryKeys.comments, getComments,{
select: commentMapFn
})
: 개발을 하다보면 특정 데이터를 받아와서 거기서 온 id 값을 통해 다른 api를 호출해서 값을 다시 받아와야 하는 케이스가 있다. 그런 상황에서 아래와 같이 사용해준다. user 정보가 업데이트 되면 그 user 의 comment를 받아오도록 하는 식의 코드이다.
export function useUserComments(): Comment[] {
const { user } = useUser();
const fallback:Comment[] = [];
const { data: userComments = fallback } = useQuery(
"user-comments",
() => getUserComments(user),
{
enabled: !!user
})
return userComments;
}