이 글을 읽고 queryOptions API 글을 읽는 걸 추천합니다
1. 들어가기
Query Keys는 React Query에서 아주 중요한 핵심 개념이다.
`queryKey` 덕분에 라이브러리 내부적으로 데이터를 올바르게 캐싱하고 `queryKey` dependency가 변경될 때 자동으로 refetch 할 수 있다.
또한 mutations 후 수동으로 쿼리 무효화(invalidate)를 하기위해 필요하다.
밑줄 친 위 기능들을 하나씩 살펴보고,
각 기능을 효과적으로 수행하기 위해 필요한 효율적인 `queryKey` 선언 방법에 대해 알아보자.
2. `queryKey` 관련 기능
2.1. 데이터 캐싱
내부적으로 Query 캐시는 javascript 객체이다.
key: 직렬화된 `queryKey` , value: Query data 와 메타 정보
queryKey는 배열 `[ ]` 형태이며 배열의 원소는 string, object등이 들어갈 수 있다.
원소들의 순서가 다르면 서로 다른 key로 인식된다. (`['todos', 'kuma']` 와 `['kuma', 'todos']` 는 서로 다르게 취급된다)
다만 원소가 객체일때, 해당 객체 내부 key의 순서가 달라져도 같은 쿼리로 간주된다.
// 쿼리 1
useQuery({
queryKey: ['todos', { status: 'completed', page: 1 }],
queryFn: fetchTodos,
})
// 쿼리 2
useQuery({
queryKey: ['todos', { page: 1, status: 'completed' }],
queryFn: fetchTodos,
})
// 두 쿼리는 같다
다만 key는 같더라도 value가 달라지만 다른 쿼리이다.
// 쿼리 1
useQuery({
queryKey: ['todos', { status: 'completed', page: 1 }],
queryFn: fetchTodos,
})
// 쿼리 2
useQuery({
queryKey: ['todos', { status: 'pending', page: 1 }],
queryFn: fetchTodos,
})
// 두 쿼리는 다르다.
또한 React Query가 캐시 목록에서 `queryKey`에 대응하는 value를 찾으면 이를 사용하기 때문에,
`queryKey`는 반드시 unique 해야한다.
또한 `useQuery` 와 `useInfiniteQuery`에 동일한 키를 사용할 수 없다는 점을 기억하자.
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// 🚨 useQuery와 queryKey가 동일하기 때문에 동작하지 않는다.
useInfiniteQuery({
queryKey: ['todos'],
queryFn: fetchInfiniteTodos,
})
// ✅ 다른 queryKey를 선언하자.
useInfiniteQuery({
queryKey: ['infiniteTodos'],
queryFn: fetchInfiniteTodos,
})
2.2. 자동 Refetch
쿼리는 선언적이다.
이는 아무리 강조해도 지나치지 않는 매우 중요한 컨셉이다.
대부분의 사람들은 쿼리를 생각할때, 특히 refetching을 할때 명령적으로 생각한다.
기존 쿼리가 있을때 필터 조건을 걸 수 있을때 이런식으로 사용하고 싶을 수 있다.
function Component() {
const { data, refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// ❓ 매개변수를 넘기고 싶은데.. ❓
return <Filters onApply={() => refetch(???)} />
}
하지만 이렇게 하면 안된다.
이는 refetch의 존재 목적이 아니다. - refetch의 목적은 같은 매개변수로 데이터를 다시 불러오는 것이다.
만약 필터와 같이 데이터를 변경하는 상태가 있는 경우, `queryKey`가 변경될 때마다 React Query가 자동으로 refetch 하므로, 해당 상태를 `queryKey`에 넣기만 하면 된다.
즉 쿼리에 필터를 적용하고 싶다면 클라이언트 상태를 변경하기만 하면 된다.
function Component() {
const [filters, setFilters] = React.useState()
const { data } = useQuery({
queryKey: ['todos', filters],
queryFn: () => fetchTodos(filters),
})
// ✅ 로컬 상태를 set하여 query를 가져오자.
return <Filters onApply={setFilters} />
}
`setFilters` 호출을 통해 리렌더링이 되면 다른 `filters`(queryKey)가 쿼리에 전달되어 data가 refetch 된다.
따라서 선언적으로 refetch가 되는 것이다.
선언적인 queryKey에 관해서는 1. 실용적인 React Query 글에도 언급한 적이 있다.
2.3. Manual Interaction (수동 상호작용)
`invalidateQueries` 나 `setQueriesData` 같이 개발자가 수동으로 쿼리를 조작할 수 있는 메서드는 호출할 때 Query Fliters를 넣어줘야한다. Query Filters에는 조작할 쿼리를 특정하기위해 `queryKey`가 이용된다.
// 첫 원소가 posts로 시작하는 queryKey를 가진 모든 inactive 쿼리들을 제거한다.
queryClient.removeQueries({ queryKey: ['posts'], type: 'inactive' })
3. 효과적인 React Query Keys
3.1. Colocate
참고) Kent C. Dodds의 Maintainability through colocation
모든 `queryKey`를 `/src/utils/queryKeys.ts`에 전역적으로 배치하는게 좋을까?
- src
- features
- Profile
- index.tsx
- queries.ts
- Todos
- index.tsx
- queries.ts
다음과 같이 훅이 가까이 쓰이는 폴더에 `fetch`, `useQuery`, `queryKey`를 같은 파일에 함께 선언해보자.
나(영진)는 보통 `useQuery`를 감싸는 커스텀훅을 내보내고, `queryKey`도 필요하다면 그때 내보낸다.
+ 추가
`queryKey`는 관련된 기능의 공통 부모 폴더에 하나의 객체로 선언해두는게 좋아보인다.
3.3 쿼리 키 팩토리 사용하기 참조
3.2. queryKey 배열 원소 선언 순서
일반적인 key -> 구체적인 key 순서대로 선언하자.
다음은 필터링 가능한 list와 각 item별 상세 보기가 가능한 투두리스트의 `queryKey` 예시이다.
['todos', 'list', { filters: 'all' }]
['todos', 'list', { filters: 'done' }]
['todos', 'detail', 1]
['todos', 'detail', 2]
이렇게 선언하면 `['todos']`와 관련된 모든 쿼리를 무효화할 수 있고, 정확한 `queryKey`를 알고있다면 특정 쿼리 하나를 타겟팅할 수 있다.
또한 `mutation` 결과(`onSuccess`, `onError`에서)에 따라 쿼리를 업데이트 할 수 있다.
// mutation 결과에 따라 query 업데이트 하기.
function useUpdateTitle() {
return useMutation({
mutationFn: updateTitle,
onSuccess: (newTodo) => {
// ✅ ['todos', 'detail', newTodo.id]인 캐시 업데이트
queryClient.setQueryData(
['todos', 'detail', newTodo.id],
newTodo
)
// ✅['todos', 'list'] 캐시 내부에 있는 todo.id에 해당하는 todo 업데이트
queryClient.setQueriesData(['todos', 'list'], (previous) =>
previous.map((todo) =>
todo.id === newTodo.id ? newtodo : todo
)
)
},
})
}
하지만 위 방법은 복잡하고, list와 detail의 todo 형식이 많이 다른 경우에는 적용하기 까다로울 수 있다.
대신 모든 `['todos']` 캐시를 무효화하는 방법이 있다. (서버에 재요청)
// 캐시 무효화 하기.
function useUpdateTitle() {
return useMutation({
mutationFn: updateTitle,
onSuccess: (newTodo) => {
queryClient.setQueryData(
['todos', 'detail', newTodo.id],
newTodo
)
// ✅ ['todos', 'list'] 캐시 무효화
queryClient.invalidateQueries({
queryKey: ['todos', 'list']
})
},
})
}
URL에서 필터를 읽는 등의 방식으로 `queryKey` 를 구성하여 `setQueryData`를 수행할 수도 있다.
function useUpdateTitle() {
// imagine a custom hook that returns
// the current filters, stored in the url
const { filters } = useFilterParams()
return useMutation({
mutationFn: updateTitle,
onSuccess: (newTodo) => {
queryClient.setQueryData(
['todos', 'detail', newTodo.id],
newTodo
)
// ✅ 현재 위치한 list 업데이트
queryClient.setQueryData(
['todos', 'list', { filters }],
(previous) =>
previous.map((todo) =>
todo.id === newTodo.id ? newtodo : todo
)
)
// 🥳 모든 list 무효화
// 다만 active인 list는 refetch 하지 않는다 (refetchType: 'none')
queryClient.invalidateQueries({
queryKey: ['todos', 'list'],
refetchType: 'none',
})
},
})
}
(사실 위 예시는 이해못했음. 그냥 `invalidateQueries` 만 해도 되지않나? `['todos', 'list', { filters }]` 캐싱을 하고싶은 건가?)
3.3. 쿼리 키 팩토리 사용하기
위 예제에서는 `queryKey`를 수동으로 선언해두었다.
이렇게 하면 더 세부적인 key를 추가하는 경우와 같이 변경하기가 여러워진다.
다음과 같이 어떤 기능과 관련된 `queryKey` 들은 객체 하나에 모두 모아두자.
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,
}
유연하고 독립적으로 키에 접근할 수 있다.
// 🕺 todo와 관련된 모든 쿼리를 제거한다.
queryClient.removeQueries({
queryKey: todoKeys.all
})
// 🚀 모든 todo list를 무효화한다.
queryClient.invalidateQueries({
queryKey: todoKeys.lists()
})
// 🙌 특정 todo 하나를 prefetch 한다.
queryClient.prefetchQueries({
queryKey: todoKeys.detail(id),
queryFn: () => fetchTodo(id),
})
각 기능별로 쿼리 키를 선언해두면 서버 상태로 한눈에 보기 쉬운 장점이 있을 것 같다.
Reference
'React Query' 카테고리의 다른 글
6. React Query가 필요한 이유 (0) | 2024.07.21 |
---|---|
5. React Query의 Status (0) | 2024.02.21 |
3. React Query와 에러핸들링 (2) | 2024.02.08 |
2. Form에서 React Query 잘 활용하기 (0) | 2024.01.22 |
1. 실용적인 React Query (0) | 2024.01.10 |