[React Query] (4) useMutation()
🧐 서론
지난 포스팅까지, useQuery()를 쓰는 다양한 기법들과 더욱 확장된 기능을 위한 훅들을 알아보았다. (useQueries, useInfiniteQuery 등)
계속 언급했듯, useQuery()는 데이터 조회(HTTP 요청의 GET)를 담당하는 훅이다.
이번 포스팅에서는, useMutations() 훅을 통해 POST, PUT, DELETE를 어떻게 핸들링하는지 알아보도록 하겠다!
(공식문서는 위 요청을 Mutation으로 구현할 것을 권장한다. useQuery의 onSuccess, onError로 POST도 구현가능하나 가급적 지양)
* https://www.youtube.com/watch?v=NYCG1o38oEQ&list=PLC3y8-rFHvwjTELCrPrcZlo6blLBUspd2&index=21
🌺 useMutation() 이란?
useMutation() 훅은 React Query를 이용해서 서버의 데이터 변경작업을 요청할 때 사용한다.
일반적으로 mutation은 부가효과(side effect)를 일으키는 함수로, 여기서는 '서버에 부가효과를 일으키는 함수' 라는 의미를 내포한다.
useMutation()의 기본적인 문법은 아래와 같다.
import { useMutation } from "react-query";
const { data, isLoading, mutate } = useMutation(mutationFn, options);
useMutation() 훅은 인자로 1) mutationFn(mutate를 위한 패치함수, 필수), 2) options(옵션, 선택) 2가지를 받는다.
(useQuery() 처럼 key값이 따로 없음)
mutationFn 함수로 POST, PUT, DELETE 및 각 요청에 필요한 인자들이 포함된다.
또한, options로 설정하는 값들, 그리고 반환되는 프로퍼티들은 아래와 같은 값들이 있다. (모든 값은 공식문서 참고)
* Options
- onMutate: (variables) => Promise<Context> | void. mutation 전에 실행되는 함수로, 미리 렌더링 하고자할 때 유용하다.
이 함수가 반환하는 값을 아래 함수들의 context로 사용가능. - onSuccess: (data, variables, context?) => void. mutation이 성공하고 결과를 전달할 때 실행.
- onError: (error, variables, context?) => void. mutation이 실패했을 시 에러를 전달한다.
- onSettled: (data, error, variables, context?) => void. mutation의 성공/실패 여부와 상관없이 완료됬을 때 실행.
- 이외에도, mutationKey, retry, cacheTime 등 다양한 옵션 존재
* Returns(반환값)
- mutate: (variables, { onSuccess, onError, onSettled }) => void. mutation을 실행시키는 메서드로 가장 많이 쓰임.
variables 매개변수가 mutationFn으로 전달되며, 옵션의 메서드들은 위 Options 함수들과 동일하다.
* 단, 두 곳 모두에서 콜백을 실행하면, Options의 콜백 -> mutate 옵션의 콜백 순으로 실행된다. - mutateAsync: (variables, { onSuccess, onError, onSettled }) => Promise<Data>. mutate 결과를 Promise로 반환.
mutation 결과를 다루거나 비동기 연쇄로직이 필요할 경우 유용하나, catch()로 에러 핸들링을 직접 해야한다는 유의점이 있다. - 이외에도, status(idle, loading, error, success), data, isLoading, error, isError 등 다양한 값을 반환
🌺 useMutation() 코드 구현
예제 코드에서, useMutation() 에서 데이터를 추가하는 POST 요청을 어떻게 구현했는지 알아보겠다.
1) useAddSuperHeroMutation() Hooks 만들기
// src/hooks/apis/useSuperHeroQuery.js
import { useMutation, useQuery, useQueryClient } from "react-query";
import axios from "axios";
const fetchAddSuperHero = (hero) => {
return axios.post("http://localhost:4000/superheroes", hero)
}
export const useAddSuperHeroMutation = () => {
return useMutation(fetchAddSuperHero)
}
- mutationFn인 fetchAddSuperHero() 함수를 만든다. axios.post를 반환하며, body에 값으로 담을 hero를 인자로 받는다.
(이 hero 값은, 추후 useMutation() 의 mutate 메서드를 통해 전달) - useAddSuperHeroMutation() 훅을 제작한다. 이는, useMutation() 자체를 반환하며, 인자로 패치함수와 옵션 등을 설정한다.
2) 컴포넌트에서 활용하기
import { useState } from "react";
import { Link } from "react-router-dom";
import { useSuperHeroesQuery, useAddSuperHeroMutation } from "../hooks/apis/useSuperHeroesQuery";
export const RQSuperHeroesPage = () => {
const [newHero, setNewHero] = useState({ name: "", alterEgo: "" });
const { isLoading, data, isError, error, refetch } = useSuperHeroesQuery();
// 1) useAddSuperHeroMutation() Hooks 가져오기
const { mutate: addHero, isLoading2, isError2, error2 } = useAddSuperHeroMutation(newHero)
// 2) mutate() 함수 실행부
const handleClickAddButton = () => {
addHero(newHero)
setNewHero({ name: "", alterEgo: "" })
}
if (isLoading) return <h2>Loading...!!</h2>;
if (isError) return <h2>{error.message}</h2>;
return (
<>
<h2>React Query Super Heroes Page</h2>
<div>
<input
value={newHero.name}
onChange={(e) =>
setNewHero((prev) => ({ ...prev, name: e.target.value }))
}
placeholder="name"
/>
<input
value={newHero.alterEgo}
onChange={(e) =>
setNewHero((prev) => ({ ...prev, alterEgo: e.target.value }))
}
placeholder="alterEgo"
/>
<button onClick={handleClickAddButton}>Add Hero</button>
</div>
<button onClick={refetch}>refresh</button>
{data?.data.map((hero) => (
<div key={hero.name}>
<Link to={`/rq-super-hero/${hero.id}`}>{hero.name}</Link>
</div>
))}
</>
);
};
- useAddSuperHeroMutation() 훅을 가져온다.
useMutation() 이 반환하는 프로퍼티들을 사용할 수 있으며, 여기서 특히 mutate() 함수를 사용하는 것이 중요하다. - mutate() 함수를 addHero 라는 네이밍으로 가져왔다.
이는, mutation을 실행하는 함수이며, newHero를 인자로 전달하면 이는 useAddSuperHeroMutation() 훅에서 useMutation()의 첫 번째 인자인 mutationFn에 전달된다. - button을 클릭했을 때, handleClickAddButton 핸들러 함수가 실행된다. 이 때, 포함된 mutate() 함수 역시 실행될 것이다.
기본적으론 이렇게 구현될 수 있으며, 이를 queryClient를 활용해서 더 고도화시킬 수 있는 방법들을 아래에 계속해서 기술하겠다.
1. invalidateQueries() : Query Invalidation
queryClient 인스턴스의 invalidateQueries() 메서드는 queryKey 값들에 일치하는 useQuery()의 유효성을 제거시키는 함수다.
이는, mutation 이후 쿼리를 무효화시켜, 서버에서 데이터를 재호출하여 화면을 최신 상태로 갱신하기 위해 사용된다.
const fetchAddSuperHero = (hero) => {
return axios.post("http://localhost:4000/superheroes", hero)
}
export const useAddSuperHeroMutation = () => {
// 1) queryClient 인스턴스 생성
const queryClient = useQueryClient();
return useMutation(fetchAddSuperHero, {
// 2) onSuccess 콜백으로 실행
onSuccess: (data) => {
queryClient.invalidateQueries('super-heroes')
},
})
}
invalidaQueries는 queryKey 값 혹은 키들의 배열을 인자로 받으며, 여기에 해당하는 쿼리들을 무효화한다.
여기서, 무효화한 모든 쿼리가 재호출되는 것이 아니라, 화면 렌더링에 연관된 쿼리만 재호출되고 나머지는 stale 상태로만 전환되는 것이 React Query의 Invalidation 기능의 높은 효용성 중 하나이다.
2. setQueryData() : Mutation Response 활용을 통한 직접 업데이트
반드시 쿼리 전체를 갱신해야만 하는게 아니라면, 쿼리 Invalidation을 하지 않고도 정보를 추가 및 삭제할 수 있다.
바로, 우리가 mutate() 함수에서 받은 인자(variables)와 queryClient 인스턴스의 setQueryData() 메서드를 통해서다.
setQueryData() 는 queryKey와 콜백함수 2가지 매개변수를 받으며, 해당 키값에 대응되는 쿼리 데이터를 새롭개 정의하는 메서드다.
const fetchAddSuperHero = (hero) => {
return axios.post("http://localhost:4000/superheroes", hero)
}
export const useAddSuperHeroMutation = () => {
const queryClient = useQueryClient();
return useMutation(fetchAddSuperHero, {
onSuccess: (data) => {
queryClient.setQueryData(
'super-heroes',
(prevData) => ({ ...prevData, data: [...prevData.data, data.data] })
)
},
})
}
위처럼, onSuccess 시 인자로 받는 mutation 결과 data와 setQueryData() 의 콜백이 인자로 받는 현재 data를 통해 갱신할 데이터를 만든다.
setQueryData()는 onSuccess 콜백으로 실행되며, 첫 번째 인자로 queryKey, 두 번째 인자는 콜백함수로 반환하는 값으로 쿼리 데이터를 갱신한다. 컴포넌트 역시 변형된 data로 리렌더링되며, 이 때 서버통신이 없다는 것이 특징이다.
mutation 이후 컴포넌트 최신화를 위해 invalidateQueries와 setQueryData 둘 모두 사용 가능하나,
데이터가 적절하게 갱신(형상이나 정렬순서 등)되거나 혹은 최신상태를 유지하고자 하려면 Invalidation이 더 장점이 있다고 생각한다.
3. Optimistic Updates
Optimistic(낙관적) 업데이트는 React Query의 주요 기법 중 하나이다.
이는 mutation의 성공/실패 여부를 확인하기 전에 성공할 것이라는 낙관적인 가정하에, 사용자의 UI를 먼저 업데이트 시키는 기법이다.
const fetchAddSuperHero = (hero) => {
return axios.post("http://localhost:4000/superheroes", hero)
}
export const useAddSuperHeroMutation = () => {
const queryClient = useQueryClient();
return useMutation(fetchAddSuperHero, {
// 1) onMutate로 mutate 이전 설정
onMutate: async (newHero) => {
// a. 연산에 영향을 주지 않도록 쿼리를 우선 정지
await queryClient.cancelQueries('super-heroes')
// b. 이전 쿼리값을 가져온다(스냅샷)
const prevHeroes = queryClient.getQueryData('super-heroes')
// c. 캐시 데이터를 우선 수정하여 UI를 변경시킨다.
queryClient.setQueryData('super-heroes', (prevData) => {
return { ...prevData, data: [...prevData.data, { id: prevData?.data?.length, ...newHero }] }
})
// d. 에러 발생 시 롤백을 위해 스냅샷 데이터를 반환
return { prevHeroes }
},
// 2) onError로 에러 발생 시 롤백처리
onError: (error, payload, context) => {
// context의 위 스냅샷 데이터(prevHeroes)로 캐시 데이터를 원복
queryClient.setQueryData('super-heroes', context.prevHeroes)
},
// 3) onSettled(혹은 onSuccess)로 정보변경 완료 시 쿼리 갱신
onSettled: () => {
queryClient.invalidateQueries('super-heroes')
},
})
}
예제 코드처럼 1) onMutate 시 데이터 스냅샷 및 Optimistic Update, 2) onError 시 롤백처리, 3) onSettled 시 무효화로 쿼리 갱신 3단계로 이루어진다.
useMutation() 을 통해 POST 요청을 다루는 방법과, 이 이후에 변경된 서버 데이터를 어떻게 갱신하는지 다양한 방법을 알아보았다.
위에서 언급했듯이 직접 업데이트도 요청을 거치지 않는 나름의 장점은 있지만,
queryClient로 구현할 수 있는 다양한 기능들과 특히 Optimistic Updates 기법을 통한 빠른 UI 업데이트와 쿼리갱신 로직을 공식문서를 포함한 많은 레퍼런스에서 권장하고 있다.
마지막으로, React Query의 최신버전인 v4의 주요 변경점과 유의사항을 정리하며 시리즈를 마무리하려고 한다. (22년 7월 릴리즈)
📎 출처
- [공식] React Query 공식문서 : https://react-query-v3.tanstack.com/overview
- [강의] Codevolution 유튜브 강의 : https://www.youtube.com/watch?v=NYCG1o38oEQ&list=PLC3y8-rFHvwjTELCrPrcZlo6blLBUspd2&index=21
- [useMutation] DevKkiri 님의 블로그 : https://devkkiri.com/post/b3fe8ba3-46df-4cf0-b260-2c862628c0d9
- [useMutation] kimhyo_0218 님의 블로그 : https://velog.io/@kimhyo_0218/React-Query-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%BF%BC%EB%A6%AC-useMutation-%EA%B8%B0%EB%B3%B8-%ED%8E%B8
- [useMutation 번역 및 다양한 정보] itchallenger 님의 블로그 : https://itchallenger.tistory.com/587
- [Optimistic Updates] raverana96 님의 블로그 : https://velog.io/@raverana96/react-query-Optimistic-Update