이번 주제는 react query에서 제공하는 useMutation에 관해 다뤄보려고 한다.
내가 지금 진행중인 프로젝트에서는 react query를 이용하여 보다 효율적으로 서버 상태 관리를 하고 있는데, 보통 처음 스크린에 포커싱 될 때, react query를 이용하여 초기 데이터를 빠르게 불러오기 위하여 react query의 캐싱 기능을 이용한다.
보통 이렇게 스크린/컴포넌트 진입 시점에 데이터를 불러와야 할 경우에는 useQuery를 이용했었다.
const {
data: tempCharts,
error: chartsError,
isFetching: isFetchingCharts,
refetch,
} = useQuery({
queryKey: ['chartsV2'],
queryFn: getV2Chart,
staleTime: 3600000, // 1시간 동안 캐시 유지
select: data => data.data,
});
이번 프로젝트에서 사용한 코드의 일부이다.
인기차트의 데이터를 불러온 후, queryKey로 캐싱을 해두었고, queryFn은 axios를 이용해 api를 호출하여 response를 받도록 사전에 정의한 함수명이다.
받은 데이터의 기본 response는 data, message 키로 이루어져 있어 data에 있는 내용만 select하여 사용하였다.
사실, react query는 캐싱 외에도 data, error, isFetching, refetch 등 굉장히 사용하기 편하게 미리 상태값을 정의해준다.
이때 내가 사용한 기능은 refetch 정도였다. 새로고침을 진행할 시 fetch를 다시 한번 더 진행할 수 있도록 하기 위하여 사용했었다.
그러면 error 처리는..?
사실, 잘 안했다.
debug 모드에서는 error라는 표시가 빨간색으로 대문짝만하게 보여서 안고칠 수가 없어서 처리하려고 하지만, 실제 release 모드에서는 error가 나도 error 표시가 사용자에게 인식될 수 있는 UI로 보통은 표시되지 않았다.
그래서, error가 나더라도 기존 받으려고 했던 데이터가 저장되지 않으면 그 저장되지 않은 조건을 기반으로 또 다른 UI를 제공했던 것이 전부였다.
귀찮았기도 하고, 개발하는 동안에는 관련 에러를 보지 못했기에 필요성을 못느꼈기 때문이다.
error 처리 중요성을 깨닫는 계기
에러 불감증이던 내가 error 처리를 해줘야 하는 중요성을 깨달았던 계기가 있었다.
서버 응답 시간이 다른 api 요청에 비해 시간이 다소 걸리는 특정 api가 있었는데, 그 api를 통해 데이터를 받기까지 로딩 gif를 띄워주었다.
근데 서버에서 에러가 발생할 경우에도 데이터가 저장되지 않아 무한으로 로딩 gif가 뜨는 것이었다. 처음에는 서버가 그냥 시간이 좀 걸리는 거겟지라고 생각하였지만, 이렇게 허술한 에러 로직으로 관리하다보니 콘솔을 확인해보지 않는 이상 서버에 에러가 발생한 줄도 몰랐었다.
사용자에게는 error가 났음을 숨기는 게 좋은 UI가 아니라, error가 났으면 났다고 명확한 정보를 제공하는 것이 더 좋겠다라는 생각을 이때 처음으로 하게 되었다.
글의 주제는 왜 useMutation인가요?
사실 지금 글을 쓰고 있는 나에게 던지는 질문이다.
error에 관한 이야기를 하다보니 글의 흐름이 너무 다른 방향으로 가는 것 같긴 한데, useMutation을 쓰려고 하는 이유는 바로 react query에서 제공해주는 Error 상태를 이용해 분기 처리를 하기 위해서이다.
기존의 axios를 이용해 api를 요청하고, 응답을 받고 이에 대한 error가 발생할 경우 에러가 발생했다는 새로운 컴포넌트를 띄우기에는 상당히 로직 자체가 깔끔하지 않게 되는 문제가 있었다.
이때, 특정 조건 (예를 들어, 버튼을 누르거나, 특정 사용자의 행동 등)에서 트리거되는 api 요청일 경우 useMutation을 이용하면 저장되는 데이터, fetching 중인지, error가 발생했는지를 정말 간편하게 다룰 수 있다.
const {mutateAsync, isLoading} = useMutation({
mutationFn: async () => {
if (!title || !contents || !title.trim() || !contents.trim()) {
Toast.show({
type: 'selectedToast',
text1: '제목과 내용을 입력해주세요.',
position: 'bottom',
visibilityTime: 2000,
});
throw new Error('제목과 내용을 입력해주세요.'); // 오류 발생 시 예외 처리
}
return postPosts(contents, songIds, title);
},
onError: (error: Error) => {
Toast.show({
type: 'selectedToast',
text1: error.message || '잠시 후 다시 시도해주세요.',
position: 'bottom',
visibilityTime: 2000,
});
},
onSuccess: () => {
Toast.show({
type: 'selectedToast',
text1: '성공적으로 등록되었습니다.',
position: 'bottom',
visibilityTime: 2000,
});
navigation.goBack();
// 성공 시 추가 로직 (예: navigation.pop())
},
});
내가 처음 적용해본 useMutation을 이용한 코드이다.
useMutation에서 사용하는 api를 호출하는 함수는 mutationFn으로 정의해주어야 한다.
Error: Promise<any>' has no properties in common with type 'UseMutationOptions<unknown, Error, void, unknown>'
그렇지 않다면 위의 에러가 발생할 텐데, 만약 이 에러가 발생했다면 mutationFn으로 정의가 정확하게 되었는지 확인해주면 된다.
(참고한 링크: https://stackoverflow.com/questions/78076695/error-promiseany-has-no-properties-in-common-with-type-usemutationoptionsu)
mutationFn에서 바로 api 함수를 호출하는 것이 아닌, api 함수에서 필요한 props가 올바른 값이 들어갔는지를 검증 먼저 해주었다.
만약 조건에 부합하지 않는 경우는 api 함수 호출 대신, 사용자가 올바른 props를 입력할 수 있도록 안내 메세지를 보여주었다.
밑의 onError와 onSuccess를 이용하여 api 함수 호출에 성공했을 경우에는 성공적으로 등록되었다는 문구와 함께 이전 화면으로 돌아가는 로직을 구성하였다.
onError의 경우, 이전 화면으로 돌아가는 것 없이 어떤 에러가 발생했는지를 보여주었다.
error의 종류에 따른 처리 방법
error는 굉장히 다양한 원인으로 발생한다.
사용자가 잘못된 정보를 입력하여 이를 api로 호출하여 에러가 발생할 수도 있고, 아님 서버 상의 문제일 수도 있다.
나의 경우는 사용자가 잘못된 정보를 입력한 경우 이를 바로 api 함수 호출에 이용하기 보다는 한번 더 검증 과정을 거친 후, 만약 잘못된 입력을 시도했다면 다시 입력하게끔 처리하였다.
서버 문제의 경우에만 onError를 이용하여 toast 메세지를 띄워주는 것으로 따로 처리를 해주었다.
이렇게 프론트에서 에러가 발생할 경우를 한번 더 검증해주면 더 정확한 에러 원인을 탐색하는데 시간을 줄일 수 있을 것이다.
정리
useMutation은 단순히 효과적인 에러 로직 처리를 위하여 사용해보았지만, 나중에 기회가 된다면 react query의 전반적인 기능과 쓰임새에 대해 더 공부해봐야 겠다. 분명 에러 로직 처리 외에도 쓰는 이유가 많을 것이다. 다만 내 포스팅에서는 에러가 났을 경우 효과적으로 다루기 위해 useMutation을 이용하는 방법만을 다루었다. 이 점 참고해주면 좋을 것 같다.