[React] React Query 공부하기

·2023년 1월 24일
1

React

목록 보기
17/21

GraphQL을 썼던 나는 회사에서 Rest API를 쓰면서 아주 다양한 불편함을 겪는 중...
동기 덕분에 React Query란 라이브러리를 알게 되었고, GraphQL을 썼던 때와 생김새가 비슷하다고 생각됐다
그래도 처음 쓰는 라이브러리이고 많이 사용되고 있는 라이브러리이니까 공부 필요..!

(구글링도 하고 공식 사이트도 보다가, 리액트 쿼리에 대해 정리를 진짜 잘해둔 블로그를 발견했다 항상 유익한 블로그글들 감사합니다!)

📌 React Query

📍 React Query란?

React Query는 서버의 값을 클라이언트에 가져오거나, 캐싱, 값 업데이트, 에러핸들링 등 비동기 과정을 더욱 편하게 하는데 도움을 주는 라이브러리이다.

📍 React Query 장점

  • 캐싱
  • get한 데이터에 대해 update할 경우 자동으로 get을 다시 수행한다. (refetch)
  • 데이터가 오래 되었다고 판단될 경우 다시 get을 수행한다. (invalidateQueries)
  • 동일한 데이터를 여러 번 요청할 경우 한 번만 요청한다. (옵션으로 중복 호출 허용 시간 조절 가능)
  • 무한 스크롤 (Infinite Queries)
  • 비동기 과정을 선언적으로 관리할 수 있다.
  • react hook과 사용하는 구조가 비슷하다.

📌 React Query 사용하기

📍 첫 사용 셋팅하기

📝 설치하기

npm install react-query 또는 yarn add react-query

📝 index.tsx 셋팅하기

React Query를 사용하기 위해서 QueryClientProvider를 최상단에서 감싸줘야 한다.

import {QueryClient, QueryClientProvider} from 'react-query';

const queryclient = new QueryClient();

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

📍 useQuery

import {useQuery} from 'react-query';

const {data, isLoading, refetch, error} = useQuery(queryKey, queryFn, options);

📝 QueryKey

QueryKey를 기반으로 데이터 캐싱을 관리한다.
QueryKey는 문자열 또는 배열로 지정할 수 있다.

useQuery('todos', ...)
useQuery(['todos'], ...)

쿼리가 변수에 의존하는 경우 QueryKey에도 해당 변수를 추가해줘야 한다.

const [data, isLoading, refetch, error} = useQuery(['todos', id], () => axios.get(`https://.../${id}`));

📝 Query Function

useQuery의 두 번째 인자에 promise를 반환하는 함수를 넣어줘야 한다.

useQuery('todos', fetchTodos);
useQuery(['todos', todoId], () => fetchTodoById(todoId));
useQuery(['todos', todoId], async () => {
  const data = await fetchTodoById(todoId);
  return data
});

📝 Query Options

enabled

enabled는 쿼리가 자동으로 실행되지 않게 설정하는 옵션이다.

const {data} = useQuery(
  ['todos', id], () => fetchTodoById(id), {
    enabled: !!id,
  });

위와 같이 옵션을 작성할 경우, id가 존재할 때만 쿼리 요청을 한다.

retry

retry는 실패한 쿼리를 재시도하는 옵션이다.
기본적으로 쿼리 실패 시 세 번 재시도하며, true로 설정하면 쿼리 실패 시 무한 재시도하고 false로 설정하면 재시도를 하지 않는다. 특정 숫자 입력 시 그 숫자만큼 재시도한다.

const {data} = useQuery(
  ['todos', id], () => fetchTodoById(id), {
    retry: 10
  });

staleTime

staleTime은 데이터가 fresh 상태로 유지되는 시간이다.
해당 시간이 지나면 stale 상태가 된다.
default staleTime은 0이며, fresh 상태에서는 쿼리가 다시 mount 되어도 fetch가 실행되지 않는다.

