fetch든, axios든.
물론 커스텀 훅이나 interceptor와 같은 개념을 활용해서 재사용을 높이기도 하지만?
직접 react-query를 써보면서 느낀 점을 정리해본다.
컴포넌트가 늘어날 수록 state는 너무 많아진다.
Redux를 쓰자니 너무 초기 세팅이 귀찮기도 하고.
무엇보다 local side, server side의 State를 각기 나눠 관리하다보면 헷갈리기 마련이다.
분명히 다른 분야의 상태인데, 이걸 개발자가 커스텀해서 꼭 관리를 해야되는 걸까?
const [data,setData] = useState(undefined)
const [isLoading,setIsLoading] = useState(false)
const [isError,setIsError] = useState(new Error())
이를테면 어떠한 api를 호출한다 치자.
통신은 비동기 방식으로 동작하게 되는데, 경우에 따라 로딩 중인지 아닌지에 대한 사실을 알아야하거나 어떤 에러가 발생했는지 따로 관리하여 View에 보여줄 필요가 있다.
Promise.then()을 통해 각각의 상태를 관리하곤 했었는데 매번 통신마다 위와 같은 처리를 하는 건 솔직히 부담된다.
그런데 react-query는 이것을 알아서 관리해주더라.
const {isLoading,isError,isFetching,data} = useQuery(queryKey,fetchFn)
// 로딩 상태 : isLoading
// 에러 여부 : isError
// 페칭 여부 : isFetching
// 출력 결과 : data
queryKey가 해당 query를 의미하는 고유한 키로서 활용된다.
만약 값이 fresh하다면 캐싱된 데이터를 queryKey를 통해 받아오게 된다.
fetchFn은 말 그대로 통신 함수를 넣으면 된다.
queryKey와 fetchFn은 required option이며, 추가로 유용한 것 중엔 select가 있다.
서버 개발자분들이 상황에 따라 맞는 api를 만들어주시는 경우도 있겠지만 일단 데이터를 받고 프론트 측에서 가공해버리는 방법이 나을 때도 있다.
보통은 이를테면, response.data를 꺼내다가 의도된 구조를 위해 이것저것 배열 메서드를 쓰게 되는데 이 또한 은근한 시간 소모가 많다.
useQuery(queryKey,fetchfn,{select:data=>data.map(item=>item.id)})
예를 들어서 뭉텅이로 들어오는 배열 데이터들에서 각각의 id만 가져와야한다고 한다.
axios(url)
.then(res => res.data.map(item=>item.id))
대충 생각하면 이런 방식이 떠오르는데, 이런 것을 useQuery는 select 옵션으로 미리 데이터를 가공해줄 수 있다.
꼭 가공에만 국한되지 않는다. 개발자가 원하는 데이터 '1'개만 뽑아올 수도 있는거고, 조건절에 따라 원하는 값들만 추출할 수도 있겠다.
결론적으로는 똑같은 동작이지 않냐? 라고 반문할 수도 있지만, 훅에서 인자로만 넘겨서 가능케한다는 부분을 통해 여러가지 함수를 만들고 어딘가 보관해두고, 꺼내다가 쓸 수만 있게끔 할 수 있으니 모듈화를 하는 데에서도 이점을 가진다고 느낀다.
Client, Server의 State가 다르게 관리되어야한다는 관점처럼, View에는 View와 관련된 것만 두고 싶다는 관심사의 분리 형태에도 react-query는 강점을 가진다고 생각한다.
- 유저가 다른 작업을 하다가 윈도우로 돌아오면 다시 페칭
- 네트워크가 끊겼다가 재연결되면 다시 페칭
- 개발자가 명시한 조건에 부합하면 다시 페칭
등등....
데이터의 최신화는 중요하다. 만약 어떠한 트랜잭션이 진행되는 중간에 무언가 데이터가 변경된 사항이 있다면? 물론 DB야 ACID 원칙에 따라가지만, 찰나에 클라이언트 사이드에서 다루는 값이 최신화되지 않은 경우가 분명히 있을테다.
그러한 환경을 Refetch 규칙으로 방어(?)할 수 있는 것 같다.
실제로 로그를 보면 윈도우를 왔다갔다하면 알아서 다시 페칭해오는 것을 관측할 수 있었다.
useQuery(queryKey,fetchFn)
useMutation((args)=> axios(url,args))
useQuery 훅은 R에 해당하며, useMutation은 C,U,D에 모두 해당된다.
Query는 곧 질문. 즉, '무슨 데이터가 있나요?' 하고 질의하는 것이자, 조회라고도 할 수 있다.
Mutate를 직역하면 '변화하다'. 그러니까, 그냥 post, put, delete 등의 메서드에 해당하는구나~ 하며 이해하면 된다.
RESTful 한 통신 방식에도 기여한다고 생각이 든다.
axios.interceptors.response.use(
function (response) {
console.log(response);
// 여기서 response를 가공할 수 있다.
return response.data.data;
},
function (error) {
errorController(error);
}
}
axios interceptor는 request나 response를 처리되기 직전에 가로채어 필요한 로직을 끼워넣을 수 있다.
이를테면 request에 토큰을 담도록 만들어서 매번 호출할 때 토큰을 넣어야하는 귀찮음을 방지한다던가, 혹은 위의 사례처럼 응답을 분해할 수도 있다.
response.data에 대해 가공할 수도 있고, 이것은 react-query에서 select가 가능한 동작에 해당한다.
axios Instance를 따로 정의했다면 굳이 select를 쓸 필요가 있는가? 하는 생각이 든다.
그러나 받아오는 응답의 구조는 항상 달라서 분해/가공 로직은 정형적이기 힘들다.
경우에 따라 다른 인스턴스를 정의해줄 순 있겠으나, 오히려 상황에 맞게 select에 대한 가공 메서드를 그 때 그 때 짜는 게 개발 속도의 부분이나, 메모리 관리에 대한 부분에 이점을 가질 수 있지 않나? 싶기도 하다.