현재 개발하고 있는 서비스는 state관리를 위해 Redux + Redux-saga + Redux-Toolkit(RTK) 을 사용하고 있었다. 이 조합을 사용하는데에 있어서 익숙해 지다보니 크게 불편함은 없었지만 최근에 몇가지 단점이 제기되었다.
서버로부터 데이터를 가져오는중 (GET/POST/DELETE등 요청을 했는데 아직 response가 도착하기 전일 경우) 이라는 것을 UI에 Loader로 보여줘야 하기 때문에 필요했다.
Error 가 났으면 노티를 준다던지 page를 이동한다던지 하는 로직을 구현하기 위해 필요했다.
이 포스트에서는 React-Query 를 도입하면서 겪었던 이슈들을 정리해보려고 한다.
React-Query란 공식 문서에 의하면 'Fetch, cache and update data in your React and React Native applications all without touching any "global state".' 이다.
즉, 'React Application에서 서버 상태를 가져오고, 캐시하고, 동기화하고, 업데이트하는 작업을 수월하게 해주는 라이브러리' 이다.
React-Query를 사용해 서버 상태를 가져오기 위해서는 useQuery를 사용하면 된다.
만약 서버의 data를 수정하고자 한다면 useMutation 을 사용한다.
요약하면 R ( Read ) 은 useQuery , CUD ( Create, Update, Delete )는 useMutation을 사용하면 된다.
자세한 사용법은 React-Query 공식 문서의 가이드 문서를 통해 확인할 수 있다. ( React-Query 공식 가이드 문서 )
이 포스트는 React-Query 사용법을 소개하는 포스트는 아니고 실제 적용하면서 겪었던 것을 기록하고 React-Query를 도입하고자 하는 또 다른 누군가에게 참고가 되었으면 해서 적게 되었다.
useQuery의 return 값으로 API의 fetch 상태를 얻어올 수 있다.
const { isLoading, error, data, isFetching } = useQuery("repoData", () =>
fetch(
"https://api.github.com/repos/tannerlinsley/react-query"
).then((res) => res.json())
);
fetch를 아직 완료하지 않은 상태일 때, UI에 Loader를 그려주고자 isLoading을 사용할 수 있을 것이다.
❗️하지만 주의해야할 점이 있다❗️
isLoading === true의 조건은 '캐시된 data가 없고 isFetching === true 일 때' 이다.
즉, refetch 가 된 query 일 경우 isLoading 이 false 라는 얘기이다.
만약 의도적으로 refetch 시에도 Loader를 그려주고 싶다면 isLoading이 아닌 isFetching을 사용해야 한다.
useQuery를 사용하면 server의 reponse를 useQuery의 return 값인 data에서 받아올 수 있다.
const fetchTodoList = () => {
const { data } = await axios.get(
"https://..."
);
return data;
}
const { data } = useQuery("todos", fetchTodoList);
하지만 useMutation의 경우, CUD에 해당하는 API 콜 후, 서버의 response를 data에서 받아올 수 없다. ( 성공/실패 여부는 당연히 알 수 있다! )
React-Query 공식 문서에서도 useMutation의 return 값인 data의 타입은 'undefined | unknown' 으로 명시되어 있다. 만약 useMutation 사용시 response 가 필요한 경우라면 mutateAsync 로 얻어올 수 있다.
mutateAsync는 Promise를 return 하게 되고 Promise result로 response를 가져올 수 있다.
const postTodo = (todo) => {
axios.post('/api/data', { todo })
}
const createTodo = useMutation(postTodo);
createTodo.mutateAsync(todo).then((data) => {
console.log(data);
// console로 찍은 data가 서버의 response 값입니다.
});
React-Query의 QueryClient 는 캐시를 이용해 여러 기능을 사용할 수 있게 해준다.
그 중 하나인 queryClient.isMutating method는 현재 mutating 중 (진행 중..) 인 mutation의 수를 반환한다.
import { QueryClient } from 'react-query'
const queryClient = new QueryClient();
const isMutating = queryClient.isMutating();
useIsMutating 이라는 hook 역시 현재 mutating 중인 mutation의 수를 반환한다.
import { useIsFetching } from 'react-query'
// How many queries are fetching?
const isFetching = useIsFetching()
얼핏 보면 두 가지 모두 같은 기능을 하는 것 처럼 보인다. 하지만 차이점이 있기 때문에 적절하게 사용하는 것이 필요하다.
🟠 useIsMutating 같은 경우에는 component에서 값의 업데이트를 알 수 있다. *
TodoList 를 관리하는 페이지가 있다고 해보자.
이 TodoList에 새로운 Todo를 추가하기 위해서는 'Todo 추가하기'라는 모달을 통해 추가하면 된다.
( 임의로 만든 매우 이상한 모달이지만 어쨌든 이 모달로 예를 들겠다.😅)
추가 할 일 입력 후 '추가하기' 버튼을 누르고 POST 요청이 성공하면 모달이 unmount 되고 TodoList에서는 방금 추가한 할 일이 추가되어 있어야 한다.
createTodo.mutate(todo, {
onSuccess: () => closeModal(); // 모달을 unmount 하는 함수
onSettled: () => queryClient.refetchQueries('todos') // 완료 후 todoList refetch
});
TodoList의 코드 내부에는 useIsMutating 을 사용해 POST 요청이 mutating 중인지 확인하고 Loader를 돌리는 로직이 존재한다고 가정한다. 그런데 위와 같이 로직을 짜면 무한으로 Loader가 보이는 현상이 발생할 수 있다.
❓ 그 이유는?
이런 실수를 방지하기 위해서는 onSuccess/onError/onSettled 내부에 unmount 하는 로직은 넣지 않는 것이 좋을 것 같다.
반면 queryClient.isMutating을 사용하면 바뀐 mutating값을 조회할 수 있다. 하지만 useIsMutating과 다르게 업데이트 감지는 안되기 때문에 주의해야 한다.
React-Query는 서버 상태를 관리하는데에 유용한 라이브러리이다 . 하지만 서버 상태가 아닌 global state 를 관리하기 위해서는 다른 라이브러리를 함께 사용하는 것도 좋을 것 같다.
나 역시 global state 를 관리할 수 있는 다른 라이브러리를 둘러보다가 constate를 발견하게 되었다. 실제로 도입하지는 않았지만 공부했던 내용에 대해서 다음 포스트에 정리해보도록 하겠다.
글 잘읽었습니다!
궁금한점이 있는데요!
try catch로 CUD 작업하는거랑
useMutation으로 작업하는거랑 어떤 차이가 있는건가요 ??