React Query 심화! 다양한 기능 활용하기

김혜림·2024년 5월 5일
0

react

목록 보기
10/12

React Query의 여러가지 도구들과 사용 전략에 대해 간략히 정리한 글입니다 :)

useQuery로 API 응답과 성공/실패/로딩중 상태를 관리하기

  • useQuery hook을 이용해서 API를 요청하고, API 응답 데이터를 상태로 관리한다.
import { useQuery } from "@tanstack/react-query";

export function Staff () {
	const { data, isLoading } = useQuery({
    	queryKey: [staff, filter],
    	queryFn: getStaff,
    	select: (staff) => filterByTreatment(staff, filter),
	});
    ...

React Query Dev Tools을 이용해서 편리하게 React Query 로직을 디버깅하기

  • react query 디버깅을 도와주는 툴
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
...

export function App() {
	return (
    	...
        <QueryClientProvider client={queryClient}>
        	...
        	<ReactQueryDevtools />
        </QueryClientProvider>
        ...
    );
}

react query hook을 Custom Hooks으로 분리해서 컴포넌트 로직과 query 로직 분리하기

  • 컴포넌트 내에서 useQuery, useMutation, prefetch등 query hook을 실행하면 기능 별로 코드가 분리되지 않아 코드 가독성이 떨어진다.
  • react query 로직을 usehook으로 관리해서 코드 가독성을 높이자!
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/react-query/constants";

export function useTreatments(): Treatment[] {
  const fallback: Treatment[] = [];

  const { data = fallback } = useQuery({
    queryKey: [queryKeys.treatments],
    queryFn: getTreatments,
  });

  return data;
}

export function usePrefetchTreatments(): void {
  const queryClient = useQueryClient();

  queryClient.prefetchQuery({
    queryKey: [queryKeys.treatments],
    queryFn: getTreatments,
  });
}

Loading, Error를 query의 최상단에서 처리하기

  • 일괄적인 error 처리 : QueryCacheonError 속성으로 설정
export const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error) => {
      errorHandler(error.message);
    },
  }),
});
  • 일괄적인 Loading UI 추가 : @tanstack/react-query의 useIsFetcing, useIsMutating hook 사용
import { Spinner } from "../내컴포넌트";
import { useIsFetching, useIsMutating } from "@tanstack/react-query";

export function Loading() {
  const isFetching = useIsFetching();
  const isMutating = useIsMutating();
  
  const display = isFetching && isMutating ? "inherit" : "none";

  return (
    <Spinner $display={display}/>
  );
}

React Query를 이용한 Pagination

  • currentPage, maxPage를 useState로 관리
  • queryKeycurrentPage값을 추가 -> currentPage가 변경되면 query가 재호출됨
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";

import { fetchPosts, deletePost, updatePost } from "./api";
import { PostDetail } from "./PostDetail";

const maxPostPage = 서버에서 주는 maxPage

export function Posts() {
  const [currentPage, setCurrentPage] = useState(1);
  const [selectedPost, setSelectedPost] = useState(null);

  const { data, isLoading, isError } = useQuery({
    queryKey: ["blog", "post", "list", currentPage],
    queryFn: () => fetchPosts(currentPage),
    staleTime: 2000, //2 seconds
  });

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error!</div>;
  
  return (
    <>
      <ul>
        {data.map((post) => (
          <li
            key={post.id}
            className="post-title"
            onClick={() => setSelectedPost(post)}
          >
            {post.title}
          </li>
        ))}
      </ul>
      <div className="pages">
        <button
          disabled={currentPage <= 1}
          onClick={() => {
            setCurrentPage((prev) => prev - 1);
          }}
        >
          Previous page
        </button>
        <span>Page {currentPage}</span>
        <button
          disabled={currentPage >= maxPostPage}
          onClick={() => {
            setCurrentPage((prev) => prev + 1);
          }}
        >
          Next page
        </button>
      </div>
      <hr />
      {selectedPost && <PostDetail post={selectedPost} />}
    </>
  );
}

Pre-fetching과 usePreFetchQuery Hooks

  • 다음에 실행될 가능성이 높은 query의 cache를 미리 background에서 조회해둘 수 있음.
  • pre-fetching을 사용하면 다음 화면에서 로딩이 되더라도 캐시 데이터가 있음 -> 데이터가 없는 상태가 화면에 나타나지 않아 사용자 경험에 좋음
  • pre fetch는 queryClient의 객체임. 얘의 queryKey도 useQuery와 같아야 하니까 useQuery Hook이 있는 파일에 같이 hook으로 분리해주면 코드가 더 깔끔해질 것임.
// /hooks/useTreatments.ts 
import { useQuery, useQueryClient } from "@tanstack/react-query";

import type { Treatment } from "@shared/types";

import { axiosInstance } from "@/axiosInstance";
import { queryKeys } from "@/react-query/constants";

// for when we need a query function for useQuery
async function getTreatments(): Promise<Treatment[]> {
  const { data } = await axiosInstance.get("/treatments");
  return data;
}

