react-query v3를 TanStack Query v5로 올렸더니 useErrorBoundary가 사라져 있었다
메이저 업글이 바꾼 것 — 그리고 왜 그렇게 바꿨나
#TanStack Query #react-query #프론트엔드 #리팩토링 #React
예전에 react-query(v3)랑 useErrorBoundary로 API 에러를 한 곳에서 처리하는 글을 썼다. 그 구조를 잘 쓰고 있었는데, TanStack Query v5 로 올렸더니 그 useErrorBoundary가 throwOnError 로 이름이 바뀌어 있었다. 내 옛날 코드가 안 도는 거다. 이 글에선 v3에서 v5로 오면서 뭐가 바뀌었는지, 그리고 왜 그렇게 바꿨는지 를 정리해본다. 변경 이유를 보면 이 라이브러리가 뭘 후회했는지가 보인다. 일단 이름부터 바뀌었다 react-query는 v4부터 이름이 @tanstack/react-query 로 바뀌었다. React 말고 Vue, Svelte에서도 같은 코어를 쓰게 되면서, 특정 프레임워크 이름을 떼고 TanStack Query라는 우산 아래로 들어간 거다. import 경로부터 갈아야 한다. useErrorBoundary → throwOnError 내 49번 글의 핵심이던 옵션이다. 쿼리가 실패하면 에러를 ErrorBoundary로 던지는 그 옵션인데, 이름이 throwOnError 로 바뀌었다. 이유는 단순하다 — 이름이 기능을 더 정확하게 설명 하기 때문이다. "에러 바운더리를 쓴다"가 아니라 "에러를 throw한다"가 실제 동작이다. // v3 useQuery('posts', fetchPosts, { useErrorBoundary: true }); // v5 — 이름도 바뀌고, 인자 구조도 바뀜 useQuery({ queryKey: ['posts'], queryFn: fetchPosts, throwOnError: true }); 인자가 객체 하나로 통일됐다 위 코드에서 봤듯, 호출 방식 자체가 바뀌었다. v3는 useQuery(키, 함수, 옵션)처럼 인자를 넣는 방법이 여러 가지 였다. 키만 넣어도 되고, 함수까지 넣어도 되고… 이런 오버로드가 너무 많았다. v5는 이걸 다 없애고 객체 하나 로 통일했다. queryKey, queryFn을 객체에 담아 넘긴다. 왜 이렇게 바꿨냐면, 오버로드가 많을수록 라이브러리 내부에서 "지금 들어온 인자가 어떤 형태냐"를 런타임에 매번 확인해야 했고, 타입 정의도 지옥이 됐기 때문이다. 입구를 하나로 줄여서 유지보수랑 타입을 단순하게 만든 거다. 상태 이름도 바뀌었다 — loading → pending 이것도 헷갈리던 걸 바로잡은 변경이다. 기존 status의 'loading' 이 'pending' 으로, isLoading이 isPending으로 바뀌었다. 대신 새로운 isLoading이 생겼는데, 이건 isPending && isFetching 으로 정의된다. 즉 "데이터가 아직 없고(첫 로딩) + 지금 가져오는 중"일 때만 참이다. "처음 불러오는 중"과 "백그라운드에서 갱신 중"을 명확히 구분하려고 이름을 정리한 거다. useQuery에서 onSuccess / onError 콜백이 빠졌다 이게 처음엔 제일 당황스러웠다. 쿼리 성공/실패 시 부르던 onSuccess, onError 콜백이 useQuery에서 통째로 제거 됐다. 이유가 설득력 있다 — 이 콜백들이 리렌더링마다 일관되게 불리지 않아서 사람들이 자꾸 헷갈리고 버그를 냈기 때문이다. 캐시된 데이터를 쓸 땐 안 불리기도 해서, "왜 안 불리지?"가 반복됐다. 그래서 부수효과는 useQuery 밖(예: useEffect나 mutation 쪽)에서 다루도록 정리됐다. 헷갈리는 기능을 보강하는 대신 아예 빼버린 결정이다. 그 외 바뀐 것들 keepPreviousData 옵션은 placeholderData 로 흡수됐고, Suspense를 쓸 거면 전용 훅인 useSuspenseQuery 를 따로 쓰게 됐다. 그리고 v5는 내부적으로 useSyncExternalStore를 써서 React 18 이상이 필수 다. (그래서 앞서 React 19로 올려둔 게 자연스럽게 이어졌다.) 다행히 공식 codemod가 있어서, 이름 바뀐 것들은 어느 정도 자동으로 갈아줬다. 완벽하진 않아도 손이 많이 줄었다. 정리하면 라이브러리 메이저 업글은 귀찮지만, "왜 이렇게 바꿨나" 를 따라가다 보면 그 라이브러리가 뭘 후회했는지가 보인다. 오버로드를 없애 입구를 하나로 줄인 것, 헷갈리던 콜백을 아예 뺀 것, 이름을 동작에 맞게 고친 것 — 전부 "API는 적고 분명할수록 좋다" 는 자기반성이었다. 덕분에 내 코드도 같이 정리됐다. 49번 글에서 짠 에러 처리 구조를 throwOnError로 옮기면서, 군더더기가 빠지고 더 명확해졌다. 메이저 업글을 단순히 "깨진 거 고치는 일"로 보면 손해다. 라이브러리가 다듬은 방향을 따라가면, 내 코드도 같이 다듬어진다.