[react-query] Optimistic Update

Ell!·2021년 11월 2일
5

react

목록 보기
6/28

개요

이전에 쓴 [react-query] useQuery의 데이터 불변성을 지켜야할까?? 와 이어진다.
이전 글에서는 useQuery로 받아온 data를 어떻게 건들이지 않아야하는지를 다뤘다.

이번에는 해당 데이터를 어떻게 수정해서 mutate까지 이어지는지 적어보고자 한다. (이것때문에 하루종일 맘고생했다😢)

Optimistic Update?

서버 업데이트를 하기 전에 미리 화면의 UI를 바꿔준 후, 서버와의 통신 결과에 따라 확정 / 롤백을 결정하는 방식이다. (아, 어차피 되게 되어있다구~ 해서 낙관적이라고 명명했나보다.)

코드는 다음과 같다.


  const onDragEnd = result => {
    // source : 현재 위치한 droppable의 위치, 인덱스
    // destination : dnd를 마친 후 droppable의 위치, 인덱스

    // 바깥으로 drop 시에
    if (!result.destination) {
      return;
    }

    // 같은 자리에 가져다 두었다면 그냥 리턴
    if (result.destination.index === result.source.index) {
      return;
    }

    const items = reorder(
      gameList,
      result.source.index,
      result.destination.index,
    );

    if (data) gameList = items; // 새로운 것 옮겨두고.

    gameListMutate.mutate(gameList); // 드래그 앤 드랍이 끝나면 이걸 mutate
  };

const component = () => {

...


  let gameList = useMemo(() => data.data, [data]); // data의 불변성을 유지해주기 위해서
  
  const queryClient = useQueryClient();
  const gameListMutate = useMutation(
    items => {
      return axiosInstance.put('게임리스트 API', items);
    },
    {
      // onMutate : mutation function이 시작되기 전에 작동
      onMutate: () => {
        const oldData = queryClient.getQueryData(['game-account-list']);
        // 우리 update overwrite하지 않기 위해 미리 취소
        queryClient.cancelQueries(['game-account-list']); 
		// 미리 UI에 적용시켜 놓음
        queryClient.setQueryData(['game-account-list'], () => {
          return gameList;
        });
        // 만약 에러나서 롤백 되면 이전 것을 써놓음.
        return () => queryClient.setQueryData(['game-account-list'], oldData);
      },
      // success든 error든 invalidate해서 새로 받아옴.
      onSettled: () => {
        return queryClient.invalidateQueries(['game-account-list']);
      },
      // onError의 세번째 인수 rollback이 onMutate의 return 
      onError: (err, values, rollback) => {
        if (rollback) {
          rollback();
        } else {
          console.log(err);
        }
      },
    },
  );
...
}

(+ 계정 위치를 바꿀 때마다 변경되고 새로운 데이터 받아와서 깜빡이는 것 해결)

위의 방법으로 data의 불변성을 유지한채로 통신이 가능해졌으나, 계정의 위치를 이동시킬 때마다 통신이 되어서 매번 새로운 데이터를 불러와서 깜빡이는 문제가 발생했다. 다음의 방법으로 해결했다.

const component = () => {

...


  let gameList = useMemo(() => {
    return data.data
      ? data.data
      : queryClient.getQueryData(['game-account-list']);
  }, [data, queryClient]); // 🌹 gameList에 넣은 data가 없을 때를 대비해서 이전에 캐싱해둔 데이터를 넣어주기로 했다.
  
  
  const gameListMutate = useMutation(
    items => {
      return axiosInstance.put('게임리스트 API', items);
    },
    {
      // onMutate : mutation function이 시작되기 전에 작동
      onMutate:  async newData => { // 🌹 공식 문서를 따라서 newData 부분 수정
        const oldData = queryClient.getQueryData(['game-account-list']);
        // 우리 update overwrite하지 않기 위해 미리 취소
        await queryClient.cancelQueries(['game-account-list']); 
		// 미리 UI에 적용시켜 놓음
        queryClient.setQueryData(['game-account-list'], newData);
        // 만약 에러나서 롤백 되면 이전 것을 써놓음.
        return () => queryClient.setQueryData(['game-account-list'], oldData);
      },
      // success든 error든 invalidate해서 새로 받아옴.
      onSettled: () => {
        //return queryClient.invalidateQueries(['game-account-list']);
        // 🌹 mutate가 일어나고 바로 새로운 것 받아오지 않도록 변경. 이러면 setQueryData에서 설정한 새로운 값이 캐싱되어서 그걸 쓸 것임.
      },
      // onError의 세번째 인수 rollback이 onMutate의 return 
      onError: (err, values, rollback) => {
        if (rollback) {
          rollback();
        } else {
          console.log(err);
        }
      },
    },
  );
  
  // 🌹 대신 저장 버튼 누르면 쿼리 다시 받아오기로.
  // 제출 button click 시에
  const onGameEnrollmentOrderSubmit = () => {
    gameListMutate.mutate(gameList, {
      onSuccess: () => {
        closeModal();
        return queryClient.invalidateQueries(['game-account-list']); // 저장 눌어야만 실제 서버 데이터랑 동기화
      },
      onError: err => {
        console.log(err);
      },
    });
  };
...

11.05 업데이트

data.data에서 받아오는 gameList의 check를 수정할 때, 저장 버튼을 누르지 않고 닫기를 눌러도 저장되는 것 처럼 보이는 버그 발생.

해결방법 : gameList를 lodash의 cloneDeep으로 저장

  let gameList = useMemo(() => {
    return data?.data
      ? _.cloneDeep(data.data)
      : [...queryClient.getQueryData(['game-account-list'])];
  }, [data, queryClient]);

참고

https://velog.io/@sv002/%EC%A2%8B%EC%95%84%EC%9A%94-%EA%B8%B0%EB%8A%A5-react-query-optimistic-update

https://react-query.tanstack.com/guides/optimistic-updates#_top

profile
더 나은 서비스를 고민하는 프론트엔드 개발자.

0개의 댓글