서버의 데이터를 단지 읽어오기만 하는 query와는 다르게, mutation은 서버 데이터를 변형시킨다. 이런 차이로 인해 query를 요청할 때와는 hook을 사용하는 방식에 다소 다른 점이 존재하며, 만약 mutation의 결과를 화면에 반영해 보여줘야하는 상황이라면 mutation 결과에 맞춰 직접 client cache를 수정해주는 절차가 뒤따른다.
아래 코드는 SNS에서 게시글에 좋아요를 누를 때 호출하는 postLike api를 예시로 작성해 보았다.
//back-end
import { gql } from "apollo-server";
export default gql`
type mutationResponse {
ok: Boolean!
error: String
}
type Mutation {
postLike(id: Int!): mutationResponse!
}
`;
import React form 'react'
import { gql, useMutation } from "@apollo/client";
const POST_LIKE_MUTATION = gql`
mutation postLike($id: Int!) {
postLike(id: $id) {
ok
error
}
}
`;
//LikeComponent는 prop으로 postId를 받는다.
const LikeComponent = ({postId}) =>{
//mutation으로 데이터 업데이트가 완료되면 실행될 함수
//cache와 서버의 응답값인 result를 받아 실행한다.(변수명은 변경 가능하다.)
const updatePostLike = (cache, result) => {
const {
data: {
postLike: { ok },
},
} = result;
//만약 서버에서 성공했다는 응답이 오면 캐시를 직접 수정한다.
if(ok){
cache.modify({
id:`Post${postId}`
fields:{
isLiked:(prev)=>!prev,
likes: (prev)=>prev+1
}
})
}
}
//useMutation은 2개의 item이 담긴 배열을 반환하고, 첫번째 item은 mutation을 호출할 수 있는 함수,
//두번째 item은 useQuery을 사용했을 때와 유사한 목록이 담긴 object이다.
const [postLikeMutation,{loading}] = useMutation(POST_LIKE_MUTATION, {
//mutation의 데이터 업데이트를 완료하고 서버에서 응답이 도착했을 때 실행할 함수를 정의할 수 있다. 주로 여기서 cache를 업데이트한다.
update: updatePostLike,
});
//로딩중이면 "Loading..."문구를 출력하고 로딩중이 아니면 버튼을 노출한다.
return <div>
{loading ? "Loading..." : <button onClick={()=>postLikeMutation({variables:{id:postId}})}>Like</button>}
</div>
}
export default LikeComponent
react에서 apollo mutation을 사용하는 과정은 크게 4단계로 나뉘어 진다. 호출할 api와 그 응답값을 정의하는 document 선언 단계, useMutation을 사용해 실제 mutation을 선언할 함수와 각종 부가 데이터 및 함수를 받아오는 단계, 사용자 상호작용을 통해 mutation 함수를 실행해 호출하는 단계, 마지막으로 mutation 결과를 client cache에 반영하는 단계이다. useQuery를 실행하는 순간 즉시 api가 호출되어 data를 반환하는 query와는 다르게 mutation은 사용자가 원하는 시점에 호출할 수 있도록 실행함수를 반환한다.(lazyquery를 사용하면 query 호출도 시점을 조절할 수 있다.) 또한, query를 실행했을 때는 없었던 cache 보정 단계가 추가되었다. 이 중에서 mutation을 사용할 때 가장 중요하고 까다로운 부분이 바로 cache를 수정하는 파트이다.
apollo에서 client cache는 상태관리 도구이자 데이터베이스라고 생각하는 편이 편리하다고 이전에 언급한 적이 있었다. 만약 mutation을 실행하고 cache를 적절하게 수정해주지 않으면 어떤일이 벌어질까?
가장 먼저 생각해볼 수 있는 문제는, mutation 결과를 눈으로 확인할 수 없게 된다는 점이다. react에서 캐시를 수정하면 자동으로 리렌더링이 발생하여 변경된 캐시를 화면에 반영해준다. 반대로 말하면 cache의 수정 없이는 client가 어떤 데이터가 바뀌었는지 알 방법도 없고, 그렇다보니 적절한 최신 데이터를 화면에 다시 그려줄 수도 없는 것이다. 물론 화면에 보여지는게 문제라면 react state를 사용하면 되는 것 아니냐고 생각할 수 있다.
...other code
const [likeData,setLikeData] = useState({isLiked:false, likes:0})
const likePostUpdate =(cache,result) =>{
const {data:{postLike:{ok}}} = result
if(ok){
setLikeData({
isLiked:true,
likes:likeData.likes+1
})
}
}
...other code
그렇다 위 코드처럼 state를 활용해도 당장 내가 좋아요를 누른 사실을 화면에 반영해 보여줄 수 있다. 하지만 만약 다른 페이지로 이동했다가 다시 돌아왔을 때도 그 정보가 유지되고 있을까? 내가 좋아요를 누른 사실을 Profile 페이지에서도 확인할 수 있을까? 이런 문제를 해결하기 위해서는 likeData를 상태관리 도구를 사용해 전역으로 관리하거나 mutation을 실행할 때마다 매번 새로 데이터를 불러오는 refetch 함수를 실행할 수밖에 없다. 이러한 비효율을 해결하기 위해서 cache를 수정하는 작업이 필요한 것이다. 다음에는 캐시를 다루는 몇가지 케이스를 조금 더 정리해볼 예정이다.