쿼리 키는 리액트 쿼리를 사용하는 데 핵심적 역할을 한다. 이 쿼리 키를 잘 이해하지 못하고 사용하면, 캐시를 업데이트하거나, 필요한 캐시를 가져오는 게 내 마음 같지 않다.. 분명히 getQueryData를 하는데 계속 undefined가 출력된다거나 invalidateQueries를 했는데 화면이 원하는대로 업데이트되지 않는다면 쿼리 키를 잘못 사용하고 있을 가능성이 크다. 좌절하지 말고 하나씩 확인해보고 이해가 안 되면 아래를 읽어보자 !
확인할 사항
- 쿼리 키를 사용하여 직접적으로 쿼리 캐시에 접근하는 메서드(invalidateQueries, setQueryData, getQueryData 등) 사용 시, useQuery로 받아온 데이터를 캐싱하고 있나요?
- 변하는 값에 따라 refetch해야 하는 경우, 변하는 값을 쿼리 키에 함께 주고 있나요?
- 고유한 쿼리 키를 배열에 필요한 순서대로 잘 작성하였나요?
쿼리 키의 역할
- 쿼리에 대한 종속성이 변경되면 데이터를 캐싱하고 자동으로 refetch하기 위해 필요하다. 예를 들어, useQuery([’queryKey’, storeId])와 같이 키 값을 설정하면 storeId가 변경될 때마다 데이터가 refetch된다.
- 쿼리 키는 쿼리 캐시를 직접 변경할 때도 사용될 수 있다. 예를 들어, mutation 후 데이터를 업데이트(setQueryData)하거나 일부 쿼리를 직접 무효화(invalidateQueries)할 때 사용한다.
특징
- 쿼리 키는 고유해야 한다. 쿼리 캐시는 { 고유한 쿼리 : **쿼리 데이터, 메타 정보 }**를 가지는 자바스크립트 객체이다.
- 쿼리 키가 변경되면 refetch한다.
- 따라서 특정 값이 변경될 때마다 자동으로 refetch하려면 쿼리 키에 해당 값을 넣어주면 된다. 이 쿼리 키는 useEffect가 dependency array 변경 시마다 실행하는 형태와 유사하다.
// useStore.js
const storeQuery = (storeId) => ({
queryKey: [...storeQueryKey, storeId],
queryFn: fetchStore(storeId),
onError: (error: AxiosError) => console.error(error),
});
const useStore = (storeId) => useQuery(storeQuery(storeId));
export default useStore;
- 예를 들어, 위의 storeQuery는 storeId가 변경되면 refetch한다. storeId는 pathname에서 받아오는 값으로, url이 변경되면 storeId도 변경되고, 새롭게 store 데이터를 가져온다.
- 직접 캐시를 업데이트할 때 쿼리 키의 구조가 중요하다. 예를 들어, InvalidateQueries 또는 setQueryData와 같은 메서드는 쿼리 필터를 활용해 쿼리 키를 구성할 수 있다.
기본 사용법
간단한 쿼리 키
가장 간단하게 constants 값을 넣을 수 있다. 이 형태는 일반적인 리스트나 인덱스 자료구조, 또는 비계층적 자료구조(배열, 스택, 큐 등)에 유용하다.
// store 데이터
useQuery({ queryKey: ['store'], ... })
// 페이지네이션을 적용한 댓글 데이터
useQuery({ queryKey: ['comments', 'currentPage'], ... })
변수(variables)가 있는 배열 쿼리 키
타겟팅하려는 데이터를 가리키는 정보가 더 필요한 경우, string 배열과 객체를 사용할 수 있다. 이는 계층적/중첩 구조(트리), 그리고 추가적인 파라미터가 필요한 쿼리에 유용하게 사용된다. 예를 들어, todo에서 특정 item을 찾기 위해 id나 index를 전달할 수 있고, 추가적인 option을 전달할 수도 있다.
// 하나의 todo
useQuery({ queryKey: ['todo', 5], ... })
// preview 형식의 todo
useQuery({ queryKey: ['todo', 5, { preview: true }], ...})
// 'done' 상태인 todo
useQuery({ queryKey: ['todos', { type: 'done' }], ... })
쿼리 키는 결정론적(?)으로 해시된다!
결정론적(deterministic)이라는 것은 주어진 입력 값이 항상 같은 해시 값을 생성한다는 것이다. 즉, 배열에 키를 전달하면 항상 같은 해시 값을 생성한다. 따라서 순서가 유의미한 배열에서는 동일한 순서의 키를 전달해주어야 원하는 쿼리 캐시에 접근할 수 있다.
객체(순서 의미 X) 내에 있는 키들의 순서는 상관없이 똑같이 취급된다.
// 아래는 모두 같음.
useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})
useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... })
하지만 배열(순서 의미 O) 요소인 키들은 순서에 의미가 있다.
// 같지 않음.
useQuery({ queryKey: ['todos', status, page], ... })
useQuery({ queryKey: ['todos', page, status], ...})
useQuery({ queryKey: ['todos', undefined, page, status], ...})
쿼리 키 효율적으로 사용하기
리액트 쿼리 공식 문서 블로그인 TKdodo의 블로그에서는 프로그램이 복잡해지는 경우, 아래와 같이 쿼리 키를 사용하기를 권장한다.
1. 리액트 쿼리 관련 파일은 관련된 기능 폴더 하위에 둔다. 모든 쿼리 키를 /src/utils/queryKeys.ts처럼 전역에 두기보다는 관련 기능의 폴더 아래 두는 것이 유지보수에 좋다.
- src
- features
- Profile
- index.tsx
- queries.ts
- Todos
- index.tsx
- queries.ts
2. 쿼리 키는 항상 배열로 사용해야 한다. (React Query v4부터)
3. 쿼리 키 구조
['todos', 'list', { filters: 'all' }]
['todos', 'list', { filters: 'done' }]
['todos', 'detail', 1]
['todos', 'detail', 2]
- [ ’todos’ ]로 모든 todo관련 캐시, [’todos’, ‘list’] 또는 [ ’todos’, ‘detail’ ]로 todos의 list나 detail 관련 캐시를 무효화할 수도 있다.
- 정확한 키(ex. 1, 2와 같은 todoId)를 사용하면 하나씩 쿼리 캐시를 업데이트할 수 있다.
4. 위처럼 쿼리 키를 직접 선언하기보다 Query Key factory를 사용하자. 이는 하나의 기능 당 하나의 Key를 객체로 관리하는 것이다.이렇게 사용하면 각 키가 다른 키를 기반으로 만들어지지만, 독립적으로도 접근할 수 있어 유연하다.
const todoKeys = {
all: ['todos'], // todos 전체
lists: () => [...todoKeys.all, 'list'], // todos 전체 중 list
list: (filters: string) => [...todoKeys.lists(), { filters }], // filtered lists
details: () => [...todoKeys.all, 'detail'], // todos 전체 중 detail
detail: (id: number) => [...todoKeys.details(), id], // id를 만족하는 detail
}
이렇게 사용하면 각 키가 다른 키를 기반으로 만들어지지만, 독립적으로도 접근할 수 있어 유연하다.
// todos 관련 모든 기능 제거
queryClient.removeQueries({ queryKey: todoKeys.all })
// 모든 lists 무효화
queryClient.invalidateQueries({ queryKey: todoKeys.lists() })
// 하나의 todo prefetch
queryClient.prefetchQueries({
queryKey: todoKeys.detail(id),
queryFn: () => fetchTodo(id),
})
REF
'FrontEnd > React' 카테고리의 다른 글
TanStack Query를 활용하여 Route 기반 Prefetching하기(feat.Render-as-you-fetch) (25) | 2024.02.27 |
---|---|
[React Query] React Query에서 ErrorBoundary로 에러 핸들링하기 (3) | 2023.12.10 |
[React-Query] React Query로 페이지네이션하기(keepPreviousData 옵션) (0) | 2023.07.04 |
[React-Query] useMutation 활용편(낙관적 업데이트, custom hook으로 활용하기) (0) | 2023.07.01 |
[React-Query] useMutation 개념편 (0) | 2023.07.01 |