React-query 알아보고 사용해보기

2022. 11. 13. 18:57Web_Programming/React

 

React-query 란

캐싱, 값 업데이트, 에러 핸들링 등 비동기 과정을 편리하게 제공해주는 라이브러리로 알려진 react-query에 관해 간단하게 공부해보았습니다.

 

기존 상태 관리 라이브러리 redux 및 RTK를 사용해오면서 비동기 로직을 처리 부분에서 클라이언트 데이터와 서버 데이터가 뒤섞이고 어느 순간 방대한 로직이 되어버려 로직 파악이 힘들어지고 state 관리가 힘들어지는 상황을 몇 번 직면하였습니다.

이에 대한 해결 방안으로 react-query가 사용된다고 하네요.

 

react-query의 최대 강점이 서버, 클라이언트 데이터를 분리해주는 것으로 이해했습니다.

 

이외에도 react-qeury의 장점으로는 아래와 같이 있습니다.

  • 캐싱
  • 자동 refreshing
    • get을 한 데이터에 대해 update를 하면 자동으로 get을 다시 수행한다. 
  • 동일 데이터 여러번 요청하면 한번만 요청한다. (옵션에 따라 중복 호출 허용 시간 조절 가능)
  • 비동기 과정을 선언적으로 관리할 수 있다.
  • react hook과 사용하는 구조가 비슷하다.

react-query의 기능들을 살펴보겠습니다-


React-query의 기능

 

useQuery

역할 | GET 수행

 

useQuery의 함수 타입을 살펴보면 아래와 같습니다.

export declare function useQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(queryKey: TQueryKey, queryFn: QueryFunction<TQueryFnData, TQueryKey>, options?: Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'queryKey' | 'queryFn'>): UseQueryResult<TData, TError>;

 

 

useQuery의 함수 인자로는 queryKey, queryFn, options 로 나뉘고,
  • queryKey는 임의 로 부여하게 되는 해당 useQuery의 key값
  • queryFn는 쉽게 말해 API를 호출하는 함수를 말합니다.
  •  options는 대표적으로 api 호출 성공 후 콜백 되는 onSucess, 에러발생 시 콜백 되는 onError 등이 있고, 이외에도 많은 설정 옵션이 있습니다.
반환 값으로는 대표적으로 isLoading, isError, data, error 를 담은 result 객체가 되고
returns에 대한 여러 값들과 자세한 options의 정보들은 아래 react-query의 docs에서 볼 수 있습니다.

https://tanstack.com/query/v4/docs/reference/useQuery?from=reactQueryV3&original=https://react-query-v3.tanstack.com/reference/useQuery 

 

useQuery | TanStack Query Docs