const {data} = useQuery(
  ['todo', () => fetchTodoById(id), {
    staleTime: 1000
  });

cacheTime

cacheTime은 inactive 상태인 캐시 데이터가 메모리에 남아있는 시간이다.
default cacheTime은 5분이며, 이 시간이 지나면 캐시 데이터는 가비지 컬렉터에 의해 메모리에서 제거된다.

refetchOnMount

refetchOnMount는 데이터가 stale 상태일 경우 마운트 시 마다 refetch를 실행하는 옵션이다.
default는 true이며, always로 설정하면 마운트 시 마다 매번 refetch를 실행한다.

refetchOnWindowFocus

refetchOnWindowFocus는 데이터가 stale 상태일 경우 윈도우 포커싱 될 때마다 refetch를 실행하는 옵션이다.
크롬에서 다른 탭을 눌렀다가 다시 원래 보고 있던 탭을 눌렀을 경우 혹은 개발자 도구 창을 켜서 개발자 도구 창을 사용하다 페이지 내부를 다시 클릭하는 경우가 이에 해당한다.
default는 true이며, always로 설정하면 항상 윈도우 포커싱 될 때마다 refetch를 실행한다.

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
    },
  },
});

index.tsx에서 queryClient 생성 시, default option에 refetchOnWindowFocus를 설정할 수 있다. 위와 같이 false로 설정 시 프로젝트 전체에 refetch 기능이 실행되지 않는다.

onSuccess

onSuccess는 쿼리 성공 시 실행되는 함수이다.
매개변수 data는 성공 시 서버에서 넘어오는 response값이다.

const { data: content, isLoading: isContentLoading } = useQuery<IContentInfo>(
    ["content", currentLanguage],
    getContentList,
    {
      onSuccess(data) {
        setPlayerSetting((prev) => {
          return {
            ...prev,
            nowPlaying: data.live_play_info.hls_url,
          };
        });
        setLiveBoard({
          home_team_name: data.live_board_info.home_team_name,
          away_team_name: data.live_board_info.away_team_name,
          home_team_icon: data.live_board_info.home_team_icon,
          away_team_icon: data.live_board_info.away_team_icon,
          present_round: data.live_board_info.present_round,
          home_team_score: data.live_board_info.home_team_score,
          away_team_score: data.live_board_info.away_team_score,
        });
      },
    },
  );

onError

onError는 쿼리 실패 시 실행되는 함수이다.
매개변수로 에러 값을 받을 수 있다.

