React-Query 기본 정리

jonyChoiGenius·2023년 4월 26일
0

React.js 치트 시트

목록 보기
17/22

react-query?

리액트 쿼리는 비동기 작업을 요청하고, 캐싱하고, 업데이트하는 상태관리 라이브러리이다.

대부분의 상태를 서버로부터 받아온다는 점을 감안하여, 각 API와 비동기 작업, 그리고 그로 인해 발생되는 데이터를 하나의 식별자로 묶어 상태관리를 하는 개념이다.

Redux에서의 비동기 작업은 복잡하다. 특히 redux-saga는 코드가 방대해지기 십상이다. 때문에 react-query와 같은 데이터 페칭 라이브러리를 이용해 데이터를 관리하는 것이 점차 대세가 되어가고 있다.

경쟁자로는 버셀의 swr이 있다. swr이 먼저 나왔다.
하지만 react-query는 기능의 다양성을 앞세워 빠르게 성장하였고, 2023년도에서는 react-query가 대세가 된 듯 하다.

한국어로 된 튜토리얼은 아래의 레포지토리가 꼼꼼하게 잘 번역되어 정리된 것 같다.
https://github.com/ssi02014/react-query-tutorial

yarn add @tanstack/react-query 명령어로 최신버전인 리액트 쿼리 v4를 설치할 수 있다.

useQuery

데이터를 fetch할 때에 사용하는 문법이다.
useQuery는 queryKey, queryFn를 필수로 받고 기타 등등등등등등 엄청 많은 파라미터를 옵션으로 받을 수 있다.
useQuery는 data, error를 비롯해 기타 등등등등등 엄청 많은 값을 반환한다.

한 번에 다양한 값을 반환하기 때문에 redux-saga처럼 이터레이터로 처리하지 않아도 된다는 강력한 장점이 생긴다.

아래는 실제 예시이다.

export const listTodos = () => TodosApi.get("");//함수 형태로 선언한다.

const { data, isLoading } = useQuery(["todo-list"], listTodos);

useQuery는 기본적으로 3개의 인자를 받습니다. 첫 번째 인자가 queryKey(필수), 두 번째 인자가 queryFn(필수), 세 번째 인자가 options입니다.

queryKey

해당 쿼리키를 기반으로 데이터를 캐싱한다.

v4버전에서 쿼리키는 항상 배열이다. (문자열이 아니다.)

한편 쿼리에 특정 변수나 params가 필요하다면 배열에 추가해주면 된다.

export const retrieveTodos = (id: number) => TodosApi.get(`?id=${id}`);
  const { data: TodoListData, isLoading: TodoListIsLoading } = useQuery(
    ["todo-list"],
    listTodos,
  );

  //todoId를 넘겨서 하나의 데이터만 받아올 때
  const useSelectTodo = (todoId: number) =>
    useQuery(["todo-list", todoId], () => retrieveTodos(todoId));

  const { data: TodoData, isLoading: TodoIsLoading } = useSelectTodo(1);

반환값

useQuery의 주요 반환값은 아래와 같다.
status, isLoading, isError, error, data, isFetching

  • status : 데이터가 있는지에 대한 상태이다.
    ~~ - idle : { enabled: false }라서 시작이 되지 않은 상태~~ v4부터 fetchStatus로 대체됨
    - loading
    - error
    - success
  • fetchStatus : 네트워크 연결 상태이다. v4에서 추가.
    - fetching: 쿼리가 현재 실행중이다.
    - paused: 쿼리를 요청했지만, 잠시 중단된 상태이다.
    - idle: 쿼리가 현재 아무 작업도 수행하지 않는 상태이다.
  • data : Promise에서 resolved된 데이터
  • isLoading : boolean 값으로 반환 된다. 이미 fetching된 데이터가 있다면 항상 false이다.
  • isFetching : boolean 값으로 반환 된다. 이미 fetching된 데이터가 있더라도 쿼리가 실행중이면 true이다.
  • error : (서버 에러가 아니라) 쿼리 함수에서 오류가 발생한 경우
  • isError : boolean 값으로 반환 된다.
  • refetch : refetch를 수동으로 다시 요청하는 함수이다.
  • isPreviousData : keepPreviousData 옵션을 사용중일 때, 현재의 Data가 fetch된 데이터인지 이전 데이터인지를 의미한다.

