Front-End/React

[React] React-Query

Voyage_dev 2022. 6. 6. 10:10

React-query

  • 서버의 값을 클라이언트에 가져오거나, 캐싱, 값 업데이트, 에러핸들링 등 비동기 과정을 더욱 편하게 하는데 사용

사용하는 이유

  • react-query를 사용함으로 서버, 클라이언트 데이터를 분리

장점

  • 서버 데이터 캐싱
  • 데이터 패칭 시 로딩, 에러 처리를 한 곳에서 처리 가능
  • prefetching, retry 등 다양한 옵션
  • 쉬운 상태 관리
  • get을 한 데이터에 대해 update를 하면 자동으로 get을 다시 수행
  • 데잍가 오래 되었다고 판단되면 다시 get
  • 동일 데이터 여러번 요청하면 한 번만 요청
  • 무한 스크롤
  • 비동기 과정을 선언적으로 관리할 수 있다

React Query의 라이프 사이클

  • fetching - 데이터 요청 상태 (요청을 수행하는 중인 쿼리)
  • fresh - 데이터가 프레시한 (만료되지 않은) 상태
    • 컴포넌트의 상태가 변경되더라도 데이터를 다시 요청하지 않는다
    • 새로고침 하면 다시 fetching 한다
  • stale - 데이터가 만료된 상태 (인스턴스가 존재하지만 이미 패칭이 완료된 쿼리)
    • 데이터가 만료되었다는 것은 서버에서 한 번 프론트로 데이터를 주면 그 사이에 다른 유저가 데이터를 추가, 수정, 삭제 등등 할 수 있기 때문에 만료
    • 컴포넌트가 마운트, 업데이트되면 데이터를 다시 요청
    • fresh에서 stale로 넘어가는 시간 → 기본 값 : 0
  • inactive - 사용하지 않는 상태 (active 인스턴스가 하나도 없는 쿼리)
    • 일정 시간이 지나면 가비지 콜렉터가 캐시에서 제거함
    • 기본 값 5분
  • delete - 가비지 콜렉터에 의해 캐시에서 제거된 상태
💡 fetching → fresh → stale → inactive → delete

React-Query 준비

  • 상위에서 QueryClient를 생성 후 넣어준다
npm i react-query
yarn add react-query
  • ReactQueryDevtools는 React-Query의 개발도구
