본문 바로가기

FrontEnd

리액트 쿼리 : 쿼리키

반응형

원문 : https://tkdodo.eu/blog/effective-react-query-keys

 

쿼리 키

  • 라이브러리가 내부적으로 데이터를 올바르게 캐시하기 위해 필요합니다.
  • 쿼리 종속성이 변경될 때 자동으로 다시 가져올 수 있도록 하기 위해 필요합니다.
  • 쿼리 캐시와 수동으로 상호 작용할 수 있도록 해줍니다.
    • 변이(뮤테이션) 후 데이터를 업데이트하거나 일부 쿼리를 수동으로 무효화해야 하는 경우와 같이 필요할 때 

데이터 캐싱

내부적으로 쿼리 캐시는 단지 자바스크립트 객체입니다.
키는 직렬화된 쿼리키이며 
값은 메타 정보가 추가된 쿼리 데이터입니다.
 
 
키는 결정적인 방식(deterministic way)으로 해시되므로 객체도 사용할 수 있습니다.
최상위 수준에서 키는 문자열 혹은 배열이어야 하며, 일반 객체의 경우 필드 순서가 중요하지 않지만, 배열 객체의 경우 필드 순서가 중요합니다. 

가장 중요한 것은 키가 쿼리에 대해 고유해야 한다는 것입니다.

React Query가 캐시에서 키에 해당하는 항목을 찾으면, 이를 사용합니다.
 
또한 useQuery와 useInfiniteQuery에 동일한 키를 사용할 수 없다는 점에 유의하십시오.
결국 하나의 쿼리 캐시만 있으며 이 둘 간에 데이터를 공유하게 됩니다.
무한 쿼리는 "일반" 쿼리와 구조가 근본적으로 다르기 때문에 좋지 않습니다.
useQuery(['todos'], fetchTodos)

// 🚨 this won't work
useInfiniteQuery(['todos'], fetchInfiniteTodos)

// ✅ choose something else instead
useInfiniteQuery(['infiniteTodos'], fetchInfiniteTodos)​

자동 리페칭

쿼리는 선언적입니다.

쿼리가 있습니다. 데이터를 가져옵니다. 이제 이 버튼을 클릭하고 다시 가져오고 싶지만 다른 매개변수를 사용합니다.

 

이렇게 하면 안됩니다!

function Component() {
  const { data, refetch } = useQuery(['todos'], fetchTodos)

  // ❓ how do I pass parameters to refetch ❓
  return <Filters onApply={() => refetch(???)} />
}

리페치는 동일한 매개변수로 다시 가져오기 위한 것입니다.

데이터를 변경하는 상태가 있는 경우 쿼리 키에 입력하기만 하면 됩니다.
React Query는 키가 변경될 때마다 자동으로 리페치를 트리거하기 때문입니다.
따라서 필터를 적용하려면 클라이언트 상태를 변경하기만 하면 됩니다.
function Component() {
  const [filters, setFilters] = React.useState()
  const { data } = useQuery(['todos', filters], () => fetchTodos(filters))

  // ✅ set local state and let it "drive" the query
  return <Filters onApply={setFilters} />
}​
setFilters 업데이트에 의해 트리거된 리렌더링은 다른 쿼리 키를 React Query에 전달하여 다시 가져옵니다.
#1: Practical React Query - 쿼리 키를 종속성 배열처럼 취급(#1: Practical React Query - Treat the query key like a dependency array)에 더 자세한 예가 있습니다.

캐시와 상호작용

쿼리 캐시와의 수동 상호 작용은 쿼리 키의 구조가 가장 중요한 위치입니다.
invalidateQueries 또는 setQueriesData와 같은 많은 상호 작용 방법이 Query Filters를 지원하므로
쿼리 키를 정확하게, 혹은 유사하게 매칭하여 작업할 수 있습니다.
 

효과적인 쿼리 키

같은 위치에 두기 (Colocate)

아직 Kent C. Dodds의 Maintainability through colocation를 읽지 않았다면 꼭 읽어보세요.
나는 feature 디렉토리에 같은 위치에 있는 각각의 쿼리 옆에 내 쿼리 키를 유지하므로 다음과 같습니다.
- src
  - features
    - Profile
      - index.tsx
      - queries.ts
    - Todos
      - index.tsx
      - queries.ts​

