본문 바로가기

FrontEnd

리액트 쿼리 : 뮤테이션

반응형

원문 : https://tkdodo.eu/blog/mastering-mutations-in-react-query

리액트 쿼리 라이브러리 작업 대부분은 useQuery hook을 통한 데이터 검색에 관련한 것입니다.

그러나 데이터 작업에 두 번째로 필수적인 부분이 있습니다. 바로 업데이트입니다.

 

이 사용 사례의 경우 React Query는 useMutation hook을 제공합니다.
뮤테이션은 변형 > (서버 데이터 형태의 변경)으로 번역함. 4글자 다 적기도 귀찮고 의미도 애매한것 같아서...

변형(mutations)이 무엇인가요?

일반적으로 변형은 부작용이 있는 함수입니다.
예를 들어 Arrays의 push 메서드를 살펴보십시오.
값을 푸시하는 위치에서 배열을 변경하는 부작용이 있습니다.
const myArray = [1]
myArray.push(2)

console.log(myArray) // [1, 2]
concat은 작업 배열을 직접 조작하는 대신 새 배열을 반환합니다.
const myArray = [1]
const newArray = myArray.concat(2)

console.log(myArray) //  [1]
console.log(newArray) // [1, 2]

 

이름에서 알 수 있듯이 useMutation에도 일종의 부작용이 있습니다.
React Query로 서버 상태를 관리(managing server state)하는 컨텍스트에 있기 때문에
변형은 서버에서 이러한 부작용을 수행하는 함수를 설명합니다.
 
데이터베이스에 todo를 추가하는 것은 변형이 될 것입니다.
사용자 로그인은 사용자에 대한 토큰을 생성하는 부작용을 수행하기 때문에 고전적인 변형이기도 합니다.
일부 측면에서 useMutation은 useQuery와 매우 유사합니다만, 차이점은 명확합니다.

useQuery와의 유사점

useMutation은 useQuery가 쿼리에 대해 수행하는 것처럼 변형 상태를 추적합니다.

loading, error 및 status 필드를 통해 사용자에게 무슨 일이 일어나고 있는지 쉽게 표시할 수 있습니다.

 

또한 useQuery가 가지고 있는 것과 동일한 멋진 콜백(onSuccess, onError 및 onSettled)을 사용할 수 있습니다.
여기까지가 유사점의 끝입니다.

useQuery와의 차이점

useQuery는 선언적이며 useMutation은 명령형 스타일입니다.
즉, 쿼리는 대부분 자동으로 실행됩니다.
종속성을 정의하지만 React Query는 쿼리 실행을 즉시 처리하고 필요한 경우 스마트하게 백그라운드 업데이트도 수행합니다.
화면에 표시되는 내용을 백엔드의 실제 데이터와 동기화된 상태로 유지하기를 원하면 쿼리가 유용합니다.
 
 
변형의 경우 잘 동직하지 않습니다.
브라우저 창에 초점을 맞출 때마다 새로운 할일이 생성된다고 상상해 보세요... 🤨
 
따라서 변형을 즉시 실행하는 대신

React Query는 변형을 만들고 싶을 때마다 호출할 수 있는 함수를 제공합니다.

function AddComment({ id }) {
  // this doesn't really do anything yet
  const addComment = useMutation((newComment) =>
    axios.post(`/posts/${id}/comments`, newComment)
  )

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault()
        // ✅ mutation is invoked when the form is submitted
        addComment.mutate(new FormData(event.currentTarget).get('comment'))
      }}
    >
      <textarea name="comment" />
      <button type="submit">Comment</button>
    </form>
  )
}​

또 다른 차이점은 변형은 useQuery처럼 상태를 공유하지 않는다는 것입니다.

다른 컴포넌트에서 동일한 useQuery 호출을 여러 번 호출할 수 있으며 동일한 캐시된 결과를 반환할 수 있습니다.

그러나 이것은 변형에 대해서는 동작하지 않습니다.


쿼리와 뮤테이션 연결하기

변형은 설계에 따라 쿼리에 직접 연결되지 않습니다.

블로그 게시물을 "좋아요"하는 변형은 해당 블로그 게시물을 가져오는 쿼리와 관련이 없습니다.

