회사에서 운영하던 서비스는 Mobx를 기반으로 상태관리를 해왔는데요, 이번 스프린트 부터는 react-query를 사용해보려고 합니다. 여러 라이브러리 중에 react-query를 선택한 이유는 상태관리를 최소화하고 캐쉬를 이용해서 사용자경험도 최적화해보고 싶습니다.
기존 상태관리는 데이터를 요청한 후에, 뷰에 걸맞는 형식으로 데이터를 전환하고 전역상태로 정해주었습니다.
//ex - sudo
function Test(){
const convertedData = getConvertedData()
//대략 이 페이지에서 하는 일
...
await axios().then((res)=>{
const convertedData = convertResponse(res.data)
setConvertedData(convertedData)
})
...
return <div>{convertedData}</div>
}
// + 상태관리를 담당하는 Controller(또는 Store)와 Model에 관한 코드가 더 요구됨...
이 상태는 뷰를 한번 그리는데에만 사용되기도 하고, 수시로 변경되는 자료가 아니기 때문에 캐시를 사용하면 좋겠다.... 라는 생각이 자주 들었습니다.
실제로 지금 서비스의 아쉬운 점이 캐싱이 잘 되지 않아 loading의 노출이 빈번해서 유저경험이 좋지 못합니다. 또 모든 응답값을 상태로 저장하느라 코드가 더블x2이 되어 생산성이 좋지않습니다.
//ex - sudo
function Test(){
const {data} = useGetConvertedData()
const convertedData = convertedData(data)
return <div>{convertedData}</div>
}
대충 쓴 코드지만, react-query를 사용하면 훨씬 코드가 간결해 보기 좋아집니다.
새로운 마음으로.... :)
타입스크립트를 더 유용하게 사용할 방법을 생각해봤습니다.
현 백엔드 팀에서는 swagger로 api문서를 제공해주고 있습니다.
swagger의 타입스키마를 그대로 클라이언트에서 사용할 수 있다면 정확성을 높일 수 있고, 개발하기도 너무 편할 것 같습니다.
처음에는 OpenAPI 라이브러리를 사용해서 타입을 받았습니다.
오... 그동안 힘들게 써왔던 타입들이 한 파일에 모두 저장이 되어서, 찾아서 타입을 전해주기만 하면 되네요.... (그동안의 시간들 i o i)
그러나 결국에는 OpenAPI가 아닌 Orval을 사용하게 되었습니다.
이유는 다음!
넵, 위에서 말씀드린 대로 OpenAPI 말고, Orval을 선택한 이유는 react-query generator를 지원한다는 점 때문입니다.
Orval docs에 들어가면 아주 한눈에 서비스의 개요를 설명해 줍니다.
https://orval.dev/
오.. 저 대신 react-query문을 작성해준다니요.
직접 작성했을 때에는 react-query에 대한 스터디도 많이 필요했고 키 관리 등 고민되는 부분이 현재로서는 90퍼센트는 해결된 것처럼 보입니다.
거기다 꽤 까다로워 보이는 타입들도 많아서 직접 작성하려했다면 시간이 꽤나 걸렸을 것 같습니다.
//example - useQuery
const useShowPetById = <
TData = Awaited<ReturnType<typeof showPetById>>,
TError = AxiosError<Error>
>(
petId: string,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof showPetById>>,
TError,
TData
>;
axios?: AxiosRequestConfig;
}
): UseQueryResult<TData, TError> & { queryKey: QueryKey } => {
const { query: queryOptions, axios: axiosOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getShowPetByIdQueryKey(petId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof showPetById>>> = ({
signal,
}) => showPetById(petId, { signal, ...axiosOptions });
const query = useQuery<
Awaited<ReturnType<typeof showPetById>>,
TError,
TData
>(queryKey, queryFn, { enabled: !!petId, ...queryOptions }) as UseQueryResult<
TData,
TError
> & { queryKey: QueryKey };
query.queryKey = queryKey;
return query;
};
[참고] https://orval.dev/guides/custom-axios
orval에서 만들어준 인스턴스의 base url과 실제 api url간의 차이가 있어 커스텀 인스턴스를 생성했습니다. ((baseUrl + /caseService) 뒤에 추가로 붙는 주소가 인식되지 않은 문제가 있었어요.)
orval은 따로 수정하지 않는다면 기본적으로 axios로 데이터를 요청합니다.
미리 쿼리문을 만들어주기 때문에 axios나 fetch나 무얼 써도 크게 상관은 없다만 기본적인 fetch를 선호한다면 커스텀 인스턴스에서 fetch를 사용할 수도 있습니다.
저 역시 fetch를 사용하려고 했으나 기존에 사용하던 axios에서 interceptor 기능을 같이 누리기 위해서 axios를 사용했습니다.
우선 일이 많이 줄었습니다.
수정할것도 있습니다.
번거롭지만 좋은 점도 있습니다
api가 업데이트 될때마다 orval을 재 실행해야하는 번거러움도 있습니다.
하지만 그렇게 번거롭지도 않고 오히려 기존과의 변경점을 깃으로 확인할 수 있어 서버개발자에게 전달을 받지 못했더라도 놓치지 않고 빠르게 변경을 파악할 수 있는 것 같네요.
한마디로 생산성이 개선, api 파악이 쉬움, 서비스 정확성이 올라감
tanstack query + typeScript 조합이라면, 특히 typeScript라면 이런 제너레이터 사용을 매우 추천합니다.
tanstack query 제너레이트까지 필요없다면
더 많이 알려진 OpenApi도 타입 제너레이트까지 가능하기 때문에
비교해보는 것도 좋을 것 같습니다.