[MBTI 테스트] - Tanstack Query에서 Optimistic Updates 적용하기
Optimistic Updates란?
UX를 개선하기 위해 실제 서버 응답을 기다리지 않고 UI를 먼저 업데이트하는 기법
서버 응답이 성공하면 UI를 그대로 유지하고, 실패하면 이전 UI로 돌아간다.
Optimistic Updates는 좋아요 버튼, 북마크 버튼, 장바구니 담기 등에서 많이 사용한다.
예를 들어, 사용자가 좋아요 버튼을 눌렀는데 바로 바뀌지 않고 3-4초 있다가 바뀌면 좋지 못한 사용성을 제공하게 된다.
문제 상황
테스트 결과를 공개로 전환하는 버튼과 테스트 결과를 삭제하는 버튼을 눌렀을 때,
약간의 로딩 시간이 발생한 뒤에 원하는 동작이 발생하였다.
gif의 속도를 기본으로 설정해 두었다. 공개하기 버튼을 눌렀을 때 느리게 비공개하기로 바뀌는 것이 보이는가?
삭제 버튼을 누를 때도 테스트 결과가 바로 사라지는 것이 아니라, 1-2초 정도 늦게 사라진다.
서비스를 사용하는 입장에서는 답답함을 느끼게 된다.
Optimistic Updates 적용 방법
이 글은 tanstackQuery의 기본 사용법을 알고 있다는 가정 하에 쓰는 글이다.
tanstackQuery의 기본 사용법에 대해 잘 모른다면, 기본 사용법 먼저 익히고 보는 것을 추천한다.
💻 기존의 useDeleteTestResult.js 코드
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { QUERY_KEY } from "../constants/constants";
import { deleteTestResult } from "../api/testResults";
export const useDeleteTestResult = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteTestResult,
onSuccess: () => {
queryClient.invalidateQueries([QUERY_KEY]);
},
});
};
기존 코드에서는 기본 설정만 해두었다.
deleteTestResult는 외부 서버에 데이터 삭제 요청을 보내는 함수이다.
이 코드의 동작은 다음과 같다.
사용자가 삭제 버튼을 누르면, 외부 서버에 데이터 삭제 요청을 보내고,
성공했을 시에 쿼리 무효화를 통해 데이터를 갱신한다.
해당 코드에 Optimistic Updates를 적용시켜 보자.
useMutation 안에는 onSuccess 뿐만 아니라 3가지의 속성이 더 존재한다.
3가지의 속성을 모두 이용하여 Optimistic Updates를 구현할 수 있다.
각각의 속성을 코드와 함께 설명하겠다.
📍 onMutate: Mutation이 실행되기 전에 호출됨
캐시를 미리 변경하거나 UI 업데이트를 위해 사용한다.
요청이 실행되기 전에 기존 데이터를 저장하여 롤백이 가능하게 해 준다.
export const useDeleteTestResult = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteTestResult,
onMutate: async (testResultId) => {
// 이전 데이터를 저장함(롤백을 위하여)
await queryClient.cancelQueries({ queryKey: [QUERY_KEY] });
const prevTestResults = queryClient.getQueryData([QUERY_KEY]);
// 캐시를 미리 업데이트함
queryClient.setQueryData([QUERY_KEY], (old) =>
old.filter((test) => test.id !== testResultId)
);
// onError에서 사용할 이전 데이터를 반환함
return { prevTestResults };
},
});
};
📍 onError: Mutation 요청이 실패했을 때 호출됨
Mutation 요청이 실패했을 때 onMutate에서 건네받은 데이터를 사용하여 이전 데이터로 롤백해 준다.
return useMutation({
mutationFn: deleteTestResult,
onError: (_, __, context) => {
// 이전 데이터로 롤백함
queryClient.setQueryData([QUERY_KEY], context.prevTestResults);
},
});
📍 onSettled: Mutation 요청이 성공하든 실패하든 항상 마지막에 호출됨
Mutation 요청의 성공/실패 여부와 상관없이 데이터를 최신 상태로 유지하는데 사용한다.
return useMutation({
mutationFn: deleteTestResult,
onSettled: () => {
// 데이터를 최신 상태로 유지함
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
},
});
💻 전체 코드
return useMutation({
mutationFn: deleteTestResult,
onMutate: async (testResultId) => {
await queryClient.cancelQueries({ queryKey: [QUERY_KEY] });
const prevTestResults = queryClient.getQueryData([QUERY_KEY]);
queryClient.setQueryData([QUERY_KEY], (old) =>
old.filter((test) => test.id !== testResultId)
);
return { prevTestResults };
},
onError: (_, __, context) => {
queryClient.setQueryData([QUERY_KEY], context.prevTestResults);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
},
});
결과
Optimistic Updates를 적용하면 UX가 개선된 것을 바로 느낄 수 있다.
공개하기 버튼에서 비공개하기 버튼으로 빠르게 바뀌는 것이 보이는가?
삭제 버튼을 눌렀을 때도 테스트 결과가 바로 사라진다!
사용자 입장에서 UI가 바로바로 바뀌는 것이 UX측면에서 좋은 경험을 가져다줄 것이다.