Tanstack Query - 2. Mutation

miinhho·2025년 6월 16일

Tanstack-Query

목록 보기
2/4
post-thumbnail

아래 글은 tanstack query 공식 문서(5.80.7)를 기반으로 쓰여졌습니다. 대부분의 예제와 설명은 tanstack query 문서에서 가져왔습니다.


useMutation

쿼리와 다르게, mutation 은 데이터 변경 작업이나 서버 사이드 작업을 할 수 있다. Tanstack Query 는 이를 위해 useMutation 훅을 제공한다.

function Todo() {
  const { isPending, isError, isSuccess, error, mutate } = useMutation({
    mutationFn: (newTodo) => {
      return axios.post('/todos', newTodo)
    },
  })

  return (
    <div>
      {isPending ? (
        'Adding todo...'
      ) : (
        <>
          {isError ? (
            <div>An error occurred: {error.message}</div>
          ) : null}

          {isSuccess ? <div>Todo added!</div> : null}

          <button
            onClick={() => {
              mutate({ id: new Date(), title: 'Do Laundry' })
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  )
}

대표적으로 isPending, isError, isSuccess 를 통해 작업의 상태를 확인할 수 있고, error 로 오류를 받아올 수 있다.

mutate 를 통해 mutation 함수에 변수를 전달해 호출할 수 있다.


// This will not work in React 16 and earlier
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (event) => {
      event.preventDefault()
      return fetch('/api', new FormData(event.target))
    },
  })

  return <form onSubmit={mutation.mutate}>...</form>
}

// This will work
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (formData) => {
      return fetch('/api', formData)
    },
  })
  const onSubmit = (event) => {
    event.preventDefault()
    mutation.mutate(new FormData(event.target))
  }

  return <form onSubmit={onSubmit}>...</form>
}

mutate 함수는 비동기 함수이기 때문에, React 16 과 그 이전 버전에서는 직접적으로 사용할 수 없다.

위의 예제에서 React 16 이전 버전에서는 new FormData() 라는 객체에 event.target 의 값을 저장하여 사용한다.

이는 React 의 Event Pooling 과 관련이 있다. React 16 까지는 Event Pooling 을 통해 SyntheticEvent 를 재사용해서 onSubmit 과 같은 함수에서 event 객체를 비동기로 받아오기 위해서는 event.persist() 를 사용해서 React 가 Event Object 의 프로퍼티를 유지하도록 해야한다.


Mutation 상태 초기화

어떤 상황에서는 mutation 요청의 errordata 를 초기화해야 할 수도 있다. 초기화를 위해서는 reset 함수를 사용할 수 있다.

const CreateTodo = () => {
  const [title, setTitle] = useState('')
  const { mutate, error, reset, error } = useMutation({ mutationFn: createTodo })

  const onCreateTodo = (e) => {
    e.preventDefault()
    mutate({ title })
  }

  return (
    <form onSubmit={onCreateTodo}>
      {error && (
        <h5 onClick={() => reset()}>{error}</h5>
      )}
      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <br />
      <button type="submit">Create Todo</button>
    </form>
  )
}

Mutate Side Effect

useMutation 에서 제공하는 옵션을 사용해서 mutation 중 side effect 를 간편하게 발생시킬 수 있다.

useMutation({
  mutationFn: addTodo,
  onMutate: (variables) => {
    // A mutation is about to happen!

    // Optionally return a context containing data to use when for example rolling back
    return { id: 1 }
  },
  onError: (error, variables, context) => {
    // An error happened!
    console.log(`rolling back optimistic update with id ${context.id}`)
  },
  onSuccess: (data, variables, context) => {
    // Boom baby!
  },
  onSettled: (data, error, variables, context) => {
    // Error or success... doesn't matter!
  },
})
  • onMutate: mutate 가 발생했을 때 호출된다.
  • onError: 에러가 발생했을 때 호출된다.
  • onSuccess: 성공했을 때 호출된다.
  • onSettled: 항상 호출된다.

useMutation({
  mutationFn: addTodo,
  onSuccess: (data, variables, context) => {
    // I will fire first
  },
  onError: (error, variables, context) => {
    // I will fire first
  },
  onSettled: (data, error, variables, context) => {
    // I will fire first
  },
})

mutate(todo, {
  onSuccess: (data, variables, context) => {
    // I will fire second!
  },
  onError: (error, variables, context) => {
    // I will fire second!
  },
  onSettled: (data, error, variables, context) => {
    // I will fire second!
  },
})

mutate 함수에서도 side effect 를 발생시킬 수 있다. 다만 useMutation 보다 이후에 처리된다.


연속된 mutation

onSuccess, onError, onSettled callback 은 연속된 mutation 에서는 처리가 살짝 달라진다. mutate 함수에서는 컴포넌트가 여전히 마운트될때에 한번만 실행된다.

이유는 mutation observer 가 mutate 함수가 호출될 때마다 매번 삭제되고 구독 취소되기 때문이다. 이와 반대로 useMutationmutate 호출마다 실행된다.

useMutation({
  mutationFn: addTodo,
  onSuccess: (data, variables, context) => {
    // Will be called 3 times
  },
})

const todos = ['Todo 1', 'Todo 2', 'Todo 3']
todos.forEach((todo) => {
  mutate(todo, {
    onSuccess: (data, variables, context) => {
      // Will execute only once, for the last mutation (Todo 3),
      // regardless which mutation resolves first
    },
  })
})

