React-Query Optimistic UI

전해림·2025년 3월 20일

React-Query Optimistic Updates

사용자 경험을 향상시키기 위해 서버 응답을 기다리지 않고 UI를 미리 업데이트하는 기법이다. 이를 통해 사용자는 서버 응답을 기다리는 동안 지연 시간을 느끼지 않고 피드백을 받을 수 있다.

장바구니 기능을 예로 들면

  1. 사용자가 장바구니 담기 or 삭제를 누른다.
  2. 장바구니 api 요청을 서버로 전송한다.
  3. 서버로부터 요청에 대한 응답을 받는다.
  4. 받은 응답을 바탕으로 UI를 업데이트한다.

Optimistic UI를 적용한 과정

  1. 사용자가 장바구니 담기 or 삭제를 누른다.
  2. 장바구니 담기 api가 성공할 것이라고 가정하고 UI를 먼저 업데이트한다.
  3. 만약 에러가 발생하면 에러 메세지를 보여주고 UI 변경사항을 롤백한다.

React-Query의 useMutation hook에서 제공하는 onMutate, onError, onSuccess, onSettled등의 옵션을 활용하면 UI를 미리 업데이트한 후 에러가 발생했을 경우 롤백하는 것도 가능해진다.

Optimistic UI를 사용하는 이유

장바구니 기능을 사용할때 대부분은 즉각적인 반응을 보이지만, 다만 성능이 좋지 않는 디바이스를 사용하거나, PC에서 많은 작업을 동시에 처리 중이어서 일시적으로 느려진 상태, 또는 네트워크 환경이 좋지 않은 경우에는 즉각적인 반응이 나타나지 않아 오류가 발생한 것처럼 보일 수 있다.

여기서 OPtimistic Updates를 활용하여 API 응답ㅇ이 도착하기 전에 미리 UI에 변경사항을 반영해 놓고, 이후에 에러 처리나 상태 값을 업데이트하는 방식을 적용하면 실제 API 처리 시간은 비슷하더라도 사용자가 경험하는 체감 속도는 크게 개선될 것이다.

Optimistic UI를 사용

export const useShoppingCart = (
	options?: Omit<UseMutationOptions<AxiosResponse<void>, AxiosError, string>, 'mutateKey' | 'mutateFn'>,
) => {
	return useMutation([SHOPPING_CART_MUTATION_KEY], fetchShoppingCart, options);
};
const {mutateAsync: addShoppingCartAsync, isLoading: isLoadingAddShoppingCart } = useShoppingCart({
	onMutate: async() => {
		//뮤테이션 시작 전에 실행되는 콜백 함수
		//현재 쿼리 데이터를 백업하고 optimistic 업데이트를 수행
		await qeuryClinet.cancelQuerys([MATCH_LIST_QUERY_KEY, pageParams]);
		const previousData = queryClient.getQueryData<{
			pages:{
				matchingList: MatchingStatuesListVO;
				page: PageState;
			}[];
		}>([MATCH_LIST_QUERY_KEY, page Params]);
		
		//Optimistic 업데이트 수행
		queryClient.setQueryData([MATCH_LIST_QUERY_KEY, pageParams], () => {
			return{
				...previusData,
				pages:[
					{
						...previousData?.pages[0],
						matchingList:{
							...previouData?.pages[0].matchingList.matchingList.map((item) => {
								if(item.matchingSn === matchingSn){
									return{
										...item,
										ShoppingYn:true,
									};
								}
								return item;
							}),
						},
					},
				],
			};
		});
		
		return {previuseDat};
	},
	
	onError:(_error, _newData, context) = > {
		//뮤테이션 실패 시 실행되는 콜백 함수
		//백업된 데이터를 사용하여 이전 상태로 롤백
		queryClient.setQueryData(
			[MATCH_LIST_QUERY_KEY, pageParams],
			(context as {previousData: MatchingStatusDto[]}).previusData,
		);
    openToast({ iconType: 'info', type: 'error', content: '에러 발생 시 나타나는 토스트' })
   },
   onSuvvess: () => {
     openToast({ iconType: 'info', type: 'error', content: '장바구니 담기 성공' })
   },
   onSettled: () => {
	    //뮤테이션 완료 후 실행되는 콜백 함수
	    //쿼리 무효화 또는 리페칭 등의 작업 수행
	    queryClient.invalidateQueries([MATCH_LIST_QUERY_KEY, pageParams]);
	  },
	});
  • onMutate
    • Optimistic Updates를 덮어씌우지 않기 위해 refetch하려는 쿼리를 cancel한다.
    • refetch 하려는 쿼리의 getQueryData 메서드로 이전 데이터를 가져오고, setQueryData메서드로 데이터를 업데이트 한다.
  • onError
    • 에러가 발생한 경우 onMutate로 넘겨준 previousDatafmf setQueryData를 통해 이전 데이터로 롤백한다.
  • onSettled
    • 모든 error 또는 success 이후에 원하는 쿼리를 refetch하도록 한다.

결과

네트워크 속도가 빠를때와 에러가 발생하는 경우에는 두 가지 방식이 비슷하게 동작한다. 즉시 에러 발생한다면 적용 전에는 단순히 onError만 호출하면 되는 반면, 적용 후에는 onMutate에서 setQueryData로 UI관련 업데이트를하고, 이후 onError에서 다시 setQueryData를 호출하여 데이터를 롤백하는 차이가 있다.

UI가 먼저 업데이트 되니 사용자 입장에서는 빠르다고 느낄 수 있다.

정리

Optimistic Updates를 활용하면 빠른 사용자 피드백이라는 이점도 얻으면서 혹시 에러가 발생하더라도 추가적인 Ui표시와 데이터 롤백이 가능하기에 좋은사용자 경험을 만들 수 있을 것 같다.

참고

profile
프론트엔드 개발자 전해림입니다

0개의 댓글