React Query를 잘 써보기 위한 탐구 [1]
최근에 회사에서 사용하는 React Query를 더 잘 쓰기 위한 논의를 하다가 이야기가 나온 것이 React Query의 기여자 중 한 분인 tkdodo의 React Query 포스팅 시리즈였습니다. React Query를 더 잘 쓰기 위한 고민들과 프랙티스들이 많아 정말 좋은 참고자료였는데요.
요즘 요 포스팅 타래를 쭉 읽어보고 있습니다. 현재 글은 23개인데, 대충 절반 정도인 11개의 포스팅을 읽고 각각의 글에 대한 간단한 독후감을 작성해 봤습니다. 내용을 요약/인용하고, 제 느낌이나 생각을 덧붙이는 방식입니다. 나중에 다 읽고 2탄도 써보겠습니다.
1. practical react query
So it seems that we have always been treating this server state like any other client state. Except that when it comes to server state (think: A list of articles that you fetch, the details of a User you want to display, ...), your app does not own it. We have only borrowed it to display the most recent version of it on the screen for the user. It is the server who owns the data.
To me, that introduced a paradigm shift in how to think about data. If we can leverage the cache to display data that we do not own, there isn't really much left that is real client state that also needs to be made available to the whole app.
데이터를 소유권의 개념으로 구분한 것이 명쾌하다고 느껴졌습니다. 이것은 처음 RQ를 접했을때부터 느낀 것인데요.
서버에서 데이터를 빌려(borrow)서 가장 최신의 버전을 보여주는 책임만이 클라이언트에 존재한다는 말로, 데이터 소유권에 따른 책임을 명확히 하고 있습니다. 이 구분이 라이브러리 존재의 시작점이 되기도 하고, 개발의 로드맵이 어디에 집중할지도 명시가 되는 것 같습니다.
(Don't use the queryCache as a local state manager) If you tamper with the queryCache (
queryClient.setQueryData
), it should only be for optimistic updates or for writing data that you receive from the backend after a mutation. Remember that every background refetch might override that data, so use something else for local state.
명시적으로 데이터를 set하는 것을 최대한 지양하라는 말로 해석이 되었습니다. 맥락으로 볼때 앞에서 말했던 상태의 분리, 그리고 background fetching이라는 동작이 최대한 의도된 바 그대로 이루어지게 하려는 것 같습니다.
"use something else"로 제시한게 react의 state, zustand, redux인데 RQ 역시 구조적으로는 Provider가 있는 탑다운 방식의 상태관리를 하고 있으니 이 맥락에서 맞는 도구들을 제시한게 아닌가 싶습니다.
2. React Query Data Transformations
On the backend
일하다보면 백엔드에서 데이터를 바꿔줄 수 있다는 것을 가끔 잊어먹기도 하기에 좋은 포인트를 짚어줬다고 생각합니다. 역시 다 내가 해야 하는 것은 아닙니다.
어떤 데이터 변경(transform)들은 백엔드에서 하는게 더 맞을 때도 있습니다. 또한 데이터를 빌리는 클라이언트가 여러개라면 백엔드에서 단일성을 보장하는게 더 좋을 때도 있습니다. 백엔드의 데이터를 변경하는 로직이 클라이언트에 많이 들어있을수록 어디서 뭔 일이 일어나는지 판단하기 항상 어려워졌습니다.
But if you pass a selector, you are now only subscribed to the result of the selector function. This is quite powerful,
selector는 query가 watch해야할 변경점을 좁히는 역할도 같이 합니다.
사실 이 글에는 데이터를 변경하는 장소(backend, queryFn, render function, select) 이렇게 보여주고 select
가 가장 단점이 적어 보이지만
경험상 백엔드의 응답을 query의 결과값에 그대로 넣어야 좋은 상황이 꽤 많았습니다. query와 특정 백엔드 데이터를 1:1로 대응시키는게 더 단순한 멘탈 모델이고, 여러 작업자가 문서화 등으로 데이터의 생김새를 쉽게 알 수 있는 백엔드의 응답값이라는 공동의 이해 위에서 작업을 하는게 더 나았어요. select
는 쉽게 암묵지가 되거나 select
가 설정된 query를 훅으로 말아서 사용하면 특정 기능에서 원하는 데이터를 가져올 수가 없어서 중복 패칭을 유발할 수도 있었습니다.
그래서 결국 백엔드의 응답을 query에 살려두고, 이 query에서 나온 응답을 select
해주는 경우가 확장성이 더 나을 수도 있다는 생각은 들었습니다. 이런 전제라면 원래의 query에서 나온 응답을 select
하기 위해 또 query를 쓰는게 오버킬처럼 느껴질 수도 있어서 render function에서의 데이터 transform도 빈번하게 이뤄질 수 있을 것 같네요.
3. React Query Render Optimizations
Render optimizations are an advanced concept for any app. React Query already comes with very good optimizations and defaults out of the box, and most of the time, no further optimizations are needed.
I'd take an "unnecessary re-render" over a "missing render-that-should-have-been-there" all day every day.
React와 관련된 최적화 썰에서 항상 "최적화를 하자"와 "발적화를 하지말자"는 항상 같이 나오는 말인 것 같습니다. Kent Dodds의 좋은 글도 레퍼런스를 걸어놨습니다.
I'm quite proud of this feature, given that it was my first major contribution to the library. If you set
notifyOnChangeProps
to'tracked'
, React Query will keep track of the fields you are using during render, and will use this to compute the list.
v4부터 기본으로 적용되었던 tracked query는 정말 잘 만든 기능이라고 생각합니다. 사용자가 신경써야 할 것을 적절하게 라이브러리 단으로 가져오며 바깥으로 나가는 인터페이스는 설정값 딱 하나만 만들었습니다.
필요한 속성만 뽑아내 쓰라는 것 이상의 무언가를 이해하는 것이 앱 운영이나 라이브러리 사용에 필수적인 것도 아닙니다. 상당한 엣지케이스라도 그럴 필요가 없을 것 같은 느낌이 드네요.
요런 특성의 기능들을 잘 파악해서 라이브러리 안으로 깔끔하게 가져오는 것이 라이브러리 구현 관점의 미덕이 아닌가... 하는 생각이 듭니다.
- If you use object rest destructuring, you are effectively observing all fields. Normal destructuring is fine, just don't do this:
전개 연산자로 디스트럭쳐링을 하면 tracked query의 효과가 없어진다고 합니다.
4. Status Checks in React Query
success
: Your query was successful, and you havedata
for iterror
: Your query did not work, and anerror
is setpending
: Your query has no data
status
에 대한 설명입니다. v4부터 바뀌었던 status
와 fetchStatus
의 분화로 설명하기 좋아진 것 같습니다. status
는 data
의 유무를 기준으로 하기에 멘탈 모델이 단순합니다. v5부터는 loading
이 pending
으로 이름이 바뀌었습니다. 주로 pending이 데이터가 없음을 더 잘 나타내는 네이밍이라는 이유라서 그런 듯 합니다. promise의 pending 상태를 생각해보면 말이 됩니다.
This is even more relevant when we take into account that React Query will retry failed queries three times per default with exponential backoff, so it might take a couple of seconds until the stale data is replaced with the error screen. If you also have no background fetching indicator, this can be really perplexing. This is why I usually check for data-availability first:
페치 실패시 retry
는 3번이 기본값이므로 에러 발생시 에러 뷰를 띄운다면 에러를 화면에서 알 수 있는 시점이 꽤나 늦을 수 밖에 없습니다. 그래서 에러나 로딩 상태보다 데이터가 available한지를 먼저 알아내서 분기를 하는게 더 좋은 프랙티스라고 이야기하는 부분입니다.
제시된 예제처럼 하면 리페치시 에러 유무에 상관없이 데이터가 있으면 무조건 그 데이터를 보여줄 것입니다.
// data-first
const todos = useTodos();
if (todos.data) {
return <div>{todos.data.map(renderTodo)}</div>;
}
if (todos.error) {
return 'An error has occurred: ' + todos.error.message;
}
return 'Loading...';
5. Testing React Query
백엔드 API를 모킹할 수 있는 좋은 방식을 설명하고 그 다음에 RQ에 한정된 부분을 설명하는 글 전개 방식이 명확해서 좋았습니다. 그리고 테스트 꿀팁들이 있어요.
It's one of the most common "gotchas" with React Query and testing: The library defaults to three retries with exponential backoff, which means that your tests are likely to timeout if you want to test an erroneous query. The easiest way to turn retries off is, again, via the
QueryClientProvider
.
테스트용 QueryClient
는 retry
옵션을 끄자는 말입니다.
The best advice I can give you for this problem is: Don't set these options on
useQuery
directly. Try to use and override the defaults as much as possible, and if you really need to change something for specific queries, use queryClient.setQueryDefaults.
테스트의 상황에 맞게 특정 query의 옵션을 바꿔줄 수 있는 API인 setQueryDefaults
가 있습니다. 이건 테스트가 아닌 애플리케이션에서 바로 사용하면 설정값이 어디서 들어왔는지 모르게 되서 작업자의 뇌정지를 유발할 수도 있을 것 같다는 생각이 들었습니다.
Since React Query is async by nature, when running the hook, you won't immediately get a result. It usually will be in loading state and without data to check
당연한 이야기지만 비동기로 테스트 결과값을 기대해야 한다고 합니다.
6. React Query and TypeScript
If you (like me) like to keep your api layer separated from your queries, you'll need to add type definitions anyways to avoid implicit any, so React Query can infer the rest:
Since React Query is not in charge of the function that returns the Promise, it also can't know what type of errors it might produce. So
unknown
is correct.
타입 추론을 최대한 이용하려면 queryFn
에 들어가는 함수에 리턴 타입을 주고, 에러 타입은 제네 릭을 주지 않으면 타입을 알 수 없으니(unknown
) 직접 핸들하는 방식으로 사용해야 합니다.
It will further help TypeScript to narrow types when using the status field or one of the status booleans, which it cannot do if you use destructuring:
useQuery
의 리턴값은 설정값에 따라 추론되니 디스트럭쳐링 없이 사용하는게 타입 추론에는 더 이점이 있습니다. 근데 필요한 프로퍼티만 디스트럭쳐링 해야 잘 작동한다는 tracked query 쪽 내용이랑 좀 상충되는거 같네요.
7. Using WebSockets with React Query
React Query doesn't have anything built-in specifically for WebSockets. That doesn't mean that WebSockets are not supported or that they don't work well with the library. It's just that React Query is very agnostic when it comes to how you fetch your data: All it needs is a resolved or rejected
Promise
to work - the rest is up to you.
React Query가 명시적으로 웹소켓을 지원하기위한 특정 구현을 가지고 있지 않음을 말하는데, promise 기반으로만 동작시켜주면 되기 때문입니다. 사실 이런 컨셉 때문에 데이터를 어디에서 어떻게 받아오든 상관이 없겠죠. 요걸 짚어준게 좋았습니다.
This goal overlaps a lot with WebSockets, which update your data in real-time. Why would I need to refetch at all if I just manually invalidated because the server just told me to do so via a dedicated message?
웹 소켓은 이벤트 기반으로 명시적으로 데이터를 업데이트, invalidate하기 때문에 시간에 따라 stale해짐을 상정할 필요가 없을 수도 있습니다. 따라서 staleTime
을 Inifinity
로 잡아도 괜찮을 수 있다는 말이 되겠습니다.
8. Effective React Query Keys
If you have some state that changes your data, all you need to do is to put it in the Query Key, because React Query will trigger a refetch automatically whenever the key changes. So when you want to apply your filters, just change your client state:
refetch
를 핸들러에 넣어서 페치하려고 하지 말고, queryKey
를 바꿔 페치가 다시 이루어지도록 하라는 말인데요. refetch
는 파라미터를 바꿔서 다시 요청하려고 할 때 쓰려고 만든게 아니라고 합니다.
query를 선언적으로 작성하고 다루는데 필요한 사용 방식으로 이해됩니다. Treat the query key like a dependency array 에서 이야기 했던 설명과 이어지기도 합니다.
Manual Interactions with the Query Cache are where the structure of your Query Keys is most important. Many of those interaction methods, like invalidateQueries or setQueriesData support Query Filters, which allow you to fuzzily match your Query Keys.
명시적으로 query cache를 쉽게 다루기 위해 query filter가 있다는건 오래 전부터 알고 있었습니다.
그런데 뭔가 실제 개발할때 많이 (혹은 엄밀하게) 써보질 않았습니다. 앱 내에서 queryKey
를 제대로 관리하지 못하는 경우, 혹은 뭉뚱그려서 그냥 이것저것 다 invalidate 시켰던 경우가 생각났습니다. 그만큼 사용자 경험을 저하시켰을 텐데 반성이 됩니다.
I have found these strategies to work best when your App becomes more complex, and they also scale quite well. You definitely don't need to do this for a Todo App 😁.
tkdodo님의 기술 글쓰기가 편안하다고 느껴지는 부분들 중 하나는, 이렇게 특정 프랙티스에 대한 전제를 아주 잘 짚고 넘어가는 부분이 많아서인 것 같습니다.
Yes, Query Keys can be a string, too, but to keep things unified, I like to always use Arrays. React Query will internally convert them to an Array anyhow, so:
v4로 넘어오면서 배열 queryKey
만 쓸 수 있도록 되었는데, string을 허용할 때에도 queryKey
는 배열로 변형되는 구조였다고 합니다.
Structure your Query Keys from most generic to most specific, with as many levels of granularity as you see fit in between. Here's how I would structure a todos list that allows for filterable lists as well as detail views:
['todos', 'list', { filters: 'all' }][('todos', 'list', { filters: 'done' })][
('todos', 'detail', 1)
][('todos', 'detail', 2)];
인지적으로도 더 좋은 방법으로 보여지네요. 제공된 예제도 queryKey
가 이런 식으로 짜지지 않았다면 더 선언적으로 보이지 않았을 것 같습니다.
That's why I recommend one Query Key factory per feature. It's just a simple object with entries and functions that will produce query keys, which you can then use in your custom hooks. For the above example structure, it would look something like this:
const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
details: () => [...todoKeys.all, 'detail'] as const,
detail: (id: number) => [...todoKeys.details(), id] as const,
};
데이터에 특성에 맞게 queryKey
팩토리를 만들라는 말인데 좋은 방법처럼 보입니다. 특히 커다란 프로젝트의 경우에는 더더욱 그럴 것 같아요.
곁가지 이야기인데 queryKey
를 실무에서는 URL 엔드포인트와 request body 같은 것들을 자동으로 넣어 구분시키는 경우도 꽤 있었는데 이게 좋은 프랙티스일까 생각했던 적이 있었습니다.
queryKey
를 서버에서 받아온 정보를 토대로, 어느정도 클라이언트에서 조정하고 이해할 수 있는 개념으로 작성해야 할 것입니다. 그래야 다루기가 더 쉽고, 데이터를 클라이언트 위주로 다룰 수 있는 레이어가 만들어질 수 있는 것 같습니다.
query를 명시적으로 처리하기 위해 api 요청했던 URL을 떠올려야 한다거나... 하는 프랙티스는 뇌정지를 유발하기가 쉬우니, 서버의 정보를 토대로 만들어진 queryKey
를 팩토리로 만들고 쉽게 가져다 쓸 수 있는 형태는 좋은 것 같습니다. 팩토리를 만들지 않는다면, 관리가 유용하게끔 queryKey
에 들어갈 것들을 간단하게 잘 만들어 써야 하겠습니다. 결국 인지의 용이함 이야기인 것 같기도 하고 그렇네요.
역시 저만 관심있는 주제는 아니었는지, 댓글창에 여러 맥락의 질문들이 이루어지고 있던게 흥미로웠습니다. 눈에 띄던 몇 개를 가져와보자면
if every key starts with the name of the feature, there shouldn't be any clashes unless you have features with the same name. It was rather a problem for us to have global query keys, because they can get quite large, and if you then copy-paste one line but don't change the query key prefix (it happens!), you'll get key duplications which are very hard to spot. I've been there and it took me hours to find
queryKey
collocation해도 중복될 수 있으니 글로벌하게 관리하면 어떨까? 하는 질문에 그렇게 하면 너무 커지고 알아보기도 어려워지니 granuality를 보장하는 방식으로 queryKey
가 만들어 지는게 중요하다는 답변입니다.
query key factory concept is very useful! how about using api baseUrl (with path parameter, query parameter) to query key? 👀
yes, you can do that as well and then even leverage the defaultQueryFn as described here: default query function
라이브러리 입장에서도 서버의 정보를 바탕으로 queryKey
를 만드는게 완전 비추천하는 프랙티스는 아닌 것 같습니다.
8a. Leveraging the Query Function Context
Don't use inline functions - leverage the Query Function Context given to you, and use a Query Key factory that produces object keys
export const useTodos = () => {
const { state, sorting } = useTodoParams();
// 🚨 can you spot the mistake ⬇️
return useQuery({
queryKey: ['todos', state],
queryFn: () => fetchTodos(state, sorting),
});
};
여러개의 파라미터가 필요한 데이터 요청의 경우 queryFn
에 param을 계속 넣어주는 방식으로 확장을 시킬 수 있는데, 이렇게 하면 queryFn
의 인자와 queryKey
배열을 동시에 수정해야 하니 너무 많아졌을 때 관리에 고충이 생기고 실수할 여지가 생기니 queryFunctionContext
패턴을 써보면 좋다 - 정도로 이해가 되었습니다.
The
QueryFunctionContext
is an object that is passed as argument to thequeryFn
const fetchTodos = async ({ queryKey }) => {
// 🚀 we can get all params from the queryKey
const [, state, sorting] = queryKey;
const response = await axios.get(`todos/${state}?sorting=${sorting}`);
return response.data;
};
export const useTodos = () => {
const { state, sorting } = useTodoParams();
// ✅ no need to pass parameters manually
return useQuery({
queryKey: ['todos', state, sorting],
queryFn: fetchTodos,
});
};
9. Placeholder and Initial Data in React Query
여기는 잘 몰랐던 내용이 많아서 그냥 글 전체를 요약했습니다.
Another way is to synchronously pre-fill the cache with data that we think will potentially be right for our use-case, and for that, React Query offers two different yet similar approaches: Placeholder Data and Initial Data.
As already hinted, they both provide a way to pre-fill the cache with data that we have synchronously available. It further means that if either one of these is supplied, our query will not be in
loading
state, but will go directly tosuccess
state
사실 placeholderData
라는게 있는지 몰랐는데요. placeholderData
와 initialData
는 주어지는 순간 loading
상태를 건너뛰고 바로 success
로 직행한다는 공통점이 있습니다.
For each Query Key, there is only one cache entry. This is kinda obvious because part of what makes React Query great is the possibility to share the same data "globally" in our application.
An observer in React Query is, broadly speaking, a subscription created for one cache entry.
cache
는 queryKey
당 하나의 진입점만을 가지고 있고, observer
는 하나의 queryKey
에 여러개가 될 수도 있습니다.
이 observer
라는 개념을 처음 접했는데요. RQ의 동작방식 가지고 동료들과 이야기할 때 query를 기본적인 단위로 상정해서 이야기를 많이 했습니다. query를 hook으로 말아놓아도 "잉 왜 설정값은 똑같은데 여기서는 동작이 다르지" 싶은 것도 있었습니다.
그것은 RQ의 동작이 query를 기반으로 하는 것이 아니라 query observer를 기반으로 동작하기 때문이겠구나 싶네요. query와 query observer는 다릅니다.
InitialData
works on cache level, whileplaceholderData
works on observer level. This has a couple of implications:
initialData
는 캐시 레벨에, placeholderData
는 옵저버 레벨에 존재한다고 말할 수 있습니다. 아래는 이 부분을 요약한 것입니다.
- Persistence:
initialData
는 캐시에 저장되지만placeholderData
는 그렇지 못하다. - Background refetches:
initialData
는staleTime
과 연관이 있지만placeholderData
는 없다. - Error Transitions:
initialData
확보시의 실패는 일반적인 background query error처럼 처리되어 기존의 캐시 데이터가 남아있으나,placeholderData
는 따로 에러 처리가 되지 않고 캐시 데이터는undefined
로 남는다.
I personally like to use
initialData
when pre-filling a query from another query, and I useplaceholderData
for everything else.
initialData
가 여러 observer들로 하여금 같은 데이터를 가지고 있을 수 있게끔 "전파"한다고 이해할 수도 있어 보이네요.
What do you think will happen in each situation? I've hidden the answers so that you can try to come up with them for yourselves if you want before expanding them.
깨알같지만 이런 부분도 기술 관련 글을 전개하는데 좋은 포인트인 것 같네요.
10. React Query as a State Manager
React Query is loved by many for drastically simplifying data fetching in React applications. So it might come as a bit of a surprise if I tell you that React Query is in fact NOT a data fetching library.
React Query is an async state manager. It can manage any form of asynchronous state - it is happy as long as it gets a Promise.
React Query는 Data fetching library가 아니라 Async state manager입니다. RQ는 네크워크나 비동기 요청이 이루어지는 레이어에 대해 아주 조금만 알고 있을 뿐이고(offline 모드와 같은 부분), 데이터 패칭은 실질적으로 axios나 ky같은 http 클라이언트가 담당합니다.
RQ가 저장할 수 있는 상태가 Promise를 통해 제공되는 이상, 사실 데이터 패칭이 이루어지지 않아도 어떤 데이터든지 RQ는 가지고 있을 수 있습니다. 아까 웹 소켓의 사례를 생각해보아도 맞는 말이죠.
Because React Query manages async state (or, in terms of data fetching: server state), it assumes that the frontend application doesn't "own" the data. And that's totally right. If we display data on the screen that we fetch from an API, we only display a "snapshot" of that data
클라이언트 애플리케이션 데이터 페칭에 국 한된 고유한 지점들을 상태관리 라이브러리의 구현에 받아들였다는 것이, RQ와 기존 비동기 데이터를 처리할때 썼던 Redux같은 상태관리 도구와 가장 큰 차이점인 것 같습니다.
데이터 요소의 특성에 맞게 시계열로 데이터를 관리할 수 있는 방식이라던지(staleTime
, cacheTime
), SWR이라는 컨셉, React의 생명주기와 관련되어 관리할 수 있는 방식들(refetchOnMount
)을 제공하고 있는 것이 그런 구현들로 생각이 됩니다.
Redux에서는 상태는 그냥 상태일 뿐이고 비동기 데이터를 처리하는 방식은 미들웨어 등의 다른 방식으로 직접 만들어줘야 했었죠.
The principle is that stale data is better than no data, because no data usually means a loading spinner, and this will be perceived as "slow" by users. At the same time, it will try to perform a background refetch to revalidate that data.
철학 자체가 SWR 기반이니 이를 제대로 이용하고 사용자에게 로딩 서클을 최대한 보이지 않는 것이 RQ를 제대로 이용하는 방식과 같다는 생각도 듭니다.
이 포스팅에서는 RQ는 상태 관리 라이브러리다! 라고 말하고는 있지만 상태 관리 라이브러리처럼만 쓰거나 기대하면 안 된다는 생각도 동시에 드는 부분입니다. 요 단락 아래에서 나오는 smart refetches
, staleTime
과 같은 기능 설명들이 그런 이야기를 하고 있다고 느껴져요.
This is mainly because
staleTime
defaults to zero, which means that every time you e.g. mount a new component instance, you will get a background refetch.
요즘은 처음 RQ를 셋업할 때부터 staleTime
을 기본 Inifinity
로 놓고 쓰면 어떨까... 하는 생각이 좀 드는게 결국 default로 설정된 0은 가장 기본적인 상황을 상정하고 있고, 최대한 많은 페칭을 만들어내기 때문입니다.
최대한 오래 fresh한 상태로 유지하게 한 다음 필요한 사용처에 명시적인 설정값으로 refetch, Invalidation을 만들어내는 것이 더 최적점을 찾기 쉬운 프랙티스로 생각이 들긴 합니다. 해보진 않아서 짐작만으로 말하는 거긴 하지만요.
What's going on here, I just fetched my data 2 seconds ago, why is there another network request happening? This is insane!
As long as data is fresh, it will always come from the cache only. You will not see a network request for fresh data, no matter how often you want to retrieve it.
같은 queryKey
를 가진 query를 다른 옵션 없이 2개를 연달아 React 컴포넌트 안에서 호출하면 페칭도 2번 이루어집니다. 데이터가 fresh한 상태로 남아있어야 페칭이 일어나지 않습니다.
That changed a lot when hooks came around. You can now
useContext
,useQuery
oruseSelector
(if you're using redux) everywhere, and thus inject dependencies into your component. You can argue that doing so makes your component more coupled. You can also say that it's now more independent because you can move it around freely in your app, and it will just work on its own.
저는 개인적으로 React Hooks가 DI의 수단이라고 말하기에는 너무 많은 것들이 결합되게끔 로직이 짜진다고 생각해서 그렇게 생각하진 않아요. 하지만 또 이 글에서 트레이드 오프라고 말한 것인, DI를 props로 하는게 너무 쉽지 않다 라고 하는 것도 동의는 됩니다.
11. React Query Error Handling
You see, the
onError
callback onuseQuery
is called for everyObserver
, which means if you calluseTodos
twice in your application, you will get two error toasts, even though only one network request fails.
에러도 observer 단위로 처리되기 때문에, onError 콜백도 observer의 개수만큼 호출됩니다.
The global callbacks need to be provided when you create the
QueryCache
, which happens implicitly when you create anew QueryClient
, but you can also customize that:
전역적인 형태의 에러 처리는 queryClient
단에서, 지역적인 형태의 에러 처리는 ErrorBoundary
에 처리 될 것을 기대하고 개별 query에서 하면 되겠다는 생각이 들었습니다.
요런 원칙을 좀 더 잘 지킬 수 있게 룰 같은게 필요하거나, 에러 토스트 처리를 좀 잘 할 수 있는 설계가 필요하다고 느껴집니다. 에러 토스트인데 전역에서 뜨는 거랑 좀 다른 걸 띄우고 싶은 니즈같은게 생길수도 있어서요.
(끝)