이 글을 읽고 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 |
이 글을 읽고 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 |