블로그 목록

useMutation의 mutate와 mutateAsync에 대한 고찰

2024-01-117 min read
React QueryuseMutationTanStack Query

사건의 발단

운영툴 2.0에 횟수제를 추가하면서 react-query v5를 추가하여 사용하였다.

그에 이어 바이비 포스에도 react-query를 v3에서 v5로 버전업 하여 사용하였다.

PR 리뷰 도중 정원님이 mutate 함수에서 onSuccess가 제대로 작동하는지에 대해 comment를 남기셨다.

분명… 운영툴 2.0에서는 잘 작동했는데 포스에서는 작동이 안 하는 것이 아닌가……!

이건 사용하다가도 어려움을 겪을 것 같아 작동 방식에 대해서 알아 보아야겠다고 생각했다.

먼저 운영툴 코드부터 살펴보자

create는 레슨권 생성 useMutation의 mutateAsync 함수이다.

위 기능은 정상 작동한다. onSuccess, onError가 모두 잘 나오는 것을 확인하였다.

다음 포스 코드를 살펴보자

add, edit은 각 프린터의 생성/수정 useMutation의 mutateAsync 함수이다.

register는 프린터 기기 등록 useMutation의 mutateAsync 함수이다.

원래는 add, edit의 onSuccess에서 register 함수가 실행하는 구조였다. 그러나 register는 실행조차 되지 않았다……

그렇기에 기존의 async, await처럼 실행하는 것으로 구현하였다. 근데 또 onError는 실행이 잘됐다. (잘 되는지 확인 테스트는 또 기기 설정이라 포스기 앞에서.. 이 날 포스기 앞에서 살았….)

결론 이후에 펼쳐 볼 것 (추측성 글 주의)

다시 올라와서 살펴보자.

실행이 안됐던 경우 mutateAsync는 이전 스냅샷을 기억하고 있다고 했다. 그러면 add의 onSuccess에서 settingId를 사용하게 되면 이전에 있던 스냅샷이기 때문에 settingId가 없게 될 것이다. 그렇기 때문에 settingId를 가져오지 못하게 될 것이고 onRegister는 실행 실패?

실행이 됐던 경우 단순 message 출력, navigate만 실행하기 때문에 error가 발생될 일이 없는 거다. 그렇기 때문에 실행 성공?

한 가지 더, onError가 실행된 이유는? mutation.ts의 execute를 살펴보면 에러가 날 경우 error 자체를 리턴하고 있다. 그렇기 때문에 error의 경우 어쩌다보니 잘 전달되어 성공?

저번에 보니까 useQuery, useMutation 내부에서 예측 불가한 에러 발생할 땐 콘솔에도, 그 어디에도 에러를 발견할 수가 없었기에.. 이번에도 그랬던 것은 아닐까 생각한다.

근본적인 의문이 들 것이다. mutate 안 쓰고 mutateAsync를 쓴 이유가 뭘까

버튼 클릭 시 위 함수들이 실행되는 것인데 버튼은 클릭 되는 것을 확인했으나 함수(mutate)가 실행이 안 되었다. 근데 이것도 될 때가 있어……

이건 나중에 알아보도록 하자.

두 코드의 차이는 무엇일까

정원님이 알려주신 링크를 보면 onError도 안 되어야 하는 게 정상이다.

코드를 이목저목 뜯어본 결과, 가설을 세워볼 수 있었다.

mutateAsync의 onSuccess가 promise이면 안 된다. → 레슨권은 되었고, 프린터는 안 되었던 이유

따라서 mutateAsync의 onSuccess에 비동기 처리(async await)가 없으면 실행이 된다. 고로 onError 또한 비동기 처리가 모두 없기 때문에 정상적으로 실행되는 것이다.

사실 여기까지 서두였습니다. 이제 알아보도록 하자.

가장 먼저! mutate란?

mutate: (variables: TVariables, { onSuccess, onSettled, onError }) => void
  • mutate를 호출해서 mutation을 실행시킬 수 있다.
  • variables는 mutationFn에 전달하는 객체이다.
  • onSuccess, onSettled, onError는 useMutation option에 설명한 것과 동일하다.

그 다음! mutateAsync란?

mutateAsync: (variables: TVariables, { onSuccess, onSettled, onError }) => Promise<TData>
  • mutate와 같으나 promise를 반환한다!

코드를 뜯어보았다

여기까지 찾아봤는데 딱히 명확한 해답이 보이질 않았다.

그래서 코드를 뜯어봤다. (@tanstack/query-core/src 폴더에 담겨있는 코드들이다.)

observer.mutate(variables, mutateOptions).catch(noop)를 파고 들어보자.

  • mutate: observer를 추가하고 execute 된다. this.#currentMutation.execute의 일부를 보면 mutation을 실행하고 onSuccess를 실행하게 된다.
  • mutateAsync: 그저 실행할 함수가 listener에 추가된다. 그저 콜백이 전해진다.

결론

  1. mutate는 observer가 직접 실행하는 함수이다.
  2. mutateAsync는 subscribe하고 있던 snapshot callback의 mutate를 가져온 함수이다.
  3. mutateAsync는 이전에 기억하고 있던 snapshot에 있는 것을 가져와 실행할 수도 있다는 것.
  4. mutateAsync에서 onSuccess, onError, onSettled가 실행되었다는 건 우연의 일치로 snapshot이 현재와 일치하기 때문에 발생한 일이 아닐까 싶다. 아니면 error 발생 없이 무사히 실행했다거나.
  5. 그렇기 때문에 안전한 사용을 위해, mutate에서만 onSuccess, onError, onSettled를 사용하고, mutateAsync에서는 async, await와 같이 try, catch, finally 문을 이용하여 수동으로 처리하는 방향으로 가야 할 것 같다.
  6. 결국 결론도 추측이라 잘 사용해야 할 것 같다.