옵션

useQuery의 주요 옵션은 아래와 같다.

staleTime, cacheTime : ms

  1. staleTime: (number | Infinity)
    데이터가 fresh한 상태에서 stale 상태로 변경되는 데에 걸리는 시간을 의미한다. 만약 staleTime이 3000이면 fresh상태에서 3초 뒤에 stale로 변환된다.
    fresh 상태에서는 네트워크 요청(fetch)이 일어나지 않는다.
    기본값은 0이다.
    staleTime이 0이면 항상 stale 상태가 되기 때문에 캐싱이 되지 않는다.
  2. cacheTime: (number | Infinity)
    데이터가 inactive 상태일 뙤 캐싱된 상태로 남아있는 시간.
    쿼리 인스턴스가 unmount된 후에 데이터가 inactive 상태가 된다.
    이후 데이터는 cacheTime만큼 유지되고, cacheTime이 지나면 가비지 콜렉터에 수집된다.
    기본값은 5분이다.

staleTime과 cacheTime의 관계

  • staleTime이 0이면 항상 stale이기 때문에 캐싱 자체가 일어나지 않는다.
  • staleTime이 0보다 크면, fresh한 데이터가 캐싱된다.
  • 일단 캐싱된 데이터는 staleTime과 무관하게, inactive상태에서 cacheTime만큼 유지된다.
  • cacheTime이 너무 작으면 inactive되자마자 가비지 콜렉팅 되기에 적절한 수준으로 설정해야 한다.
  const { data: TodoListData, isLoading: TodoListIsLoading } = useQuery(
    ["todo-list"],
    listTodos,
    {
      staleTime: 5,
      cacheTime: 100000,
    },
  );

  const useSelectTodo = (todoId: number) =>
    useQuery(["todo-list", todoId], () => retrieveTodos(todoId));
  const { data: TodoData, isLoading: TodoIsLoading } = useSelectTodo(1);

위와 같이 설정한 값이다.
전체를 불러오는 ["todo-list"] 쿼리는 5ms마다 만료된다.
반면 ["todo-list", todoId] 쿼리는 본 프로젝트에서 기본값으로 설정한 staleTime: Infinity,를 따른다.

아래는 링크를 오갈 때 찍힌 콘솔이다.

["todo-list"]는 쿼리가 마운트될 때마다 콘솔이 찍히는 것을 확인할 수 있다.
반면 ["todo-list", todoId]는 최초에 마운트된 값이 그대로 사용되고 있다.

이번엔 cacheTime을 서로 다르게 설정해보았다.

  const { data: TodoListData, isLoading: TodoListIsLoading } = useQuery(
    ["todo-list"],
    listTodos,
    {
      cacheTime: 100000,
    },
  );

  const useSelectTodo = (todoId: number) =>
    useQuery(["todo-list", todoId], () => retrieveTodos(todoId), {
      cacheTime: 1,
    });


["todo-list"]는 데이터가 정상적으로 캐싱되어 최초에 한 번만 요청을 보낸다.
반면 ["todo-list", todoId]는 가비지 콜렉팅되어 링크가 이동될 때마다 다시 호출되는 것을 확인할 수 있다.

refetchOnMount : (boolean | "always")

{ refetchOnMount : "always" }
기본값은 true이다.
refetchOnMount가 true이면 데이터가 stale상태일 때에 마운트 시 refetch한다.
refetchOnMount가 "always"이면 마운트시 항상 refetch한다.
refetchOnMount가 false이면 최초 마운트시에만 fetch 한다.

refetchOnWindowFocus : (boolean | "always")

윈도우가 포커스 될 때에 refetch를 할 지를 의미한다. 기본값은 true이다.

Polling - refetchInterval, refetchIntervalInBackground

  {
    refetchInterval: 2000,
    refetchIntervalInBackground: true,
  }

폴링은 하나의 장치가 다른 장치를 주기적으로 검사(poll)하는 것을 의미한다. busy waiting을 떠올릴 수 있다. busy waiting은 개념적으로 poll과 poll사이에 아무것도 안하는(waiting) 방식을 일컫는 말이다. polling을 통해 동기화를 할 수 있고, react-query에서는 리얼타임웹을 위해 polling을 쓰며, refetchInterval와 refetchIntervalInBackground의 두 가지 옵션을 제공한다.