const { data,

tanstack.com

 

 

간단하게 리액트 프로젝트에서 msw로 mock API를 생성하여 테스트해보았습니다.

const getTodoList = () => {
  return axios.get("/todos");
};
interface TodoList {
  id: number;
  username: string;
  title: string;
  content: string;
}
const UseQueryBlock = () => {
  const { isLoading, isError, data, error } = useQuery("todos", getTodoList, {
    refetchOnWindowFocus: false, 
    onSuccess: (data: { data: TodoList[] }) => {
      console.log(data);
    },
    onError: (e: any) => {
      console.log(e.message);
    },
  });

  if (isLoading) {
    return <span>Loading...</span>;
  }

  if (isError) {
    return <span>Error: {error.message}</span>;
  }
  return (
    <ul>
      {data?.data.map((todo) => (
        <li key={todo.id}>
          <h3>{todo.title}</h3>
          <span>{todo.content}</span>
        </li>
      ))}
    </ul>
  );
};

 

returns data 객체에 API 응답값 data가 담겨 오고, 이 data배열 속 리스트를 보여주도록 하였습니다.

 

기존 RTK thunk 처럼 slice에서 선언하고 전 처리하여 값을 호출하지 않고 바로 사용처에서 호출하여 사용 가능 한 점이 서버와 클라이언트 데이터가 확실히 분리됨을 느낄 수 있었습니다.


비동기 useQuery

기본적으로 한 번에 여러 useQuery를 호출하게되면 동기적으로 호출됩니다.

만약 비동기적으로 호출하고 싶다면 options에 enabled 조건에 원하는 조건을 걸어주면 됩니다.

 

테스트에서는 todos의 데이터가 생성되면 호출되도록하여 todos => nextTodos 순서로 호출하였습니다.

import axios from "axios";
import { useQuery } from "react-query";

const getTodoList = () => {
  return axios.get("/todos");
};
const getNextTodoList = () => {
  return axios.get("/next-todo");
};
interface TodoList {
  id: number;
  username: string;
  title: string;
  content: string;
}
const UseQueryBlock = () => {
  const {
    isLoading,
    isError,
    data: todoData,
    error: todoError,
  } = useQuery("todos", getTodoList, {
    onSuccess: (data: { data: TodoList[] }) => {
    },
    onError: (e: any) => {
      console.log(e.message);
    },
  });

  const { data: nextTodo } = useQuery("nextTodos", getNextTodoList, {
    enabled: !!todoData, 
    onSuccess: (data: { data: TodoList }) => {
    },
  });

  if (isLoading) {
    return <span>Loading...</span>;
  }

  if (isError) {
    return <span>Error: {todoError.message}</span>;
  }
  return (
    <ul>
      {todoData?.data.map((todo) => (
        <li key={todo.id}>
          <h3>{todo.title}</h3>
          <span>{todo.content}</span>
        </li>
      ))}
      <li key={nextTodo?.data.id}>
        <h3>{nextTodo?.data.title}</h3>
        <span>{nextTodo?.data.content}</span>
      </li>
    </ul>
  );
};
export default UseQueryBlock;


useQueries

역할 | 동기적으로 여러 API GET

동기적으로 여러 api를 호출하고자 한다면 useQueries 함수를 사용하면 됩니다.

 

useQueries의 함수 타입은 아래와 같습니다.

export declare function useQueries<T extends any[]>(queries: readonly [...QueriesOptions<T>]): QueriesResults<T>;

인자로는 queries로 queriesOption의 배열을 받습니다.

queriesOption은 queryKey, queryFn 의 객체로 이루어집니다.

 

반환 값으로는 각 query들의 useQeury와 동일한 결과 객체의 배열 형태로 이루어집니다.

 const result = useQueries([
    {
      queryKey: ["my-todos"],
      queryFn: getTodoList,
    },
    {
      queryKey: ["friends-todos", friend],
      queryFn: getFriendTodoLit
    },
  ]);

따라서 0번째 query의 결과는 result [0]에 담깁니다.

 

💡 여기서 queryKey에 대해 더 살펴보면 단순한 string의 querykey 뿐만 아니라 변수 또한 배열에 담을 수 있습니다.

이 변수는 queryFn에서 받아서 사용할 수 있습니다.

  const result = useQueries([
    {
      queryKey: ["my-todos"],
      queryFn: getTodoList,
    },
    {
      queryKey: ["friends-todos", friend],
      queryFn: () => getFriendTodoLit(friend),
    },
  ]);

 

나의 todos를 받아오는my-todos와 friends usename이 "BF1"인 친구의 todos를 가져오도록 하는 friends-todos를 useQueries로 호출하도록 하였습니다.

import axios from "axios";
import { useEffect, useState } from "react";
import { useQueries } from "react-query";

const getTodoList = () => {
  return axios.get("/todos");
};
const getFriendTodoLit = (username: string) => {
  return axios.get("/friends-todos/filter", { params: { username } });
};
interface TodoList {
  id: number;
  username: string;
  title: string;
  content: string;
}
const UseQueriesBlock = () => {
  const friend = "BF1";
  const result = useQueries([
    {
      queryKey: ["my-todos"],
      queryFn: getTodoList,
    },
    {
      queryKey: ["friends-todos", friend],
      queryFn: () => getFriendTodoLit(friend),
    },
  ]);

  const [myTodoList, setMyTodoList] = useState<TodoList[]>([]);
  const [friendsTodoList, setFriendsTodoList] = useState<TodoList[]>([]);

  useEffect(() => {
    setMyTodoList(result[0]?.data?.data);
    setFriendsTodoList(result[1]?.data?.data);
  }, [result]);
  return (
    <ul>
      <hr />
      <br />
      <h3>My Todo</h3>
      {myTodoList?.map((todo) => (
        <li key={todo.id}>
          <h3>{todo.title}</h3>
          <span>{todo.content}</span>
        </li>
      ))}
      <br />
      <h3>Friends Todo</h3>
      {friendsTodoList?.map((todo) => (
        <li key={todo.id}>
          <h3>{todo.title}</h3>
          <span>{todo.content}</span>
        </li>
      ))}
    </ul>
  );
};
export default UseQueriesBlock;


QueryCache

모든 query에 관하여 옵션을 동일하게 적용하고자 한다면 QueryClient를 생성할 때,

queryCache값을 설정하여 모든 query에 동일하게 적용시킬 수 있습니다.

https://tanstack.com/query/v4/docs/reference/QueryCache

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      console.log(error, query);
    },
    onSuccess: (data) => {
      console.log(data);
    },
  }),
});

useMutaion

역할 | POST, DELETE, PATCH 등 수행

useQuery는 GET을 수행했다면, useMutation은 자원을 수정하는 메소드를 수행합니다.

export declare function useMutation<TData = unknown, TError = unknown, TVariables = void, TContext = unknown>(mutationFn: MutationFunction<TData, TVariables>, options?: Omit<UseMutationOptions<TData, TError, TVariables, TContext>, 'mutationFn'>): UseMutationResult<TData, TError, TVariables, TContext>;