export function useTreatments(): Treatment[] {
  const fallback: Treatment[] = [];

  const { data = fallback } = useQuery({
    queryKey: [queryKeys.treatments],
    queryFn: getTreatments,
  });

  return data;
}

export function usePrefetchTreatments(): void {
  const queryClient = useQueryClient();

  queryClient.prefetchQuery({
    queryKey: [queryKeys.treatments],
    queryFn: getTreatments,
  });
}
//pages/Home.tsx 


import { usePrefetchTreatments } from "../treatments/hooks/useTreatments";

export function Home() {
  usePrefetchTreatments();

  return (
    <Stack textAlign="center" justify="center" height="84vh">
      ...
    </Stack>
  );
}

Query Key Factory를 이용한 Query Key 관리

  • query key값을 임의로 넣어주면 실수할 수도 있음
  • query key factory 라이브러리를 이용하면 실수 덜할 것임 (물론 query key factory를 스스로 만들 수도 있지만 사실 별거 아님)
  • 설치하기 : npm i @lukemorales/query-key-factory
  • 사용1. query key 객체를 query-key-factory라이브러리의 createQueryKeyStore() 를 이용해서 만들어준다.
import { createQueryKeyStore } from '@lukemorales/query-key-factory'

export const queryKeys = createQueryKeyStore({
  todos: {
    detail: (todoId: string) => [todoId],
    list: (filters: TodoFilters) => ({
      queryKey: [{ filters }],
      queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
    }),
  },
})
  • 사용2. 객체를 사용해서 쿼리키를 작성해준다.
 const {data} = useQuery({
 	queryKey: [...queryKeys.detail, ...]
 })

API 응답으로 새로운 데이터를 만들어야 할 때, select option

  • 클라이언트 단에서 이미 조회한 data에 필터 기능을 추가해 주거나, 응답 값을 이용해서 새로운 응답값을 만들어서 사용할 때 select 옵션을 사용하면 좋다.
  • select option을 사용할 때 selectFnuseCallback hook을 사용하는 것이 바람직하다. 왜냐하면 useCallback 을 사용하지 않으면 useQuery가 리패치되지 않았는데도 컴포넌트가 마운트될 때 selectFn은 재실행되기 때문이다.
  • queryClient.prefetchQuery를 실행할 때는 select 옵션을 설정하지 않아도 되는데, 이는 select 옵션의 결과는 사실 react query의 캐시 데이터로 관리되는 것은 아니기 때문이다. 그저 selectFn은 데이터가 한 번 리패치 될 때 실행되는 함수임
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useState, useCallback } from "react";
import { getAvailableAppointments } from "../utils";

export function useAppointments() {
	const [showAll, setShowAll] = useState(false);
	
	const fallback: AppointmentDateMap = {};
	
	const selectFn = useCallback(
    (data: AppointmentDateMap, showAll: boolean) => {
      if (showAll) return data;
      return getAvailableAppointments(data, userId);
    },
    [userId]
  );
	
	const { data: appointments = fallback } = useQuery({
    queryKey: [
      queryKeys.appointments,
      { year: monthYear.year, month: monthYear.month },
    ],
    queryFn: () => getAppointments(monthYear.year, monthYear.month),
    select: (data) => selectFn(data, showAll),
  });
}

refetch 옵션 설정하기

  • react query에서 데이터를 리페치 하는 경우는 다음과 같다.
    - query가 처음 mount되어 캐시된 데이터가 없을 때
    • react component를 mount할 때
    • window가 refocus될 때
    • network가 다시 연결될 때
    • 설정된 refetchInterval이 경과한 경우
  • refetch 관련 옵션 (queryClient에 전역으로 설정할 수도 있고, useQuery에 특정하여 설정할 수도 있음)
    - refetchOnMount (boolean) : mount될 때 리패치 실행 여부
    - refetchOnWindowFocus (boolean) : window가 focus되었을 때 리패치 실행 여부
    - refetchOnReconnect (boolean) : 네트워크 재연결되었을 때 리패치 실행 여부
    - refetchInterval (밀리초 단위의 시간) : 자동으로 리패치 해줄 시간
    - useQuery에서 return하는 객체에도 refetch 함수가 있다. -> 임의로 refetch를 실행해줄 수 있음
  • refetch 막는 법
    - stale time을 늘린다.
    - refetchOnMount, refetchOnWindowFocus, refetchOnReconnect 옵션을 끈다.
    - 유의사항! 자주 변경되지 않고 약간 오래되어도 사용자에게 큰 영향을 미치지 않는 데이터에 대해서만 refetch를 막는 것이 좋다.
export function useTreatments(): Treatment[] {
	const fallback: Treatment[] = [];

  const { data = fallback } = useQuery({
    queryKey: [queryKeys.treatments],
    queryFn: getTreatments,
    staleTime: 1000 * 60 * 10, //10분
    gcTime: 1000 * 60 * 15, //15분
    refetchOnMount: false,
    refetchOnWindowFocus: false,
    refetchOnReconnect: false,
	});

	return data;
}