const { isLoading, isFetching, data, isError, error } = useQuery(
  ["super-heroes"],
  getSuperHero,
  {
    refetchInterval: 2000,
    refetchIntervalInBackground: true,
  }
);

refetchInterval의 시간마다 refetch 요청을 보낸다.
refetchIntervalInBackground이 true이면 윈도우가 focus 상태가 아닌 경우에도 refetch가 실행된다.

enabled : (boolean)

{ enabled : false }
기본값은 true이다.
enabled는 쿼리가 자동으로 실행되는지 여부를 의미한다.
만일 enabled값이 false라면, 해당 쿼리는 실행되기 전까지 statuss는 loading이고, fetchStatus가 idle이다.

enabled가 true인 경우의 console이다.

"fetching"
"loading"
{data: Array(3), status: 200, statusText: 'OK', headers: AxiosHeaders, config: {…}, …}
"idle"
"success"

enabled가 true인 경우의 console이다.
클릭 이벤트에는 refetch라는 반환값을 받아서 콜백함수 형태로 사용하면 된다.

  const {
    data: TodoListData,
    isLoading: TodoListIsLoading,
    fetchStatus,
    status,
    refetch,
  } = useQuery(["todo-list"], listTodos, {
    staleTime: 3 * 60 * 1000,
    enabled: false,
  });

return (
      <div
        onClick={() => {
          console.log("클릭이벤트!!");
          refetch();
        }}
      >
        페치하기
      </div>
)
"idle"
"loading"
클릭이벤트!!
"fetching"
"loading"
{data: Array(3), status: 200, statusText: 'OK', headers: AxiosHeaders, config: {…}, …}
"idle"
"success"

retry (횟수)

{ retry: 10 }
쿼리가 실패하면 오류를 처리하기 전에 다시 시도한다.
기본값은 3이다.
false인 경우 재요청을 하지 않으며
true인 경우 무한히 재요청한다.

onSuccess, onError, onSettled : 함수명

retry가 끝나면 그 결과에 따라 함수를 실행한다.
onSuccess : 요청이 성공했을 때
onError : 요청이 실패했을 때
onSettled : 성공실패에 관계없이 실행됨

onSuccess, onError, onSettled라는 이름의 함수를 별도로 선언해서 넣는 것도 좋다.

  {
    onSuccess,
    onError,
    onSettled,
  }
  {
    onSuccess : routeToMain,
    onError : handleError,
  }

select

select는 함수의 데이터 중 일부를 선택, 가공, 반환한다.

const { isLoading, isFetching, data, isError, error, refetch } = useQuery(
  ["todo-list"],
  listTodo,
  {
    onSuccess,
    onError,
    select(data) {
      const filteredTodo = data.data.filter((todo: Data) => todo.isCompleted);
      return filteredTodo;
    },
  }
);

return (
<>
	<div>끝난 할 일</div>
    {data.map((heroName: string, idx: number) => (
      <div key={idx}>{heroName}</div>
    ))}
<>
)

keepPreviousData

React Query - keepPreviousData @velog.io/@mattew4483

  const useSelectTodo = (todoId: number) =>
    useQuery(["todo-list", todoId], () => retrieveTodos(todoId), {
      staleTime: 3 * 60 * 1000,
    });

위의 쿼리에서 todoId가 바뀔 때마다 상태가 아래와 같이 바뀐다.

["fetching", "loading"]
["idle", "success"]
["fetching", "loading"]
["idle", "success"]
["fetching", "loading"]
["idle", "success"]

와 같이 바뀐다.
["fetching", "loading"] 상태일 때에는 data가 없기 때문에 비어있는 칸이 보이게 된다.

이를 방지하는 옵션이 keepPreviousData: true이다.

페이지를 바꿀 때, 필터를 바꿀 때, 선택된 요소를 바꿀 때 등등에서 fetchStatus가 success가 될 때까지 이전의 데이터를 유지한다.

placeholderData, initialData

리액트 쿼리 : 초기값과 플레이스홀더
데이터가 없을 때에 데이터를 제공하는 방식이 있을 수 있다.
initialData는 캐싱되어 제공된다. 하나의 initialData만 존재할 수 있다.
placeholderData는 캐싱되지 않는다.
keepPreviousData: false일 때에,

