본문 바로가기

FrontEnd

리액트 쿼리 : 렌더링 최적화

반응형

https://tkdodo.eu/blog/react-query-render-optimizations

 

React Query Render Optimizations

An advanced guide to minimize component re-renderings when using React Query

tkdodo.eu

isFetching transition

아래 예제는 길이가 변경되었을 때, 한번 렌더링 되지 않습니다.

export const useTodosQuery = (select) =>
  useQuery(['todos'], fetchTodos, { select })
export const useTodosCount = () => useTodosQuery((data) => data.length)

function TodosCount() {
  const todosCount = useTodosCount()

  return <div>{todosCount.data}</div>
}

다음과 같은 쿼리 정보로 두 번 렌더링 됩니다.

{ status: 'success', data: 2, isFetching: true }
{ status: 'success', data: 2, isFetching: false }
React Query는 각 쿼리에 대해 많은 메타 정보를 노출하고 isFetching이 그 중 하나이기 때문입니다.
이 플래그는 요청이 진행 중일 때 항상 true입니다.
이것은 백그라운드 로딩 상태를 표시하려는 경우에 매우 유용합니다만, 그렇지 않으면 불필요합니다.
 

notifyOnChangeProps

이 사용 사례의 경우 React Query에는 notifyOnChangeProps 옵션이 있습니다.
React Query에 알리기 위해 관찰자별 수준에서 설정할 수 있습니다.
이러한 props 중 하나가 변경되는 경우에만 변경 사항에 대해 이 관찰자에게 알리십시오.
이 옵션을 ['data']로 설정하면 우리가 찾는 최적화된 버전을 찾을 수 있습니다.
export const useTodosQuery = (select, notifyOnChangeProps) =>
  useQuery(['todos'], fetchTodos, { select, notifyOnChangeProps })
export const useTodosCount = () =>
  useTodosQuery((data) => data.length, ['data'])

실전 사용 사례는 optimistic-updates-typescript 에서 찾아보세요

 


Staying in sync

위의 코드는 잘 작동하지만 매우 쉽게 동기화되지 않을 수 있습니다.
오류에도 대응하고 싶다면 어떻게 해야 할까요?
isLoading 플래그도 사용한다면?
우리는 컴포넌트에서 실제로 사용 중인 필드와 동기화된 notifyOnChangeProps 리스트를 유지해야 합니다.
데이터 속성만 관찰할때, 오류가 발생해도 컴포넌트가 다시 렌더링되지 않습니다.
hooks는 컴포넌트가 실제로 무엇을 사용할지 모르기 때문에 사용자 정의 훅에서 이것을 하드 코딩하면 특히 문제가 됩니다.
export const useTodosCount = () =>
  useTodosQuery((data) => data.length, ['data'])

function TodosCount() {
  // 🚨 오류를 사용하고 있지만 오류가 변경되면 알림을 받지 않습니다!
  const { error, data } = useTodosCount()

  return (
    <div>
      {error ? error : null}
      {data ? data : null}
    </div>
  )
}​
이것은 가끔 발생하는 불필요한 리렌더링보다 훨씬 더 나쁘다고 생각합니다.
물론 옵션을 사용자 정의 훅으로 전달할 수 있지만 여전히 수동적이고 상용구처럼 느껴집니다.
이 작업을 자동으로 수행하는 방법이 있습니까? 다음이 있습니다.

Tracked Queries (very good)

notifyOnChangeProps를 'tracked'로 설정하면 React Query는 렌더링 중에 사용 중인 필드를 추적하고,
이를 사용하여 리스트를 자동 계산합니다.
 
이것은 의존성 리스트에 대해 생각할 필요가 없다는 점을 제외하고는 목록을 수동으로 지정하는 것과 똑같은 방식으로 최적화됩니다.
모든 쿼리에 대해 전역적으로 이 기능을 켤 수도 있습니다.
 
업데이트: v4부터 React Query에서 기본적으로 Tracked Queries가 켜져 있으며
notifyOnChangeProps: 'all'을 사용하여 기능을 옵트아웃할 수 있습니다.
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      notifyOnChangeProps: 'tracked',
    },
  },
})
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}​

리렌더링에 대해 생각할 필요가 없습니다.

물론 사용하는 필드를 추적하는 데에도 약간의 오버헤드가 있으므로 현명하게 사용해야 합니다.

또한 Tracked Query에는 몇 가지 제한 사항이 있습니다.

 

구조분해를 쓰지 마세요

// 🚨 will track all fields
const { isLoading, ...queryInfo } = useQuery(...)

// ✅ this is totally fine
const { isLoading, data } = useQuery(...)

쿼리 추적은 렌더링 시에만 동작합니다.

const queryInfo = useQuery(...)

// 🚨 will not corectly track data
React.useEffect(() => {
    console.log(queryInfo.data)
})

// ✅ fine because the dependency array is accessed during render
React.useEffect(() => {
    console.log(queryInfo.data)
}, [queryInfo.data])

필드를 한 번 추적하면 컴포넌트(Observer)의 생명 동안 추적하게 됩니다.

const queryInfo = useQuery(...)

if (someCondition()) {
    // 🟡 we will track the data field if someCondition was true in any previous render cycle
    return <div>{queryInfo.data}</div>
}

 

Structural sharing

React Query가 기본적으로 활성화한 다른, 그러나 덜 중요한 렌더 최적화는 구조적 공유입니다.
이 기능을 사용하면 모든 수준에서 데이터의 참조 ID를 유지할 수 있습니다.
예를 들어 다음 데이터 구조가 있다고 가정합니다.
[
  { "id": 1, "name": "Learn React", "status": "active" },
  { "id": 2, "name": "Learn React Query", "status": "todo" }
]
이제 첫 번째 할일을 완료 상태로 전환하고 백그라운드에서 다시 가져왔다고 가정합니다. 백엔드에서 완전히 새로운 json을 얻습니다.
 
이제 React Query는 이전 상태와 새 상태를 비교하고 가능한 한 많은 이전 상태를 유지하려고 시도합니다.
이 예에서는 todo를 업데이트했기 때문에 todo 배열이 새 것입니다.
id 1을 가진 객체는 새 것이지만, id 2에 대한 객체는 이전 상태의 객체와 동일한 참조가 됩니다.
React Query는 아무 것도 변경되지 않았기 때문에 새 결과에 복사합니다.
[
-  { "id": 1, "name": "Learn React", "status": "active" },
+  { "id": 1, "name": "Learn React", "status": "done" },
  { "id": 2, "name": "Learn React Query", "status": "todo" }
]

 

이는 셀렉터를 사용하여 부분적인 상태 구독 시 유용합니다.
// ✅ will only re-render if _something_ within todo with id:2 changes
// thanks to structural sharing
const { data } = useTodo(2)
이전에 암시했듯이 selector의 경우 구조적 공유가 두 번 수행됩니다.
 
  • queryFn에서 반환된 결과에 대해 변경된 사항이 있는지 확인 후
  • 셀렉터 함수의 결과에 대해 한 번 더 수행됩니다.
 
매우 큰 데이터 세트가 있는 경우 구조적 공유가 병목 현상이 될 수 있습니다.
또한 json 직렬화 가능한 데이터에서만 작동합니다.
이 최적화가 필요하지 않으면 모든 쿼리에서 structureSharing: false를 설정하여 끌 수 있습니다.
자세한 동작은 replaceEqualDeep tests를 살펴보세요.

 

반응형