함수의 인자로는 mutationFn, options가 됩니다.

  • mutaionFn은 useQuery의 queryFn과 같이 API를 호출하는 함수
  • options는 여러 콜백 함수와 useQeury와 같이 다양한 옵션이 포함

 

반환 값의 객체로 mutate, mutateAsync, reset함수 및 다른 state를 담고 있습니다. 

여기서 mutate 함수를 호출하여 해당 API를 호출시킬 수 있습니다.
<button
onClick={() => {
  postingMutation.mutate(todo);
}}
>
작성
</button>

https://tanstack.com/query/v4/docs/reference/useMutation

 

useMutation | TanStack Query Docs

const { data,

tanstack.com

 

💡 update 후 get

react-query에서 가장 흥미롭게 다가왔던 기능이 update 실행 후, get을 호출할 수 있는 기능이었습니다.

아래와 같이 성공 콜백 함수에서 queryClient의 invalidateQueries에 다시 get 하고자 하는 query의 queryKey를 담아 호출시킵니다.

  const queryClient = useQueryClient();
  const postingMutation = useMutation(postTodo, {
    onMutate: (variable) => {
      console.log("onMutate", variable);
    },
    onError: (error, variable, context) => {
      // error
    },
    onSuccess: (data, variables, context) => {
      queryClient.invalidateQueries("todos");
      console.log("success", data, variables, context);
    },
    onSettled: () => {
      console.log("end");
    },
  });

 

invalidateQueries 함수로 하나의 query 뿐만 아니라 여러 query를 재실행시킬 수 있습니다. 다양한 설정 option도 제공됩니다.

https://tanstack.com/query/v4/docs/reference/QueryClient#queryclientinvalidatequeries

 

QueryClient | TanStack Query Docs

QueryClient The QueryClient can be used to interact with a cache:

tanstack.com

import axios from "axios";
import { useState } from "react";
import { useMutation, useQueryClient } from "react-query";

const postTodo = (todo: Todo) => {
  return axios.post("/todos", { todo });
};
interface Todo {
  title: string;
  content: string;
}
const UseMutationBlock = () => {
  const queryClient = useQueryClient();
  const postingMutation = useMutation(postTodo, {
    onMutate: (variable) => {
      console.log("onMutate", variable);
    },
    onError: (error, variable, context) => {
      // error
    },
    onSuccess: (data, variables, context) => {
      queryClient.invalidateQueries("todos");
      console.log("success", data, variables, context);
    },
    onSettled: () => {
      console.log("end");
    },
  });

  const [todo, setTodo] = useState({ title: "", content: "" });
  return (
    <>
      <div>
        <hr />
        <br />
        <h3>Create My Todo</h3>
        <span>제목</span>
        <input
          type="text"
          value={todo.title}
          onChange={(e) => {
            setTodo((p) => ({ ...p, title: e.target.value }));
          }}
        />
      </div>
      <div>
        <span>내용</span>
        <input
          type="text"
          value={todo.content}
          onChange={(e) => {
            setTodo((p) => ({ ...p, content: e.target.value }));
          }}
        />
      </div>
      <button
        onClick={() => {
          postingMutation.mutate(todo);
        }}
      >
        작성
      </button>
    </>
  );
};
export default UseMutationBlock;

Mutation 수행

 

mutate 후 get 요청

 

react-query의 사용 장점을 글로만 보다가 직접 코드로 적용해보니 확실히 장점들이 직접 체감되었습니다.

해당 실습을 해보면서 msw도 처음 세팅해서 사용해봤는데 간단하게 프로젝트를 구성하여 테스트해보기에 너무 편리했습니다!

 

RTK 라이브러리 자체에서도 RTK query가 제공되는데 이후에는 RTK query를 한 번 테스트해보고 공부해보려 합니다-

 

 

 

참고 블로그

https://kyounghwan01.github.io/blog/React/react-query/basic/#%E1%84%89%E1%85%A1%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%92%E1%85%A1%E1%84%82%E1%85%B3%E1%86%AB-%E1%84%8B%E1%85%B5%E1%84%8B%E1%85%B2

 

react-query 개념 및 정리

react-query 개념 및 정리, react, react16, hook, useState, useRef, useMemo, useEffect, useReducer, useCallback, useQuery 동기적으로 실행

kyounghwan01.github.io

https://tech.osci.kr/2022/07/13/react-query/

 

React-Query 도입을 위한 고민 (feat. Recoil) - 오픈소스컨설팅 테크블로그 - 강동희

Web Frontend 개발을 할 때 React 를 사용하면서 마주하게 되는 여러 가지 문제점 중 하나는 state, 상태 관리에 관한 부분입니다. 프론트엔드 개발자라면 state 와 뗄 수 없는 인연을 맺고 있습니다.오늘

tech.osci.kr

 

반응형