["fetching", "loading"]
["idle", "success"]
["fetching", "loading"]
["idle", "success"]
["fetching", "loading"]
["idle", "success"]

위와 같은 상황에서 data가 없을 때에 placeholderData가 나타나게 된다.
placeholderData는 keepPreviousData와 같이 옵저버 레벨에서 제공되는 서비스다.

useQueries

[React] React Query의 useQueries에 대해 알아보기 - J4J Storage
여러개의 쿼리가 선언되면,
여러개의 쿼리가 병렬로 요청된다.
하지만 쿼리를 순차적으로 실행시키고 싶다면 useQueries를 사용한다.
useQueries에 쿼리 배열을 넘겨주면 된다.

const ress = useQueries([
    useQuery1,
    useQuery2,
    useQuery3,
    ...
]);

여러 개의 수동으로 선언하고 싶다면 아래와 같이 하면 된다.

const res = useQueries([
    {
        queryKey: ['persons'],
        queryFn: () => axios.get('http://localhost:8080/persons'),
    },
    {
        queryKey: ['person'],
        queryFn: () => axios.get('http://localhost:8080/person', {
            params: {
                id: 1
            }
        }),
    }
]);

종속쿼리

만일 특정 쿼리가 먼저 종료된 후에, 다른 쿼리가 작동되는 것 만을 목표로 한다면
useQueries를 사용할 필요 없이 종속쿼리 패턴을 할 수 있다.

아래의 패턴은 ['user', email] 쿼리가 실행되어 channelId가 할당되면
enabled 옵션이 변경됨을 이용한 종속쿼리 패턴이다.

channelId가 할당됨에 따라 ['courses', channelId] 쿼리가 실행된다.