export function usePrefetchTreatments(): void {
  const queryClient = useQueryClient();

  queryClient.prefetchQuery({
    queryKey: [queryKeys.treatments],
    queryFn: getTreatments,
    staleTime: 1000 * 60 * 10, //10분
    gcTime: 1000 * 60 * 15, //15분
    // prefetch는 일회성이므로 refetch 관련 옵션은 설정하지 않는다. 
  });
}

refetch 옵션을 전역으로 설정하기

  • queryClient의 defaultOptions -> queryies에 설정해줄 수 있다.
export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 10, //10 minutes
      gcTime: 1000 * 60 * 15, //15 minutes
    },
  },
  ...
});

refetchInterval : 자동으로 x밀리초 간격으로 query를 재호출할 때 사용

const { data: appointments = fallback } = useQuery({
    queryKey: [ ... ],
    queryFn: () => getAppointments(...),
    select: (data) => selectFn(...),
    refetchInterval: 1000 * 60, //1분마다 쿼리 재호출
    ...,
  });

staleTime: infinity : 쿼리가 한 번 실행된 후 query cache가 없어지지 않는 한 영원히 재호출 되지 않음

export function useUser() {
  const { userId, userToken } = useLoginData();

  const { data: user } = useQuery({
    queryKey: generateUserKey(userId, userToken),
    queryFn: () => getUser(userId, userToken),
    staleTime: Infinity, // user 데이터를 한 번 조회한 후 다시 refetch 하지 않음
  });
  
  return { user };
}

enabled : 특정 상황에서 query가 실행되지 않게끔 설정할 때 사용하는 옵션

  • 예시 코드 설명 : Client State인 userId, userToken 값이 유효할 때만 useQuery가 fetch되도록 enabled option을 설정한 예제임.
export function useUser() {
  const { userId, userToken } = useLoginData();

  const { data: user } = useQuery({
  	enabled: !!userId, //userId가 유효하지 않으면 이 쿼리가 실행되지 않음
    queryKey: generateUserKey(userId, userToken),
    queryFn: () => getUser(userId, userToken),
    staleTime: Infinity, // user 데이터를 한 번 조회한 후 다시 refetch 하지 않음
  });
  
  return { user };
}

특정 쿼리의 캐시 데이터를 임의로 설정할 때 : queryClient.setQueryData

특정 쿼리 키로 시작하는 Query를 모두 삭제할 때 : queryClient.removeQueries

import { useQueryClient } from "@tanstack/react-query";

export function useUser() {
	const queryClient = useQueryClient();
    
    function updateUser(newUser: User): void {
    queryClient.setQueryData(
      generateUserKey(newUser.id, newUser.token), //queryKey
      newUser // 설정할 cache data
    );
  }
}

useMutation으로 데이터 추가/수정/삭제 API 호출하기

  • mutate를 return하여 mutationFn을 실행시킬 수 있다.
  • useQuery와 비슷하지만 query key, cache가 없다.
  • isLoading은 있지만 isFetcing은 없다.(캐시가 없으니 당연하다.)
    isLoading : cache된 데이터도 없고, query fn이 완료되지 않은 상태
    isFetcing : cache된 데이터는 있을수도 없을수도 있고, query fn이 완료되지 않은 상태
import { useMutation } from "@tanstack/react-query";
import { updatePost } from "./api";
import { Toast } from "임의의 내 컴포넌트";

export function Post(postId) {
  const { mutate, isSuccess, isPending, isError, error, reset } = useMutation({
    mutationFn: (postId) => updatePost(postId),
  });
  
  const handleUpdateClick = (postId) => {
  	mutate(postId);
    if(isError) && Toast.error(error.toString());
  }
  
  ...
  return (
  	...
    <button onClick={()=>handleUpdateClick(postId)}>수정하기</button>
  );
}

useMutation 의 전역 에러 처리, 전역 로딩 처리

  • queryClientmutationCacheonError 메소드 설정
import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query";

export const queryClient = new QueryClient({
  defaultOptions: {...},
  queryCache: new QueryCache({
    onError: (error) => {...}, // useQuery에 대한 전역 에러 처리
  }),
  mutationCache: new MutationCache({
    onError: (error) => {...}, // useMutation에 대한 전역 에러 처리
  }),
});
  • useIsMutating : 아직 완료되지 않은 mutation 의 개수를 return
import { Spinner, Text } from "@chakra-ui/react";
import { useIsFetching, useIsMutating } from "@tanstack/react-query";

export function Loading() {
  const isFetching = useIsFetching();
  const isMutating = useIsMutating();

  const display = isFetching || isMutating ? "inherit" : "none";

  return ( <Spinner display={display} /> );
}

mutation 실행 후 useQuery의 데이터를 stale 상태로 바꿔주는 invalidateQueries

  • useQuery를 stale 상태로 바꿔서 query가 재호출되게 만든다.
export function useCancelAppointment() {
  const queryClient = useQueryClient();
  const toast = useCustomToast();

  const { mutate } = useMutation({
    mutationFn: (appointment: Appointment) =>
      removeAppointmentUser(appointment),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: [queryKeys.appointments] });
      toast({ title: "취소되었습니다!", status: "success" });
    },
  });

  return mutate;
}
profile
개발 일기입니다. :-)

0개의 댓글