이것이 동작하려면 React Query에 없는 일종의 기본 스키마가 필요합니다.

변형이 쿼리의 변경 사항을 반영하도록 하기 위해 React Query는 주로 두 가지 방법을 제공합니다.

Invalidation

이것은 개념적으로 화면을 최신 상태로 유지하는 가장 간단한 방법입니다.
서버 상태는 주어진 시점의 데이터 스냅샷만 표시한다는 것을 기억하십시오.
React Query는 이를 최신 상태로 유지하려고 시도합니만, 정확히 언제 서버 상태가 변하는지는 알 수 없습니다.
 

의도적으로 변형으로 서버 상태를 변경하는 시점은

캐시한 일부 데이터가 이제 "유효하지 않음"(invalid)을 React Query에 알리는 좋은 시점입니다.

React Query는 데이터가 현재 사용 중이면 해당 데이터를 다시 가져오고 가져오기가 완료되면 화면이 자동으로 업데이트됩니다.

라이브러리에 알려야 할 유일한 것은 무효화하려는 쿼리입니다.

const useAddComment = (id) => {
  const queryClient = useQueryClient()

  return useMutation(
    (newComment) => axios.post(`/posts/${id}/comments`, newComment),
    {
      onSuccess: () => {
        // ✅ refetch the comments list for our blog post
        queryClient.invalidateQueries(['posts', id, 'comments'])
      },
    }
  )
}​

 

쿼리 무효화는 꽤 똑똑합니다.

 

모든 Query Filters와 마찬가지로 쿼리 키에 퍼지 일치를 사용합니다.

comment 목록에 대한 키가 여러 개인 경우 모두 무효화합니다.

그러나 현재 활성 상태(컴포넌트가 렌더링 중인)인 항목만 다시 가져옵니다.

나머지는 stale한 으로 표시되어 다음에 사용할 때 다시 가져옵니다.


예를 들어 코멘트를을 정렬할 수 있는 옵션이 있고,

새 코멘트가 추가되었을 때 캐시에 코멘트가 있는 두 개의 쿼리가 있다고 가정해 보겠습니다.

