이전에 useQuery의 구조를 살펴보는 글을 작성했습니다. 이번에 소개할 useMutate는 useQuery와는 달리 서버의 데이터를 변경할 때 사용하는 것이 더 적합합니다. useQuery는 ('enabled' 옵션을 사용하지 않은한) 컴포넌트가 새로 렌더링될 때마다 데이터를 요청하는 특징이 있습니다. 이 때문에 주로 데이터의 조회에 사용합니다. 하지만 useMutation은 요청을 보내는 Mutate라는 함수를 반환하기 때문에 개발자가 원하는 때에 서버에 요청을 보낼 수 있습니다. 따라서 일반적으로 POST, PUT, DELETE 등 서버의 데이터를 업데이트하는 요청을 보낼 때 useMutate를 사용합니다.

useMutation은 데이터 변경 요청을 관리하고 요청 상태 및 결과를 제공합니다. 데이터가 성공적으로 변경되었는지, 실패했는지 아니면 아직 처리 중인지에 대한 정보를 알 수 있습니다. 그렇다면 useMutate에 대해 알아볼까요?

useMuate

function TanstackQuery() {

  const { mutate, isPending, isError, error } = useMutation({
      mutationFn: requestArticle,
      onSuccess: () => {
        queryClient.invalidateQueries({ queryKey: ["articleList"] });
      },
    });

  function handleSubmitArticle(data) {
    mutate({
      data,
      method: "PUT",
    });
  };
  return (
      //...
    );
  }

useMuate의 기본 사용방법입니다. useQuery와 비슷하게 생겼지만 QueryKey가 존재하지 않습니다. 앞서 이야기했듯이 서버의 데이터를 변화시키는 요청을 보내기 때문에 캐싱하지 않기때문이죠. 그렇다면 이번에도 option과 반환값들을 알아볼까요?

useMuate의 option

★ mutationFn: (variables: TVariables) => Promise

비동기 작업을 수행하고 Promise를 반환하는 함수입니다. return의 muate에 전달될 함수입니다. muate가 호출될 때마다 해당 함수가 실행되어 서버에 요청을 보냅니다. useQuery의 queryFn과 비슷한 역할을 합니다. 하지만 자동으로 요청을 보내는 useQuery와는 달리 개발자가 mutate를 호출할 때마다 서버에 요청을 보내는 것이 차이점입니다.

★ onMutate: (variables: TVariables) => Promise<TContext | void> | TContext | void

헤당 함수는 mutation이 호출되면 mutationFn이 실행되기 전에 먼저 실행됩니다. 이후에 설명할 낙관적 업데이트를 사용할 때 유용한 기능입니다. 이 함수가 반환하는 값은 onError와 onSettled에 마지막 인자(context)로 전달됩니다.

onSuccess: (data: TData, variables: TVariables, context: TContext) => Promise<unknown> | unknown

mutation이 에러 없이 성공적으로 완료되면 호출되는 함수입니다.이 함수는 mutate가 반환하는 데이터와 mutation을 실행할 때 사용된 변수를 인자로 받습니다.

★ onError: (err: TError, variables: TVariables, context?: TContext) => Promise<unknown> | unknown

mutation에서 오류가 발생하면 실행됩니다. err는 mutation에서 발생한 에러가 할당됩니다.

★ onSettled: (data: TData, error: TError, variables: TVariables, context?: TContext) => Promise<unknown> | unknown

mutation이 성공, 실패에 관게없이 실행이 완료되면 호출되는 함수입니다.

retry: boolean | number | (failureCount: number, error: TError) => boolean

mutation이 실패했을 때 자동으로 재시도 하는 규칙을 설정합니다. retry와 retryDelay는 useQuery의 retry와 같은 방식으로 동작합니다.

반환값

★ mutate: (variables: TVariables, { onSuccess, onSettled, onError }) => void

변수를 사용하여 호출할 수 있는 함수(mutation)입니다. onSuccess, onSettled, onError를 개별적으로 지정해 줄 수도 있습니다. 단, 여기서 지정된 onSuccess, onSettled, onError의 반환값은 무시됩니다. 여러번 요청하는 경우 onSuccess는 가장 마지막 요청에 대해 실행됩니다.