쿼리 파일에는 React Query와 관련된 모든 것이 포함됩니다.

나는 일반적으로 커스텀 훅만 export 하므로, 쿼리 함수와 쿼리 키가 로컬로 유지됩니다.


배열 키 사용

React Query v4부터는 모든 키가 배열이어야 합니다.
// 🚨 will be transformed to ['todos'] anyhow
useQuery('todos')
// ✅
useQuery(['todos'])

 

구조화 (Structure)

가장 일반적인 것부터 구체적인 것으로 쿼리 키를 구조화하십시오.

필터링 기능과 세부 정보 보기를 허용하는 TODO 리스트의 쿼리키 구조 예시입니다.

['todos', 'list', { filters: 'all' }]
['todos', 'list', { filters: 'done' }]
['todos', 'detail', 1]
['todos', 'detail', 2]
이 구조를 사용하면 ['todos']와 관련된 모든 할일, 모든 목록 또는 모든 세부 사항을 무효화할 수 있을 뿐만 아니라
정확한 키를 알고 있는 경우 하나의 특정 핕터된 목록을 대상으로 쿼리를 무효화 할 수 있습니다.
Mutation Responses의 업데이트는 필요한 경우 모든 목록을 대상으로 지정할 수 있기 때문에 훨씬 더 유연해집니다.
function useUpdateTitle() {
  return useMutation(updateTitle, {
    onSuccess: (newTodo) => {
      // ✅ update the todo detail
      queryClient.setQueryData(['todos', 'detail', newTodo.id], newTodo)

      // ✅ update all the lists that contain this todo
      queryClient.setQueriesData(['todos', 'list'], (previous) =>
        previous.map((todo) => (todo.id === newTodo.id ? newtodo : todo))
      )
    },
  })
}​
목록과 세부 정보의 구조가 많이 다른 경우 해당 방법을 적용하기 어려울 수 있으므로, 대신 모든 목록을 무효화할 수도 있습니다.
function useUpdateTitle() {
  return useMutation(updateTitle, {
    onSuccess: (newTodo) => {
      queryClient.setQueryData(['todos', 'detail', newTodo.id], newTodo)

      // ✅ just invalidate all the lists
      queryClient.invalidateQueries(['todos', 'list'])
    },
  })
}​
URL에서 필터를 읽어 정확한 쿼리 키를 구성할 수 있으면, 위의 두 가지 방법을 결합하여
해당 목록에서는 setQueryData를 호출하고 나머지 필터에 대한 쿼리는 모두 무효화 할 수 있습니다.
 
function useUpdateTitle() {
  // imagine a custom hook that returns the current filters,
  // stored in the url
  const { filters } = useFilterParams()

  return useMutation(updateTitle, {
    onSuccess: (newTodo) => {
      queryClient.setQueryData(['todos', 'detail', newTodo.id], newTodo)

      // ✅ update the list we are currently on instantly
      queryClient.setQueryData(['todos', 'list', { filters }], (previous) =>
        previous.map((todo) => (todo.id === newTodo.id ? newtodo : todo))
      )

      // 🥳 invalidate all the lists, but don't refetch the active one
      queryClient.invalidateQueries({
        queryKey: ['todos', 'list'],
        refetchType: 'none',
      })
    },
  })
}

쿼리 키 팩토리 사용

위의 예에서 쿼리 키를 수동으로 많이 선언하였습니다.
이는 오류가 발생하기 쉬울 뿐만 아니라, 예를 들어 키에 다른 수준의 디테일을 추가하려는 경우와 같이 향후 변경을 더 어렵게 만듭니다.

feature당 하나의 쿼리 키 팩토리를 권장합니다.

쿼리 키를 생성하는 항목과 함수가 있는 단순한 개체일 뿐이며 커스텀 훅에서 사용할 수 있습니다.

위의 예제 구조의 경우 다음과 같이 보일 것입니다.

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,
}
각 레벨이 다른 레벨 위에 구축되기 때문에 많은 유연성을 제공하면서도, 여전히 독립적으로 액세스할 수 있습니다.
// 🕺 remove everything related to the todos feature
queryClient.removeQueries(todoKeys.all)

// 🚀 invalidate all the lists
queryClient.invalidateQueries(todoKeys.lists())

// 🙌 prefetch a single todo
queryClient.prefetchQueries(todoKeys.detail(id), () => fetchTodo(id))
반응형