function App() {
  const queryClient = new QueryClient();

  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <Users />
      </div>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

데이터 패칭 (fetching)

  • useQuery를 이용해서 데이터를 불러올 수 있다

useQuery

  • 데이터를 get 하기 위한 API. post, update는 useMutation을 사용
  • 첫 번째 파라미터로 unique Key가 들어가고, 두 번째 파라미터로 비동기 함수 (API 호출 함수)가 들어간다
    • 두 번째 파라미터는 promise가 들어가야 한다.
    • promise - 비동기 처리에 활용하는 객체 ⇒ 웹 페이지에서 서버로 데이터를 요청했을 때, 데이터를 모두 받기 전에 웹에 출력하려고 하는 경우를 방지하기 위해 활용된다. 즉, 비동기 로직을 마치 동기처럼 사용할 수 있는 기능
  • 첫 번째 파라미터로 설정한 unique Key는 다른 컴포넌트에사도 해당 키를 사용하면 호출 가능하다. unique Key는 string과 배열을 받는다. 배열로 넘기변 0번 값은 string값으로 다른 컴포넌트에서 부를 값이 들어가고 두 번째 값을 넣으면 query 함수 내부에 파라미터로 해당 값이 전달된다
  • 리턴값은 API의 성공, 실패여부, API return 값을 포함한 객체
  • 비동기로 작동된다. 즉, 한 컴포넌트에 여러개의 useQuery가 있다면 하나가 끝나고 다음 useQuery가 실행되는 것이 아닌 두개의 useQuery가 동시에 실행된다
  • 여러개면 useQueries
const Users = () => {
  const { isLoading, isError, data, error } = useQuery("todos", fetchTodoList, {
    refetchOnWindowFocus: false, // react-query는 사용자가 사용하는 윈도우가 다른 곳을 갔다가 다시 화면으로 돌아오면 이 함수를 재실행합니다. 그 재실행 여부 옵션 입니다.
    retry: 0, // 실패시 재호출 몇번 할지
    onSuccess: data => {
      // 성공시 호출
      console.log(data);
    },
    onError: e => {
      // 실패시 호출 (401, 404 같은 error가 아니라 정말 api 호출이 실패한 경우만 호출됩니다.)
      // 강제로 에러 발생시키려면 api단에서 throw Error 날립니다. (참조: <https://react-query.tanstack.com/guides/query-functions#usage-with-fetch-and-other-clients-that-do-not-throw-by-default>)
      console.log(e.message);
    }
  });

  if (isLoading) {
    return Loading...;
  }

  if (isError) {
    return Error: {error.message};
  }

  return (
   <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
};

 

  • isLoading, issuccess 말고 status로 한 번에 처리 가능하다
function Todos() {
  const { status, data, error } = useQuery("todos", fetchTodoList);

  if (status === "loading") {
    return <span>Loading...</span>;
  }

  if (status === "error") {
    return <span>Error: {error.message}</span>;
  }

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

동기적으로 useQuery 실행

  • enabled → useQuery를 동기적으로 사용 가능
  • useQuery의 3번째 인자로 옵션값이 들어가는데 그 옵션의 enabled에 값을 넣으면 그 값이 true일때 useQuery를 실행
const { data: todoList, error, isFetching } = useQuery("todos", fetchTodoList);
const { data: nextTodo, error, isFetching } = useQuery(
  "nextTodos",
  fetchNextTodoList,
  {
    enabled: !!todoList // true가 되면 fetchNextTodoList를 실행한다
  }
);

useQueries

  • 쿼리 여러 개를 동시에 수행해야 된다는 가정이면 렌더링이 거듭되는 사이사이에 계속 쿼리가 수행되어야 한다면 쿼리를 수행하는 로직이 hook룰에 위배될 수도 있다.
  • 여러 귀찮은 경우가 생기기 때문에 useQueries를 쓴다
  • 하나의 배열에 각 쿼리에 대한 상태 값이 객체로 들어온다
const usersQuery = useQuery("users", fetchUsers);
const teamsQuery = useQuery("teams", fetchTeams);
const projectsQuery = useQuery("projects", fetchProjects);

// 어짜피 세 함수 모두 비동기로 실행하는데, 세 변수를 개발자는 다 기억해야하고 세 변수에 대한 로딩, 성공, 실패처리를 모두 해야한다.
function App({ users }) {
   const userQueries = useQueries(
     users.map(user => {
       return {
         queryKey: ['user', user.id],
         queryFn: () => fetchUserById(user.id),
       }
     })
   )
 }

unique Key

  • unique key를 배열로 넣으면 query 함수 내부에서 변수로 사용 가능하다
const riot = {
  version: "12.1.1"
};

const result = useQueries([
  {
    queryKey: ["getRune", riot.version],
    queryFn: params => {
      console.log(params); // {queryKey: ['getRune', '12.1.1'], pageParam: undefined, meta: undefined}

      return api.getRunInfo(riot.version);
    }
  },
  {
    queryKey: ["getSpell", riot.version],
    queryFn: () => api.getSpellInfo(riot.version)
  }
]);

QueryCache

  • 쿼리에 대해 성공, 실패 전처리를 할 수 있다
const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      console.log(error, query);
      if (query.state.data !== undefined) {
        toast.error(`에러가 났어요!!: ${error.message}`);
      },
    },
    onSuccess: data => {
      console.log(data)
    }
  })
});