['posts', 5, 'comments', { sortBy: ['date', 'asc'] }
['posts', 5, 'comments', { sortBy: ['author', 'desc'] }

우리는 그 중 하나만 화면에 표시하기 때문에,

invalidateQueries는 하나를 다시 가져오고 다른 하나는 stale한 것으로 표시합니다.


직접 업데이트

때때로, 특히 변형이 이미 알아야 할 모든 것을 반환하는 경우 데이터를 다시 가져오고 싶지 않습니다.
블로그 게시물의 제목을 업데이트하는 변형이 있고
백엔드가 전체 블로그 게시물을 응답으로 반환하는 경우
setQueryData를 통해 쿼리 캐시를 직접 업데이트할 수 있습니다.
const useUpdateTitle = (id) => {
  const queryClient = useQueryClient()

  return useMutation(
    (newTitle) => axios.patch(`/posts/${id}`, { title: newTitle }),
    {
      // 💡 response of the mutation is passed to onSuccess
      onSuccess: (newPost) => {
        // ✅ update detail view directly
        queryClient.setQueryData(['posts', id], newPost)
      },
    }
  )
}

setQueryData를 통해 데이터를 캐시에 직접 넣으면 이 데이터가 백엔드에서 반환된 것처럼 동작하므로

해당 쿼리를 사용하는 모든 컴포넌트가 그에 따라 다시 렌더링됩니다.

#8: Effective React Query Keys에서 직접 업데이트와 위 두 접근 방식의 조합에 대한 몇 가지 더 많은 예를 보여주고 있습니다.

전체 목록을 무효화하는 것이 "더 안전한" 접근 방식입니다.

저는 개인적으로 대부분의 경우 무효화가 우선되어야 한다고 생각합니다.

물론 사용 사례에 따라 다르지만 직접 업데이트가 안정적으로 작동하려면 프런트엔드에 더 많은 코드가 필요하고

백엔드에서 어느 정도 중복 논리가 필요합니다.

예를 들어 정렬된 목록은 업데이트로 인해 내 항목의 위치가 잠재적으로 변경되었을 수 있으므로 직접 업데이트하기가 매우 어렵습니다.


낙관적 업데이트

낙관적 업데이트는 React Query 변형의 주요 포인트 중 하나입니다.

useQuery 캐시는 특히 프리페치와 결합될 때 쿼리 간에 전환 즉시 데이터를 제공합니다.

덕분에 전체 UI가 매우 매끄럽게 느껴집니다.

 

낙관적 업데이트의 아이디어는 변형을 서버로 보내기 전에 변형의 성공을 속이는 것입니다.

성공적인 응답을 받으면 실제 데이터를 보기 위해 뷰를 다시 무효화하기만 하면 됩니다.

요청이 실패할 경우 UI를 변형 이전 상태로 롤백합니다.

 

이것은 즉각적인 사용자 피드백이 실제로 필요한 작은 변형에 적합합니다.

요청이 완료될 때까지 전혀 반응하지 않는 토글 버튼 만큼 나쁜 것은 없습니다.

사용자는 해당 버튼을 두 번 또는 세 번 클릭하면 모든 곳에서 "랙"이 느껴질 것입니다.

Example

예제는 official docs 와 in TypeScript 에서 확인하세요

 

저는 낙관적 업데이트가 약간 과도하게 사용된다고 생각합니다.
모든 변형이 낙관적으로 수행될 필요는 없습니다.
롤백을 위한 UX가 좋지 않기 때문에 거의 실패하지 않는지 확인해야 합니다.
제출할 때 닫히는 대화 상자의 form이나 업데이트 후 세부 정보 보기에서 목록 보기로 리디렉션되는 것을 상상해 보십시오.
이러한 작업이 조기에 완료되면 취소하기 어렵습니다.
 
또한 즉각적인 피드백이 실제로 필요한지 확인하십시오(위의 토글 버튼 예와 같이).
낙관적 업데이트가 작동하도록 하는 데 필요한 코드는 특히 "표준" 변형과 비교할 때 사소하지 않습니다.
결과를 속일 때 백엔드가 하는 일을 모방해야 합니다.
이는 부울을 뒤집거나 배열에 항목을 추가하는 것만큼 쉬울 수 있지만 실제로는 더 복잡해질 수도 있습니다.
  • 추가하려는 todo의 id가 필요한 경우 어디서 얻나요?
  • 현재 보고 있는 목록이 정렬된 경우 새 항목을 올바른 위치에 삽입할 수 있겠나요?
  • 그 사이에 다른 사용자가 다른 것을 추가했다면 어떻게 할까요? --
    • 다시 가져온 후 우리가 낙관적으로 추가한 항목이 위치를 전환할까요?
이러한 극단적인 경우는 변형이 진행 중인 동안 버튼을 비활성화하고 로딩 애니메이션을 표시하면 충분할 수 있는 일부 상황에서
UX를 악화시킬 수 있습니다.
항상 그렇듯이 올바른 작업에 적합한 도구를 선택하십시오.

일반적인 문제들

마지막으로 처음에는 명확하지 않을 수 있는, 변형을 다룰 때 알아두면 좋은 몇 가지 사항에 대해 알아보겠습니다.

awaited Promises

뮤테이션 콜백에서 반환된 프로미스는 React Query에서 await합니다.
즉, invalidateQueries는 Promise를 반환합니다.
 
관련 쿼리가 업데이트되는 동안 변형을 loading 상태로 유지하려면
콜백에서 invalidateQueries의 결과를 반환해야 합니다.
{
  // 🎉 will wait for query invalidation to finish
  onSuccess: () => {
    return queryClient.invalidateQueries(['posts', id, 'comments'])
  }
}
{
  // 🚀 fire and forget - will not wait
  onSuccess: () => {
    queryClient.invalidateQueries(['posts', id, 'comments'])
  }
}​

Mutate or MutateAsync

useMutation은 mutate 및 mutateAsync의 두 가지 기능을 제공합니다.
차이점은 무엇이며 언제 어느 것을 사용해야 합니까?
 
mutate는 아무 것도 반환하지 않는 반면
mutateAsync는 변형 결과가 포함된 Promise를 반환합니다.
 
따라서 변형 응답에 액세스해야 할 때 mutateAsync를 사용하고 싶을 수도 있지만
저는 거의 항상 mutate를 사용해야 한다고 주장합니다.
 
콜백을 통해 데이터 또는 오류에 계속 액세스할 수 있으며 오류 처리에 대해 걱정할 필요가 없습니다.
mutateAsync를 사용하면 Promise를 제어할 수 있으므로 수동으로 오류를 잡아야 합니다.
 
그렇지 않으면 unhandled promise rejection을 을 만나게 됩니다.
const onSubmit = () => {
  // ✅ accessing the response via onSuccess
  myMutation.mutate(someData, {
    onSuccess: (data) => history.push(data.url),
  })
}

const onSubmit = async () => {
  // 🚨 works, but is missing error handling
  const data = await myMutation.mutateAsync(someData)
  history.push(data.url)
}

const onSubmit = async () => {
  // 😕 this is okay, but look at the verbosity
  try {
    const data = await myMutation.mutateAsync(someData)
    history.push(data.url)
  } catch (error) {
    // do nothing
  }
}​
React Query는 내부적으로 오류를 포착(및 폐기)하기 때문에 mutate에서는 오류를 처리할 필요가 없습니다.
다음과 같이 구현됩니다: mutateAsync().catch(noop)😎
 
내가 mutateAsync가 우월하다는 것을 발견한 유일한 상황은 Promise가 정말로 필요한 경우입니다.
  • 이것은 여러 변형을 동시에 시작하고 모두 완료될 때까지 기다리려는 경우
  • 또는 콜백과 함께 콜백 지옥에 들어가는 종속 변형이 있는 경우에 필요할 수 있습니다.

변형은 변수 하나만 취합니다.

mutate의 마지막 인수는 옵션 객체이므로 useMutation은 현재 변수에 대해 하나의 인수만 사용할 수 있습니다.
이것은 확실히 제한 사항이지만 객체를 ​​사용하여 쉽게 해결할 수 있습니다.
// 🚨 this is invalid syntax and will NOT work
const mutation = useMutation((title, body) => updateTodo(title, body))
mutation.mutate('hello', 'world')

// ✅ use an object for multiple variables
const mutation = useMutation(({ title, body }) => updateTodo(title, body))
mutation.mutate({ title: 'hello', body: 'world' })

이렇게 결정한 이유는 this discussion 을 살펴보세요


어떤 콜백은 호출되지 않습니다.

useMutation 및 mutate 자체에 대한 콜백을 가질 수 있습니다.
useMutation의 콜백이 mutate의 콜백보다 먼저 발생한다는 것을 아는 것이 중요합니다.
또한 변형이 완료되기 전에 컴포넌트가 마운트 해제되면 mutate에 대한 콜백이 전혀 실행되지 않을 수 있습니다.
 
그렇기 때문에 콜백에서 관심사를 분리하는 것이 좋습니다.
 
  • useMutation 콜백에서 반드시 실행되어야 하는 로직과 관련된(예: 쿼리 무효화) 작업을 수행합니다.
  • mutate 콜백에서 리디렉션 또는 알림 표시와 같은 UI 관련 작업을 수행합니다.
    • 변형이 완료되기 전에 사용자가 현재 화면에서 멀어지면 의도적으로 실행되지 않습니다.
 
이 분리는 useMutation이 사용자 정의훅에서 오는 경우 특히 깔끔합니다.
이렇게 하면 UI 관련 작업이 여전히 UI 컴포넌트에 있는 동안 사용자 정의 훅에 쿼리 관련 논리가 유지되기 때문입니다.
 
이것은 또한 UI와 상호 작용하는 방법이 사례별로 다를 수 있기 때문에 사용자 정의 훅을 재사용할 수 있게 합니다.
하지만 무효화 논리는 항상 동일할 가능성이 높습니다.
const useUpdateTodo = () =>
  useMutation(updateTodo, {
    // ✅ always invalidate the todo list
    onSuccess: () => {
      queryClient.invalidateQueries(['todos', 'list'])
    },
  })

// in the component

const updateTodo = useUpdateTodo()
updateTodo.mutate(
  { title: 'newTitle' },
  // ✅ only redirect if we're still on the detail page
  // when the mutation finishes
  { onSuccess: () => history.push('/todos') }
)





 

 

 

반응형