Mutate Promise

성공 시 resolve 되고 실패 시 error 를 던지는 Promise 를 얻기 위해서는, mutate 가 아닌 mutateAsync 을 사용해야 한다.

const mutation = useMutation({ mutationFn: addTodo })

try {
  const todo = await mutation.mutateAsync(todo)
  console.log(todo)
} catch (error) {
  console.error(error)
} finally {
  console.log('done')
}

Retry

Tanstack Query 는 기본적으로 mutation 에서 에러가 발생했을 때 재시도를 하지 않는다. 하지만 retry 옵션을 사용한다면 가능하다.

const mutation = useMutation({
  mutationFn: addTodo,
  retry: 3,
})

Mutation 저장

Mutation 을 필요하다면 스토리지에 저장하고 나중에 재개할 수 있다. 이는 hydration 함수에 의해 수행된다.

const queryClient = new QueryClient()

// Define the "addTodo" mutation
queryClient.setMutationDefaults(['addTodo'], {
  mutationFn: addTodo,
  onMutate: async (variables) => {
    // Cancel current queries for the todos list
    await queryClient.cancelQueries({ queryKey: ['todos'] })

    // Create optimistic todo
    const optimisticTodo = { id: uuid(), title: variables.title }

    // Add optimistic todo to todos list
    queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo])

    // Return context with the optimistic todo
    return { optimisticTodo }
  },
  onSuccess: (result, variables, context) => {
    // Replace optimistic todo in the todos list with the result
    queryClient.setQueryData(['todos'], (old) =>
      old.map((todo) =>
        todo.id === context.optimisticTodo.id ? result : todo,
      ),
    )
  },
  onError: (error, variables, context) => {
    // Remove optimistic todo from the todos list
    queryClient.setQueryData(['todos'], (old) =>
      old.filter((todo) => todo.id !== context.optimisticTodo.id),
    )
  },
  retry: 3,
})

// Start mutation in some component:
const mutation = useMutation({ mutationKey: ['addTodo'] })
mutation.mutate({ title: 'title' })

// If the mutation has been paused because the device is for example offline,
// Then the paused mutation can be dehydrated when the application quits:
const state = dehydrate(queryClient)

// The mutation can then be hydrated again when the application is started:
hydrate(queryClient, state)

// Resume the paused mutations:
queryClient.resumePausedMutations()

Hydration

dehydrate
dehydrateHydrationBoundaryhydrate 를 통해 hydrated 될 수 있는 cache 의 고정된 형태를 저장한다. 기본적으로 성공한 쿼리만 포함하며, 서버에서 클라이언트로 prefetch 된 쿼리를 전달하거나 쿼리를 로컬 스토리지나 다른 장소에 저장할 때 유용하다.

import { dehydrate } from '@tanstack/react-query'

const dehydratedState = dehydrate(queryClient, {
  shouldDehydrateQuery,
  shouldDehydrateMutation,
})

어떤 스토리지 시스템에서는 JSON 직렬화가 가능한 값들을 요구할 수 있다. Error, undefined 같은 JSON 으로 자동 직렬화가 되지 않는 값들을 dehydrate 해야 한다면 직렬화를 직접 해야 한다.

기본적으로 성공적인 쿼리만 포함하므로, Error 를 포함하고 싶다면 shouldDehydrateQuery 를 설정해야 한다.

// server
const state = dehydrate(client, { shouldDehydrateQuery: () => true }) // to also include Errors
const serializedState = mySerialize(state) // transform Error instances to objects

// client
const state = myDeserialize(serializedState) // transform objects back to Error instances
hydrate(client, state)

hydrate
hydrate 는 이전에 dehydrate 된 상태를 cache 로 추가한다.

import { hydrate } from '@tanstack/react-query'

hydrate(queryClient, dehydratedState, options)

hydration 을 하려는 쿼리가 이미 쿼리 캐시에 있다면, hydrate 는 데이터가 캐시에 있는 데이터보다 최신이여야만 데이터를 덮어쓴다.


HydrationBoundary
HydrationBoundary 는 이전에 dehydrate 된 상태를 useQueryClient() 로 반환될 수 있는 queryClient 에 추가한다. 클라이언트에 이미 데이터가 있는 경우엔 timestamp 를 기반으로 최신의 정보로 변경한다.

import { HydrationBoundary } from '@tanstack/react-query'

function App() {
  return <HydrationBoundary state={dehydratedState}>...</HydrationBoundary>
}

쿼리들만 HydrationBoundary 를 통해 dehydrate 될 수 있다.


Mutation 스코프

기본적으로 모든 mutation 은 병렬로 돌아간다. 같은 mutation 의 mutate() 함수를 여러번 호출해도 말이다. 이것을 방지하기 위해서는 id 를 가지는 scope 를 mutation 에 넣으면 된다.

모든 mutation 은 같은 scope.id 를 가지고 있으면 직렬로 실행된다. 이것은 해당 범위 내에 이미 진행중인 mutation 이 있는 경우 isPaused: true 에서 시작된다는 것을 의미한다.

mutation 은 대기열에 추가되고 대기열에서 시간이 끝나면 다시 시작된다.

const mutation = useMutation({
  mutationFn: addTodo,
  scope: {
    id: 'todo',
  },
})


공식 문서에서 아래 파트의 내용을 포함하고 있는 글입니다.

profile
재미있는 걸 좋아합니다

0개의 댓글