const DependantQueriesPage = ({ email }: Props) => {
  // 사전에 완료되어야할 쿼리
  const { data: user } = useQuery(['user', email], () =>
    fetchUserByEmail(email)
  );

  const channelId = user?.data.channelId;

  // user 쿼리에 종속 쿼리
  const { data } = useQuery(
    ['courses', channelId],
    () => fetchCoursesByChannelId(channelId),
    { enabled: !!channelId }
  );

useInfiniteQuery

useInfiniteQuery는 무한 스크롤을 구현할 때에 사용되는 쿼리이다.
상세 설명

const fetchColors = ({ pageParam = 1 }) => {
  return axios.get(`http://localhost:4000/colors?_limit=2&_page=${pageParam}`);
};

const InfiniteQueries = () => {
  const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =
    useInfiniteQuery(["colors"], fetchColors, {
      getNextPageParam: (lastPage, allPages) => {
        return allPages.length < 4 && allPages.length + 1;
      },
    });

useMutation(mutationFn, {...options})

useQuery는 데이터를 받아오는 get 요청에 사용된다.
useMutation을 이용하면 post, patch, put, delete에 모두 사용된다.

  const mutation = useMutation(createTodo, {
    onMutate() {
      /* ... */
    },
    onSuccess(data) {
      console.log(data);
    },
    onError(err) {
      console.log(err);
    },
    onSettled() {
      /* ... */
    },
  });

  const onCreateTodo = (e) => {
    e.preventDefault();
    mutation.mutate({ title });
  };
  • useMutation의 반환 값인 mutation 객체의 mutate 메서드를 이용해서 요청 함수를 호출할 수 있다.
  • mutate는 onSuccess, onError 메서드를 통해 성공 했을 시, 실패 했을 시 response 데이터를 핸들링할 수 있다.
  • onMutate는 mutation 함수가 실행되기 전에 실행되고, mutation 함수가 받을 동일한 변수가 전달된다.
  • onSettled는 try...catch...finally 구문의 finally처럼 요청이 성공하든 에러가 발생되든 상관없이 마지막에 실행된다.

mutate가 아닌 mutateAsync 메서드를 사용할 수도 있으나, 이 경우에는 Promise를 반환하며, Promise를 직접 핸들링하면 된다.

cancelQueries

시간이 오래 걸리는 쿼리를 사용자가 직접 취소할 수도 있다. queryFn의 Promise도 취소한다는 점에서 강력한 기능 중 하나이다.

const onCancelQuery = (e) => {
  e.preventDefault();
  queryClient.cancelQueries(["super-heroes"]);
};

invalidateQueries

{ enabled: true }인 쿼리를 무효화시키는 쿼리이다.
게시글을 CRUD한 후에 게시글을 새로 불러와야할 때가 있다.
이때 쿼리를 무효화하고 refetch가 필요함을 알려야 한다.

아래는 새로운 게시글을 작성한 후 기존 쿼리를 무효화하는 패턴이다.

const useAddSuperHeroData = () => {
  const queryClient = useQueryClient();
  return useMutation(addSuperHero, {
    onSuccess(data) {
      queryClient.invalidateQueries(["super-heroes"]); // 이 key에 해당하는 쿼리가 무효화!
      console.log(data);
    },
    onError(err) {
      console.log(err);
    },
  });
};

{ enabled: false }인 경우에는 invalidateQueries나 refetchQueries를 무시한다. 오로지 해당 쿼리가 반환한 refetch 메서드로만 refetch가 가능하다.

무효화하려는 키가 여럿이라면 키를 배열의 형태로 넘겨주면 된다.
queryClient.invalidateQueries(["super-heroes", "posts", "comment"]);

exact

쿼리의 무효화는 하위 쿼리들에게도 전파된다.

queryClient.invalidateQueries({
  queryKey: ["super-heroes"],
});

위의 구문과 함께

["super-heros", 'superman'],
["super-heros", { id: 1} ],

위와 같은 쿼리들도 무효화된다.

전파되지 않도록 하려면 exact 옵션을 주면 된다.

await queryClient.invalidateQueries(
  {
    queryKey: ["super-heroes"],
    exact,
  },
  { throwOnError, cancelRefetch }
);

refetchType

공식문서
무효화된 쿼리 중 active한 쿼리(즉 적극적으로 렌더링되는 중인 쿼리들)는 즉시 백그라운드에서 refetch된다.
refetch를 원치 않으면 refetchType: 'none'을 옵션으로 준다.
만일 inactive한 쿼리도 즉시 refetch하고 싶다면 refetchType: 'all'을 옵션으로 준다.
기본값은 refetchType: "active"이다.

refetchQueries

특정 쿼리를 즉시 다시 가져온다.

// v4
// 모든 쿼리를 다시 가져온다
await queryClient.refetchQueries();

// 모든 stale 상태의 쿼리를 다시 가져온다.
await queryClient.refetchQueries({ stale: true });

// 쿼리 키와 부분적으로 일치하는 모든 활성 쿼리를 다시 가져온다.
await queryClient.refetchQueries({
  queryKey: ["super-heroes"],
  type: "active",
});

// exact 옵션을 줬기 때문에 쿼리 키와 정확히 일치하는 모든 활성 쿼리를 다시 가져온다.
await queryClient.refetchQueries({
  queryKey: ["super-heroes", 1],
  type: "active",
  exact: true,
});

await queryClient.refetchQueries(
  {
    queryKey: ["super-heroes", 1],
    type: "active",
    exact: true,
  },
  { throwOnError, cancelRefetch }
);

cancelQueries와 낙관적 업데이트

낙관적 업데이트
낙관적 업데이트란, 사용자의 요청이 반영될 것이라 가정하고 클라이언트의 상태에 미리 반영하는 것이다. 만약 사용자의 요청이 rejected돠면, 클라이언트의 상태를 되돌려야 한다.

리액트 쿼리는 클라이언트의 상태를 되돌리는 cancelQueries를 제공한다.
await queryClient.cancelQueries({ queryKey: ["super-heroes"], exact: true });

removeQueries, resetQueries, clear

queryClient.removeQueries({ queryKey: ["super-heroes"], exact: true });
캐시 쿼리를 삭제할 때 사용한다.

queryClient.resetQueries({ queryKey: ["super-heroes"], exact: true });
쿼리를 초기값으로 되돌린다. initialData가 있는 경우 initialData로 되돌리며, 없는 경우엔, invalidateQueries처럼 작동하지만 쿼리를 미리 로드한다.

queryClient.clear();
연결된 모든 쿼리를 제거한다. resetQueries와 달리 구독자도 제거한다.

profile
천재가 되어버린 박제를 아시오?

1개의 댓글

comment-user-thumbnail
2024년 6월 12일

잘 읽었습니다~

답글 달기