[React Query] 리액트 쿼리 useMutation 실용 편(custom hook 으로 사용해보자)

김효선·2022년 4월 3일
47

React Query 사용기

목록 보기
4/5

지난 번에는 useMutation 의 기본 적인 사용 방법에 대해서 소개했다. 이번에는 전 편에서 잠깐 언급했던 custom hook을 사용하면서 queryClient랑 함께 사용하기와 optimistic updates 에 대한 내용을 적으려한다.

🌟 useMutation 커스텀 훅 with TypeScript

useMutationuseQuery 와 마찬가지로 커스텀 훅으로 만들어서 사용하면 많이 유용하다.

기본 형태의 커스텀 훅(1)

  • /hooks 폴더에서 생성한 useAddTodoMutation 훅에서 onSuccessonError 를 사용했을 때의 경우이다.
// src/hooks/
import { AxiosError } from 'axios';
import { useMutation, UseMutationResult } from 'react-query';
import { addTodo } from 'src/api/todos';
import { TodoType } from 'src/types/todoType';

export default function useAddTodoMutation(): UseMutationResult<TodoType, AxiosError, TodoType> {
  return useMutation(addTodo, {
    onSuccess: (data) => {
      console.log(data); // mutation 이 성공하면 response를 받을 수 있다.
    },  
    onError: (error) => { // mutation 이 에러가 났을 경우 error를 받을 수 있다.
      console.error(error);
    },
  });
}

기본 형태의 커스텀 훅(2)

  • (1)번의 훅에는 단점이 있다.

  • 공통으로 사용되는 훅인데, mutation 이 실행되고 난 후의 행동이 각각 다르다면?? onSuccess 등의 추가 콜백을 달기엔 애매한 상황이 온다.

  • 그럴 땐 useAddTodoMutation 에 달았던 콜백 함수를 지우고, 아래 예시처럼 mutate 옵션 자리에 작성해주면 된다.
    (전 편에서 언급했다시피 mutate 의 추가 콜백은 useMutation 추가 콜백 다음에 실행되며, unmount가 되면 실행되지 않을 수 있다.)

// src/components/
// ...
const { mutate: addTodoMutate } = useAddTodoMutation();

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

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

🌟 queryClient 의 invalidateQueries !

mutation 과 최고의 효율을 자랑하는 invalidateQueries 메소드는 정말 정말 유용하다.

어떨 때 사용할 수 있을까 ? 🤔

바로, 기존 쿼리를 강제로 오래된 데이터로 취급하는 무효화 처리를 하고 싶을 때 쓰면 된다!

<< useQuery 로 return 받은 data 를 그대로 UI에 렌더링 하는 경우의 이야기 입니다. >>
🍃 리액트 쿼리 사용 전
1. todo 를 새로 작성하고 create api 를 요청한다.
2. api 가 성공하고 나면 클라이언트 상태(todo list)에 response(추가한 todo)를 update 해주는 dispatch 같은 액션 함수를 사용해야한다.

🌼 리액트 쿼리 사용 후
1. todo 를 새로 작성하고 create mutation을 실행한다.
2. onSettled 나 onSuccess 콜백 함수에 todo list 쿼리를 invalidateQueries() 작성해주면 알아서 최신 값으로 refetch 된다.

바아로 (1)에 작성했던 코드를 가져와 변형시켜보겠다.

// src/hooks
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> {
  const queryClient = useQueryClient();
  return useMutation(addTodo, {
    onSuccess: () => {
      queryClient.invalidateQueries(queryKeys.todos); // mutation을 성공하면 todo list를 불러오는 useQuery를 무효화 시킨다.
    },  
    onError: (error) => {
      console.error(error);
    },
  });
}
  • invalidateQueries 가 실행되어 쿼리가 무효화되면 해당 쿼리는 오래된 것으로 취급 된다.
  • 쿼리를 다시 refetching 한다!

todo list 를 렌더링 하고 있던 컴포넌트는 새로운 todo 가 추가됨과 동시에 바로 해당 키값의 쿼리를 refetch 시키기 때문에
새로고침이나, 클라이언트 상태에 해당 데이터를 업데이트 시켜주지 않아도 된다. (별도로 클라이언트 상태를 건들 작업이 없을 경우에 해당)

  • 쿼리를 다시 refetch 하는 것을 원하지 않고 단순히 무효화만 시키고 싶을 경우에는 refetchActive: false 를 붙여주면 된다.
queryClient.invalidateQueries(queryKeys.todos, {
 refetchActive: false, 
});

queryClient.invalidateQueries 문서
queryClient.invalidateQueries 문서2


🌟 optimistic updates !

💡 mutation 의 optimistic update 는 사용자가 어떠한 액션을 발생시켰을 때 요청이 성공했는지 실패했는지 아직 알 수 없는 상태에서 성공할 것이라고 낙관적으로 가정하고 사용자가 보고 있는 UI 를 먼저 변화시켜주는 것을 말한다. 사용자에게 빠른 경험을 제공할 수 있다는 큰 장점이 있다!

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);
    },
  });
}
  • queryClient.cancelQuerie : 발신 쿼리를 취소하는 데 사용할 수 있다. (optimistic update를 덮어쓰지 않도록)
  • queryClient.getQueryData : 기존 쿼리의 상태를 가져오는 동기 함수. 쿼리가 존재하지 않으면 undefined 를 반환.
  • queryClient.setQueryData : 쿼리의 캐시된 데이터를 즉시 업데이트할 수 있는 동기 함수. 쿼리가 존재하지 않으면 생성된다.

optimistic updates 를 이용하면 사용자가 api 요청 성공 유무를 기다리지 않고 바로 UI 단에서 변화를 확인할 수 있어 유용하다.
ex) 페이스북 좋아요를 누른다고 가정할 때 좋아요를 누르거나 취소할 때마다 api 결과 기다려야 한다면 매우 답답할 것이다. 사용자가 좋아요를 마구 눌렀다 취소할 수 도 있는데 이럴 때 특히 유용하다.

optimistic updates 문서

모두 리액트 쿼리 쓰세요오오😊

profile
차근차근 나아가는 주니어 프론트엔드 개발자

2개의 댓글

comment-user-thumbnail
2022년 4월 21일

깔끔하게 잘 정리하셨네요 감사합니다

1개의 답글