React Query 낙관적 업데이트 적용

ws·2024년 11월 18일

Coworkers

목록 보기
2/5

낙관적 업데이트는 사용자 인터페이스를 더 빠르게 반응하게 만들기 위해, 서버의 응답을 기다리지 않고 UI를 미리 업데이트하는 기법입니다. React Query에서는 onMutate 옵션을 사용하여 낙관적 업데이트를 구현할 수 있습니다. 다음은 useUpdateTaskStatus 훅에 낙관적 업데이트를 추가하는 방법입니다.

구현 전 할일 완료

  • 기존 할 일 완료는 할 일 완료 PATCH API 와 할 일 GET API 를 모두 완료하고 나서 할 일 체크 UI 가 렌더링되는 구조입니다. 각 API 는 약 50 밀리초가 소요되어 100 밀리초 이상 시간이 지나야 체크된 UI 가 보여집니다.
  • 기존 할 일 완료는 할 일 상태 업데이트가 성공하면 할 일 목록에 대한 쿼리를 무효화하여(invalidateQueries) 서버에서 다시 데이터를 가져와 UI 를 리렌더링 하는 방식을 사용하고 있습니다.
// 할일 완료 UI
<CheckableText
  isChecked={task.doneAt !== null}
  onChange={() => {
	handleCheckBoxChange(task.id, !task.doneAt);
  }}
>

// 할일 완료 핸들링 함수
const handleCheckBoxChange = (taskId: number, done: boolean) => {
    updateTaskStatus({
      groupId: Number(id),
      taskListId: selectedTaskList.id,
      taskId,
      done,
      // ...
    });
  };

// 할일 상태 업데이트 쿼리
export const useUpdateTaskStatus = () => {
  const queryClient = useQueryClient();
  const { toast } = useToast();
  return useMutation({
    mutationFn: async (params: UpdateTaskStatusRequest) =>
      updateTaskStatus(params),
    onSuccess: (_, params) => {
      queryClient.invalidateQueries({
        queryKey: tasksQueryKeys.tasks({
          groupId: params.groupId,
          taskListId: params.taskListId,
          // ...
        }),
      });
      // ...
    },
    onError: (error) => {
      // ...
    },
  });
};
  • 낙관적 업데이트가 적용된다면 서버의 응답을 기다리지 않고 UI 를 업데이트하므로 해당 시간이 지나지 않더라도 바로 체크 완료 UI 가 보여질 수 있을 것입니다.

낙관적 업데이트 구현

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateTaskStatus } from '@/apis/tasks.api';
import { tasksQueryKeys } from './keys/tasks.keys';
import { UpdateTaskStatusRequest } from '@/types/dto/requests/tasks.request.types';

export const useUpdateTaskStatus = () => {
  const queryClient = useQueryClient();
  const { toast } = useToast();
  return useMutation({
    mutationFn: async (params: UpdateTaskStatusRequest) =>
      updateTaskStatus(params),
    onMutate: async (params) => {
    // 기존 쿼리 취소
      await queryClient.cancelQueries({
        queryKey: tasksQueryKeys.tasks({
          groupId: params.groupId,
          taskListId: params.taskListId,
          date: formatDate(params.startDate),
        }),
      });

	// 기존 상태
      const previousTasks = queryClient.getQueryData<Task[]>(
        tasksQueryKeys.tasks({
          groupId: params.groupId,
          taskListId: params.taskListId,
          date: formatDate(params.startDate),
        })
      );
      
	// 새 queryData 
      queryClient.setQueryData(
        tasksQueryKeys.tasks({
          groupId: params.groupId,
          taskListId: params.taskListId,
          date: formatDate(params.startDate),
        }),
        (old: Task[]) => {
          return old.map((task: Task) =>
            task.id === params.taskId ? { ...task, doneAt: params.done } : task
          );
        }
      );

	// 실패 시 사용될 이전 상태
      return { previousTasks };
    },
    onError: (error, params, context) => {
      if (context?.previousTasks) {
      // 실패 시 이전 상태 적용
        queryClient.setQueryData(
          tasksQueryKeys.tasks({
            groupId: params.groupId,
            taskListId: params.taskListId,
            date: formatDate(params.startDate ?? ''),
          }),
          context.previousTasks
        );
      }
    },
  });
};

설명

  • onMutate: 이 함수는 변이 함수가 실행되기 전에 호출됩니다. 여기서 쿼리를 취소하고, 현재 쿼리 데이터를 가져와서 UI를 낙관적으로 업데이트합니다. previousTasks를 반환하여 에러 발생 시 롤백할 수 있도록 합니다.

  • onError: 변이 중 에러가 발생하면 onMutate에서 반환한 context를 사용하여 이전 상태로 롤백합니다.

  • onSettled: 변이가 성공하거나 실패한 후에 쿼리를 무효화하여 서버의 최신 데이터를 가져옵니다.

이렇게 하면 태스크의 상태가 서버 응답을 기다리지 않고 즉시 UI에 반영되며, 에러가 발생할 경우 이전 상태로 롤백됩니다.

테스트하면 UI 가 서버의 응답을 기다리지 않고 즉시 반영되는 것을 볼 수 있습니다.

React Query의 낙관적 업데이트 문서에서 더 많은 정보를 확인할 수 있습니다.

0개의 댓글