const {data} = useQuery(
   ['todo', () => fetchTodoById(id), {
	onError: error => {
      console.log(error)
    }
  });

initialData

initialData를 설정하면 쿼리가 아직 생성되지 않았거나 캐시되지 않았을 때 쿼리 캐시의 초기 데이터로 사용된다.
staleTime이 설정되지 않은 경우 초기 데이터는 기본적으로 stale 상태로 간주한다.

const {data} = useQuery('todos', () => fetch('/todos'), {
     initialData: initialTodos,
   })

📍 useMutation

import {useMutation} from 'react-query';

const {data, isLoading, mutate, mutateAsync} = useMutation(mutationFn, options);

mutate(variables, {
  onError,
  onSttled,
  onSuccess
});

📝 Mutation Function

비동기 작업을 수행하고 promise를 반환하는 함수이다.
즉, axios를 이용해 서버에 API를 요청하는 부분이다.

const savePerson = useMutation((person: Iperson) => axios.post('/savePerson', person));

const savePerson = useMutation({
  mutationFn: (person: Iperson) => axios.post('/savePerson', person)
});

📝 Mutation Options

onMutate

onMutate는 mutation 함수가 실행되기 전에 실행되고 mutation 함수가 받을 동일한 변수가 전달된다.
optimistic update 사용 시 유용하다.

onSuccess

onSuccess는 mutation이 성공하고 결과를 전달할 때 실행된다.

onError

onError는 mutation이 error를 만났을 때 실행된다.

onSettled

onSettled는 mutation이 성공해서 성공한 데이터 또는 error가 전달될 때 실행된다.
(성공, 실패 상관없이 결과가 전달된다.)

useMutation(addTodo, {
  onSuccess: (data, variables, context) => {
    // success
  },
  onError: (error, variables, context) => {
    // error
  },
  onSettled: (data, error, variables, context) => {
    // settled
  }
});

📝 mutate

mutate를 호출해서 mutation을 실행시킨다.
variables는 mutationFn에 전달하는 객체이며, onSuccess, onSettled, onError는 useMutation의 option과 동일하다.

❗️ useMutation, mutate 두 곳에서 추가 콜백 (onSuccess, onSettled, onError)을 실행할 경우 1. useMutation의 추가 콜백 2. mutate의 추가 콜백 순서로 실행된다.
❗️ 컴포넌트가 unmount될 경우 추가 콜백이 실행되지 않는다.

mutate(todo, {
  onSuccess: (data, variables, context) => {
    // success
  },
  onError: (error, variables, context) => {
    // error
  },
  onSettled: (data, error, variables, context) => {
    // settled
  }
});

📝 mutateAsync

mutate와 동일하지만, promise를 반환한다.

📝 useMutation 예시

import { useMutation } from "react-query";
import axios from 'axios';

interface TodoType {
  id: number;
  todo: string;
}

const addTodo = async (newTodo: TodoType): Promise<TodoType> => {
  const { data } = await axios.post<TodoType>(`/todos`, newTodo);
  return data;
};

// api 요청하는 함수(addTodo) 를 작성하지 않았을 경우
const { mutate, isLoading, isError, error, isSuccess } = useMutation(newTodo => {
  return axios.post<TodoType>('/todos', newTodo);
});
// api 요청하는 함수(addTodo) 를 작성했을 경우
const { mutate, isLoading, isError, error, isSuccess } = useMutation(addTodo);

export default function App() {
  return (
    <div>
      {
        isLoading ? (
          'Adding todo...'
        ) : (
        <>
          {isError && <p>error: {error.message}</p>}
            
          {isSuccess && <p>Todo added!</p>}
            
          <button
            onClick={() => {
              mutate({ id: 1, todo: 'useMutation 블로그 작성하기' })
            }}
          >
               작성 완료
          </button>
        </>
        )
      }
    </div>
  );
}

📍 useQuery, useMutation 응용편

📝 useQuery 공통 utill로 빼기

import { AxiosError } from 'axios';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { fetchTodos } from 'src/api/todos';
import { TodoType } from 'src/types/todoType';
import { queryKeys } from 'src/types/commonType';

export default function useTodosQuery(
  options?: UseQueryOptions<TodoType[], AxiosError>
): UseQueryResult<TodoType[], AxiosError> {
  return useQuery(queryKeys.todos, fetchTodos, options);
}
import useTodosQuery from 'src/utills/useTodosQuery';

// option 추가하지 않는 경우
const {data, isLoading} = useTodosQuery()
// option 추가하는 경우
const {data, isLoading} = useTodosQuery({staleTime: 60});

📝 useMutation 공통 utill로 빼기

const { mutate: addTodoMutate } = useAddTodoMutation();

const handleAddTodo = useCallback(() => {
  addTodoMutate({
    id: 1, todo: 'mutate에서의 추가 콜백'
  },
  {
    onSuccess: (data) => {
      alert('Todo added!');
    },
  });
}, [addTodoMutate]);

return (
  // ...
  <button onClick={handleAddTodo}>작성 완료</button>
  // ...
)

📝 useMutation과 invalidateQueries 조합하여 refetch 구현하기

기존 쿼리를 강제로 오래된 데이터로 취급하여, useQuery로 return 받은 data를 그대로 UI에 렌더링하는 경우 사용할 수 있다.
1. todo를 새로 작성하고 create mutation을 실행한다.
2. onSettled나 onSuccess 콜백 함수에 todo list 쿼리를 invalidateQueries()해주면 최신 값을 refetch해준다.

import { AxiosError } from 'axios';
import { useMutation, UseMutationResult, useQueryClient } from 'react-query';
import { addTodo } from 'src/api/todos';
import { TodoType } from 'src/types/todoType';
import { queryKeys } from 'src/types/commonType'; 

export default function useAddTodoMutation(): UseMutationResult<TodoType, AxiosError, TodoType> {
  const queryClient = useQueryClient();
  return useMutation(addTodo, {
    onSuccess: () => {
      queryClient.invalidateQueries(queryKeys.todos); 
    },  
    onError: (error) => {
      console.error(error);
    },
  });
}

invalidateQueries 가 실행되어 쿼리가 무효화되면 해당 쿼리는 오래된 것으로 취급되어 쿼리를 다시 refetch 한다.

📝 optimistic update

  • queryClient.cancelQueries : optimistic update를 덮어스지 않도록 발신 쿼리를 취소하는 데 사용한다.
  • queryClient.getQueryData : 기존 쿼리의 상태를 가져오는 동기 함수로, 쿼리가 존재하지 않으면 undefined를 반환한다.
  • queryClient.setQueryData : 쿼리의 캐시된 데이터를 즉시 업데이트할 수 있는 동기 함수로, 쿼리가 존재하지 않으면 생성된다.
import { AxiosError } from 'axios';
import { useMutation, UseMutationResult, useQueryClient } from 'react-query';
import { addTodo } from 'src/api/todos';
import { TodoType } from 'src/types/todoType';
import { queryKeys } from 'src/types/commonType';  // useQuery 실용 편 참조

export default function useAddTodoMutation(): UseMutationResult<
  TodoType,
  AxiosError,
  TodoType,
  {
    previousTodos: TodoType[] | undefined;
  }
> {
  const queryClient = useQueryClient();
  
  return useMutation(addTodo, {
    onMutate: async (newTodo: TodoType) => { // mutate가 호출될 때
      // 쿼리를 확실하게 취소하고
      await queryClient.cancelQueries(queryKeys.todos);
      // 쿼리 상태를 가져온다(이전 값 스냅샷)
      const previousTodos = queryClient.getQueryData<TodoType[]>(queryKeys.todos);

      if (previousTodos) {
        // previousTodos 가 있으면 setQueryData 를 이용하여 즉시 새 데이터로 업데이트 해준다.
        queryClient.setQueryData<TodoType[]>(queryKeys.todos, (old) => [
          ...(old as TodoType[]),
          newTodo,
        ]);
      }
      return { previousTodos }; // 이전 값을 리턴한다
    },
    onError: (
      err: AxiosError,
      variables: TodoType,
      context?: { previousTodos: TodoType[] | undefined }
    ) => {
      if (context?.previousTodos) { // error 를 만났을 경우 onMutate에서 반환된 값으로 다시 롤백시켜준다.
        queryClient.setQueryData<TodoType[]>(queryKeys.todos, context.previousTodos);
      }
    },
    onSettled: () => { // mutation이 끝나면 (성공유무 상관없이) 쿼리를 무효화 처리하고 새로 가져온다.
      queryClient.invalidateQueries(queryKeys.todos);
    },
  });
}

< 참고 : https://velog.io/@kimhyo_0218/React-Query-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%BF%BC%EB%A6%AC-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-useQuery
https://velog.io/@kimhyo_0218/React-Query-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%BF%BC%EB%A6%AC-useMutation-%EC%8B%A4%EC%9A%A9-%ED%8E%B8custom-hook-%EC%9C%BC%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90
https://jforj.tistory.com/244
https://react-query-v3.tanstack.com/ >

profile
개발을 개발새발 열심히➰🐶

0개의 댓글