
tanstack-query를 서버 상태 관리 라이브러리로 이해하고 있습니다. API 요청을 통해 서버에서 데이터를 가져오는 라이브러리 말이죠. 그래서 자연스럽게 useQuery 훅으로 가져온 data를 onSettled 옵션을 통해 로컬 상태에 저장하고 있었습니다. 그런데 onSettled 옵션이 useQuery 훅에 없는 것 아니겠어요? 바로 공식 문서를 살펴보니 해당 옵션이 삭제됐다고 하더라구요.
Callbacks on useQuery (and QueryObserver) have been removed
onSuccess, onError and onSettled have been removed from Queries. They haven't been touched for Mutations.
https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5/#callbacks-on-usequery-and-queryobserver-have-been-removed
그럼 서버에서 가져온 data를 어떻게 사용하지? 의문이 들어 구글링을 시작했습니다. 검색을 통해 제가 지금까지 사용한 방법이 안티 패턴이라는 것을 알게 되었죠.
자료를 찾아가며 제가 이해한 내용을 정리해보겠습니다.
tanstack-query는 서버 상태를 굉장히 보수적으로 관리한다고 느꼈습니다. 서버에서 데이터를 가져온 순간부터 해당 데이터는 낡은 데이터로 취급하기 때문입니다. 이런 낡은 데이터를 로컬 상태에 저장한다는 것은 사용자에게 낡은 데이터로 만든 페이지를 보여준다는 의미인 것이죠. 지금까지 제가 서버에서 가져온 데이터를 로컬 상태에 저장한 것은 이와 같은 문제였던 것이었습니다.
로컬 상태에는 서버에서 가져온 데이터가 아니라 정말 로컬에서만 사용하는 상태, 예를들면 테마 상태나 사이드 바 표시 상태 등을 사용해야 합니다. 이외에 서버 상태들은 로컬 상태에서 관리하는 것이 권장되지 않습니다.
좋아요. 바람직한 상태 관리 방식을 이해하긴 했는데 어떻게 적용하라는 거죠? 서버 데이터를 로컬 상태에 저장하지 않고 사용하는 방법을 시도했습니다.
제가 구현하고 싶은 내용은 아래와 같았습니다.
제가 사용하는 API는 이미 구조가 잡힌 상용 API였습니다. 따라서 API 요청 query를 다르게 줄 수 없고 받는 값의 구조가 동일했습니다. 따라서 useQuery 훅을 바로 사용할 때 select를 사용해서 가져온 데이터를 가공할 수 없었습니다.
// 요청 주소 `https://ohlcv-api.nomadcoders.workers.dev?coinId=${coinId}`
const { data, isLoading } = useQuery<IChartData[] | IErrorData>({
queryKey: ["price", coinId],
queryFn: () => fetchCoinHistory(coinId),
});
제가 선택한 방법은 서버 데이터 fetch가 완료되었을 때 데이터를 가공하는 것이었습니다.
const [range, setRange] = useState<number>(7);
const { coinId } = useOutletContext<{ coinId: string }>();
const { data, isLoading } = useQuery<IChartData[] | IErrorData>({
queryKey: ["price", coinId],
queryFn: () => fetchCoinHistory(coinId),
});
// 사용자의 선택에 따라 범위 변경
const rangeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const rangeValue = event.currentTarget.value;
if (rangeValue === "ONE") {
setRange(7);
} else if (rangeValue === "TWO") {
setRange(14);
}
};
// useQuery를 통해 가져온 데이터 가공
const rangedDiff = useMemo(() => {
if (!data || "error" in data) return null;
const startClose = parseFloat(data[data.length - 2 - range].close ?? "0");
const endClose = parseFloat(data[data.length - 2].close ?? "0");
if (!startClose || !endClose) return null;
const closePercent = ((endClose - startClose) / startClose) * 100;
const startVolume = parseFloat(data[data.length - 2 - range].volume ?? "0");
const endVolume = parseFloat(data[data.length - 2].volume ?? "0");
if (!startVolume || !endVolume) return null;
const volumePercent = ((endVolume - startVolume) / startVolume) * 100;
const dateStart = getDateInfo(
new Date(data[data.length - 2 - range].time_close * 1000)
);
const dateEnd = getDateInfo(
new Date(data[data.length - 2].time_close * 1000)
);
return {
close: {
start: startClose,
end: endClose,
value: closePercent.toFixed(2),
isPositive: closePercent > 0,
},
volume: {
start: startVolume,
end: endVolume,
value: volumePercent.toFixed(2),
isPositive: volumePercent > 0,
},
date: {
start: dateStart,
end: dateEnd,
},
};
}, [data, range]);
위와 같은 방식으로 데이터 가공을 구현했습니다. 하지만 정말 복잡하고 지저분한 코드가 되었습니다. 깔끔하게 다듬는 방법을 고민해보다가 커스텀 훅으로 빼면 되지 않을까 생각이 들었지만 사용자 선택에 따라 API 요청이 반복되지 않을까 걱정이 되었습니다. 반복적으로 API 요청을 한다면 굳이 tanstack-query를 사용할 필요가 없을 것 같았습니다.
정확한 이해를 위해 바로 커스텀 훅으로 코드를 바꿔봤습니다.
// useCoinPrice.ts
export const useCoinPrice = (coinId: string, range: number) => {
const { data, isLoading, error } = useQuery<IChartData[] | IErrorData>({
queryKey: ["price", coinId],
queryFn: () => fetchCoinHistory(coinId),
});
if (!data || "error" in data) {
return { isLoading, error, start: null, end: null };
}
const endIndex = data.length - 2;
const startIndex = endIndex - range;
const start = startIndex < 0 ? null : data[startIndex];
const end = startIndex < 0 ? null : data[endIndex];
return { isLoading, error, start, end };
};
// Price.tsx
const { isLoading, start, end, error } = useCoinPrice(coinId, range);
위처럼 커스텀 훅으로 코드를 정리하니 한 눈에 내용을 볼 수 있었습니다. 또한 API 요청을 확인해보니 사용자 선택이 달라지더라도 추가 API 요청이 발생하지 않았습니다. 이는 query key가 동일하기 때문이었습니다. 해당 query key에 해당하는 data를 가공하기 때문에 추가 요청 없이 사용자 선택이 반영될 수 있었습니다.
프로젝트에 tanstack-query를 적용해보면서 서버 상태와 로컬 상태에 대한 개념을 다시 한 번 정리할 수 있었습니다. 또한, useQuery에서 추가 API 요청이 발생하는 경우를 제대로 이해할 수 있었습니다.
서버 데이터를 로컬 상태에 저장하는 안티 패턴을 만들지 않고 상태 관리를 적절히 할 수 있도록 공부가 필요할 것 같습니다.
https://www.codingmax.net/courses/ko-react-query/section03/lec0034