useMutations

  • useQuery와는 다르게 create, update, delete 하며 server state에 사이드 이펙트를 일으키는 경우 사용한다
  • useMutation으로 mutation 객체를 정의하고, mutate 메서드를 사용하면 요청 함수를 호출해 요청이 보내진다.
  • useMutation이 반환하는 객체 프로퍼티로 제공되는 상태값은 useQuery와 동일하다
  • mutation.reset = 현재의 error와 data를 모두 지울 수 있다
  • 두 번째 인자로 콜백 객체를 넘겨줘서 라이프사이클 인터셉트 로직을 짤 수도 있다
function App() {
   const mutation = useMutation(newTodo => axios.post('/todos', newTodo))
 
   return (
     <div>
       {mutation.isLoading ? (
         'Adding todo...'
       ) : (
         <>
           {mutation.isError ? (
             <div>An error occurred: {mutation.error.message}</div>
           ) : null}
 
           {mutation.isSuccess ? <div>Todo added!</div> : null}
 
           <button
             onClick={() => {
               mutation.mutate({ id: new Date(), title: 'Do Laundry' })
             }}
           >
             Create Todo
           </button>
         </>
       )}
     </div>
   )
 }
  • useQuery를 사용할때 처럼 실패시 retry가 디폴트는 아니지만, retry 옵션을 줄 수는 있다
useMutation(addTodo, {
   onMutate: variables => {
     // 뮤테이션 시작
     // onMutate가 리턴하는 객체는 이하 생명주기에서 context 파라미터로 참조가 가능하다.
     return { id: 1 }
   },
   onError: (error, variables, context) => {
     // 에러가 났음
     console.log(`rolling back optimistic update with id ${context.id}`)
   },
   onSuccess: (data, variables, context) => {
     // 성공
   },
   onSettled: (data, error, variables, context) => {
     // 성공이든 에러든 어쨌든 끝났을 때
   },
 })

update후에 get 다시 실행

  • react-query 장점으로 update후에 get 함수를 간단히 재실행 할 수 있다
  • mutation 함수가 성공할 대, unique key로 맵핑된 get 함수를 invalidateQueries에 넣어주면 된다
const mutation = useMutation(postTodo, {
  onSuccess: () => {
    // postTodo가 성공하면 todos로 맵핑된 useQuery api 함수를 실행합니다.
    queryClient.invalidateQueries("todos");
  }
});
  • 만약 mutation에서 retur된 값을 이용해서 get 함수의 파라미터를 변경해야할 경우 setQueryData를 사용
const queryClient = useQueryClient();

const mutation = useMutation(editTodo, {
  onSuccess: data => {
    // data가 fetchTodoById로 들어간다
    queryClient.setQueryData(["todo", { id: 5 }], data);
  }
});

const { status, data, error } = useQuery(["todo", { id: 5 }], fetchTodoById);

mutation.mutate({
  id: 5,
  name: "nkh"
});

마무리

  • React-query을 사용할 경우에는 어떤 데이터를 언제 fetch하면 되는지 목표만 기술하기 때문에 선언적으로 프로그래밍 할 수 있게 된다. 이렇게 되면 개발자가 코드를 읽을 때 글을 읽어내려가듯이 자연스럽게 흐름을 파악할 수 있다
  • useQuery 파라미터로 API 데이터의 만료 시간, 리프레싱 주기, 데이터를 캐시에서 유지할 기간, 브라우저 포커스 시 데이터 리프레시 여부, 성공/에러 콜랙 등의 기능을 제어할 수 있다
  • 캐시를 관리하기 위해 QueryClient 인스턴스를 사용한다

출처 :

https://maxkim-j.github.io/posts/react-query-preview

https://kdinner.tistory.com/113

https://kyounghwan01.github.io/blog/React/react-query/basic/#update후에-get-다시-실행

https://fe-developers.kakaoent.com/2022/220224-data-fetching-libs/