mutateAsync: (variables: TVariables, { onSuccess, onSettled, onError }) => Promise<TData>

mutate와 동일하지만 Promise를 반환합니다.

★ isPending, isSuccess, isError: boolean

isPending: mutationFn이 현재 실행 중인 경우 true
isSuccess: (마지막)mutationFn이 성공한 경우 true
isError: (마지막)mutationFn 시도가 오류로 끝난 경우 true

data: undefined | unknown

mutation이 성공적으로 수행되어 반환한 값, 서버의 응답을 나타냅니다.

error: null | TError

mutation이 실패하여 반환한 에러입니다.

낙관적 업데이트

낙관적 업데이트란?

낙관적 업데이트(Optimistic Update)는 사용자 경험을 향상시키기 위해 웹 애플리케이션에서 사용되는 기법 중 하나입니다. 이 기법은 서버의 응답을 기다리지 않고, 즉시 UI를 업데이트함으로써 애플리케이션의 반응성을 향상시킵니다. 사용자가 애플리케이션에서의 상호작용에 대한 응답 속도를 더 빠르게 느끼도록 도와주며, 애플리케이션의 반응성을 향상시킵니다. 주로 네트워크 요청이나 데이터베이스 업데이트 등과 같이 시간이 걸리는 작업에서 사용됩니다.

예를 들어, 댓글을 작성하는 기능을 가진 웹 애플리케이션이 있다고 가정해봅시다. 사용자가 댓글을 작성하고 "작성" 버튼을 클릭하면, 일반적으로는 서버로 요청을 보내고 서버에서 댓글을 저장한 후 응답을 받아야 합니다. 브라우저는 받은 응답을 기반으로 HTML문서를 업데이트 하죠. 하지만 낙관적 업데이트에서는 이 응답을 기다리지 않습니다. 사용자가 "작성" 버튼을 클릭한 즉시 댓글을 UI에 추가하고, 댓글이 화면에 보여지게 됩니다. 이때 요청은 백그라운드에서 사용자가 인지하지 못하게 처리됩니다. 백그라운에서 요청이 완료되면 브라우저는 서버의 실제 응답을 받아 서버에서 받은 데이터와 일치하도록 다시 UI를 업데이트합니다.

낙관적 업데이트를 사용하면 사용자는 즉각적인 피드백을 받을 수 있고, 전체적인 애플리케이션의 반응성이 향상됩니다. 하지만 사용자와 실제 데이터 사이의 불일치를 처리하는 것이 중요하며, 이를 위해 롤백 기능이나 오류 처리 등이 필요합니다.

useMutate로 낙관적 업데이트하기

다음은 useMutate를 이용하여 낙관적 업데이트를 구현한 것입니다.

export default function TanstackQuery() {
  const params = useParams()

  const { mutate } = useMutation({
    mutationFn: editPost,
    onMutate: async (data) => {

      // 같은 쿼리키에 해당하는 쿼리 취소(useQuery가 보낸 요청을 있을 경우 해당 요청을 취소해야 의도한대로 작동한다.)
      await queryClient.cancelQueries({queryKey: ["post", params.id]})

      const prviousEvent = queryClient.getQueryData(["post", params.id]) // 새로 쿼리 데이터를 설정하기 전에 기존의 데이터 저장


      queryClient.setQueryData(queryKey, data.post); // mutate가 받은 인자로 queryData 업데이트 

      return { prviousEvent }
    },
    onError: (error, data, context) => {
        queryClient.setQueryData(["post", params.id], context.prviousEvent); // 에러 발생 시 queryData를 원래의 상태로 돌려준다.
    },
    onSettled: (data, error, variables, context) => {
        // mutation이 성공 실패와 관련없이 완료되었을때 백엔드서버와 같은 데이터를 보장하기 위해 관련 캐시삭제
        // 해당 데이터가 필요할 경우 재요청을 보내도록함
        queryClient.invalidateQueries(["post", params.id]);
    }
  });

  // ...

  // 버튼 클릭 등의 사용자 입력으로 게시글을 수정
  function handleEditPost(postData, postId) {
    mutate({post: postData, id: postId})
  }
}

참고 자료

Tanstack Query, useMutation

+ Recent posts