tanstack query(구 react query)에 대한 내용은 알고 있었지만, 제대로 공부해본 적은 없기에 한번 글로 남기면서 숙지해보고자 한다.
내가 알고 있는 tanstack query는 API 요청해서 데이터를 불러올 때, 여러번 같은 요청을 보낼 수 있는 것을 캐싱 작업을 통해서 한번 요청을 보내서 처리하는 것으로 어렴풋이만 알고 있었지만, 한번 개념을 다시 공부해보자.
TanStack Query는 웹 애플리케이션에서 서버 상태를 효율적으로 관리하기 위한 강력한 비동기 상태 관리 라이브러리다.
이전에는 React Query로 알려졌으나, 기능이 확장되어 React뿐만 아니라 Vue, Svelte 등 다양한 프레임워크에서도 사용할 수 있게 되면서 TanStack Query로 이름이 변경되었다.
서버 상태 관리 최적화
TanStack Query는 서버 데이터의 패칭, 캐싱, 동기화 및 업데이트를 쉽게 처리할 수 있게 해준다.
확장성
원래 React Query로 시작했지만, v4부터 Vue, Solid, Svelte 등 다양한 프레임워크에서 사용할 수 있도록 확장되어 TanStack Query로 이름이 변경되었다.
복잡성 감소
기존 상태 관리 라이브러리들은 비동기 데이터 처리 시 코드의 복잡도가 크게 증가했지만, TanStack Query는 이 문제를 해결한다.
서버 상태 특화
Redux, MobX, Recoil 등은 클라이언트 상태 관리에 적합하지만, 서버 상태 관리에는 한계가 있었다. TanStack Query는 서버 상태 관리에 특화되어 있다.
자동 캐싱 및 동기화
TanStack Query는 서버 데이터를 자동으로 캐싱하고 여러 컴포넌트에서 동일한 데이터를 쉽게 공유할 수 있게 한다.
선언적이고 자동화된 데이터 관리
데이터를 어디서 가져오고 얼마동안 최신 상태를 유지할 것인지만 정의하면, TanStack Query가 자동으로 처리한다.
내장된 최적화
중복 요청 방지, 백그라운드 업데이트, 오류 처리 등 다양한 최적화 기능을 내장하고 있다.
간편한 사용
useQuery 훅을 통해 데이터 패칭, 로딩 상태, 에러 처리 등을 쉽게 관리할 수 있다.
정말 간단한 tanstakc query의 주요개념만 다뤄보고자 한다.
useQuery는 서버로부터 데이터를 가져오는 데 사용되는 핵심 훅이다.
const { data, isLoading, error } = useQuery(['users'], fetchUsers);
이걸 쓰면 로딩 상태, 에러 관리, 캐싱까지 자동으로 해결된다. 굳이 useEffect + fetch를 조합해서 관리할 필요가 없다.
useMutation은 서버 데이터를 추가, 수정, 삭제할 때 사용한다.
const mutation = useMutation(newUser => axios.post('/api/users', newUser), {
onSuccess: () => {
queryClient.invalidateQueries(['users']);
},
});
이렇게 하면 데이터를 추가한 뒤 자동으로 최신 데이터가 반영된다. 이전처럼 useState로 직접 데이터 리스트를 관리할 필요가 없다.
TanStack Query는 데이터를 자동으로 캐싱하고, 필요할 때만 백그라운드에서 새로 동기화한다.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60000, // 1분
cacheTime: 300000, // 5분
},
},
});
이게 왜 중요하냐면
staleTime이 길면, 불필요한 API 요청을 줄일 수 있다.
cacheTime이 길면, 캐싱된 데이터를 오래 유지하면서 성능 최적화가 가능하다.
덕분에 매번 새로 요청할 필요 없이, 필요한 시점에만 데이터를 최신화할 수 있다.
실제 내가 프로젝트에서 사용하는 코드를 tanstack query로 변경해보았다.
위치 기반 약국 정보를 가져오는 기능을 개발하면서 기존 API 요청 방식의 한계를 느꼈다.
로딩 상태 관리, 에러 핸들링, 캐싱 부족 이 모든 걸 직접 처리해야 하는 불편함이 있었기 때문이다.
사실 속도 차이가 크진 않지만, TanStack Query를 배운 김에 한 번 적용해 보았고, 결과적으로 더 깔끔하고 유지보수가 쉬운 코드로 리팩토링할 수 있었다.
const handleSearch = useCallback(async (lat: number, lng: number) => {
setLoading(true);
try {
const response = await fetch(`${API_URLS.PHARMACY}?lat=${lat}&lng=${lng}`);
const data = await response.json();
if (Array.isArray(data)) {
setPharmacies(data);
} else {
console.error('Invalid data format:', data);
setPharmacies([]);
}
} catch {
setError(ERROR_MESSAGES.PHARMACY_DATA_ERROR);
}
setLoading(false);
}, [setLoading, setPharmacies, setError]);
로딩/에러 상태를 직접 관리해야 함
setLoading(true/false), setError(...) 같은 코드가 반복됨 → 코드가 지저분해짐
데이터 패칭 로직이 컴포넌트 내부에 존재
같은 API 요청을 여러 곳에서 사용하려면 코드 복사 필요 → 재사용성이 떨어짐
캐싱이 없어 같은 요청을 중복 실행
같은 데이터를 다시 요청해도 새로운 API 호출 발생 → 성능 저하
네트워크 장애 시 자동 재요청이 없음
네트워크 오류가 발생하면 사용자가 직접 새로고침해야 함 → 불편함
useQuery를 사용해서 데이터 패칭 로직을 분리하고, 자동 캐싱, 에러 핸들링, 로딩 상태 관리까지 개선했다.
export const usePharmacies = (lat?: number, lng?: number) => {
return useQuery({
queryKey: ['pharmacies', lat, lng], // 동일한 요청을 캐싱하여 API 요청 최소화
queryFn: () => (lat && lng ? fetchPharmacies(lat, lng) : Promise.resolve([])),
enabled: !!lat && !!lng, // lat, lng 값이 있을 때만 실행 (불필요한 API 호출 방지)
staleTime: 24 * 60 * 60 * 1000, // 24시간 동안 기존 데이터를 유지하고, 필요할 때만 새 요청
gcTime: 24 * 60 * 60 * 1000, // 24시간 이후 캐시 데이터를 삭제하여 메모리 최적화
});
};
데이터 패칭 로직이 컴포넌트에서 분리됨 → 재사용성 증가
자동으로 상태 관리 (isLoading, error, data 제공됨)
setLoading(true/false) 같은 코드가 필요 없음
쿼리 키(queryKey) 기반 캐싱 → 중복 요청 방지
같은 데이터 요청 시 기존 데이터를 바로 사용
조건부 실행 (enabled 옵션 적용)
lat, lng 값이 없을 때는 API 요청이 실행되지 않음
캐싱 최적화 (staleTime, gcTime)
staleTime: 24시간 → 24시간 동안 기존 데이터를 유지
gcTime: 24시간 → 24시간이 지나면 캐시 데이터 삭제 (메모리 최적화)
기존 방식 (useCallback + fetch ) | 개선 방식 (useQuery ) |
---|---|
setLoading(true/false) , setError() 직접 관리 | isLoading , error 자동 제공 |
데이터 요청 코드가 컴포넌트 내부에 존재 | 훅으로 분리 → 재사용 가능 |
같은 요청이 들어와도 매번 새로운 API 호출 | queryKey 기반 캐싱 → 중복 요청 방지 |
useEffect 에서 조건문을 넣어야 함 (if(lat && lng) ) | enabled 옵션으로 불필요한 API 호출 방지 |
네트워크 장애 발생 시 새로고침 필요 | 자동 재시도 기능 (retry 지원 가능) |
사실 tanstack query를 공부하고 나서도 서버 상태관리와 서버 캐싱에 대한 개념이 많이 헷갈렸던 것 같다.
nextjs에서 서버캐싱을 사용하고 tanstack query도 같이 사용하면 중복 캐싱이 되는게 아닌가? 라는 고민이 있었다..ㅎㅎ 그 헷갈렸던 개념을 다시 정리해보자.
서버에서 데이터를 미리 준비한 후, 브라우저가 요청하면 완성된 HTML을 내려주는 방식이다.
이 방식은 초기 로딩 속도가 빠르고, SEO(검색 엔진 최적화)에 유리하다.
클라이언트(브라우저)에서 API 요청을 직접 보내고, 받아온 데이터를 캐싱해서 재사용하는 방식이다. 같은 요청이 반복되더라도, 이전에 가져온 데이터를 활용하여 불필요한 네트워크 요청을 줄일 수 있다.
Next.js에서는 다양한 방식으로 서버에서 데이터를 캐싱하여 초기 로딩 속도를 최적화하고 불필요한 API 요청을 줄일 수 있다.
TanStack Query는 클라이언트에서 서버 상태를 관리하는 라이브러리로, 데이터를 캐싱하고 백그라운드에서 동기화하여 불필요한 API 요청을 줄이는 역할을 한다.
여기서 중요한 것은 클라이언트에서 서버 상태를 관리한다는 개념이다.
처음에는 서버 상태 관리라는 단어 때문에 서버에서 직접 캐싱을 해주는 것으로 오해할 수 있지만,
TanStack Query는 기본적으로 클라이언트 측에서 캐싱을 수행하는 라이브러리다.
SSR의 개념이 도입되면서, 서버에서 가져온 데이터를 클라이언트에서 재활용하기 위해 Hydration 과정이 추가되었다. 즉, 서버에서 미리 데이터를 받아오고, 이를 클라이언트에서 TanStack Query를 통해 관리하는 방식으로 발전한 것이다.
결론적으로, TanStack Query는 원래 클라이언트 캐싱을 위한 라이브러리였지만, SSR과 함께 사용될 때는 서버 데이터를 클라이언트로 전달하여 서버 캐싱 효과를 낼 수도 있다.
실제 캐싱은 여전히 클라이언트 측에서 이루어지기 때문에 nextjs 캐싱과는 관련이 없다.
TanStack Query는 클라이언트에서 서버 상태를 관리하는 라이브러리로, 데이터를 캐싱하고 백그라운드에서 동기화하여 불필요한 API 요청을 줄이는 역할을 한다.
Next.js의 서버 캐싱(SSG, SSR, ISR)은 초기 데이터 로딩을 최적화하고, TanStack Query는 이후의 데이터 상태를 관리하는 역할을 한다. 둘을 함께 사용하면 서버에서 가져온 데이터를 클라이언트에서 캐싱하여 API 요청을 줄이고, 성능을 최적화할 수 있다.
이렇게 이론으로 공부하고 실전에 코드를 막상 적용해보면, 내가 정말 공부했던 개념이 맞나.. 깜짝깜짝 놀랄 때가 많다..ㅎㅎ 개념에서 조금만 벗어나고 막막해지고 고민을 많이 하게 된다.
내가 앞으로 고쳐야할 점은 개발공부는 정답을 맞추는 시험 공부가 아니기에, 흐름을 읽고 다양한 방법을 고민하는 연습이 필요하다. 상황에 따라서 유연하게 접근하고, 더 나은 해결책을 찾아가는 과정이 중요하다는 것을 다시다시 생각해보고 잊지 말자.
참고한 사이트들
https://www.heropy.dev/p/HZaKIE
https://psst54.me/cse/tanstack-query
https://summermong.tistory.com/505