React Query 렌더링 최적화

이재철·2023년 3월 1일
1

react-query

목록 보기
6/12
post-thumbnail

isFetching transition

이전 섹션에서 React Query의 데이터 변환 에서
데이터의 길이가 변경될 경우에만 다시 렌더링된다고 정의했지만, 기술적으로 사실이 아니다. 🤦

background-fetch 를 다시 실행할 때마다 구성 요소는 다음 쿼리 정보를 두번 re-render 처리합니다.

{ status: 'success', data: 2, isFetching: true, isLoading: true, ... }
{ status: 'success', data: 2, isFetching: false, isLoading: false, ... }

isFetching과 isLoading 무슨 차이?

정의

isFetching

isFetching 은 어떠한 react query 요청 내부의 비동기 함수 처리여부에 따라 true/false 상태로 처리됩니다.
➡ 캐시 유무와 상관없이 데이터를 가져오는 유무만 확인하여 true/false 상태로 처리합니다.

isLoading

isLoading캐시 데이터조차 없이, 처음 실행된 쿼리일 때 loading 에 따라 true/false 상태로 처리됩니다.
➡ 어떤 데이터를 호출하여 웹 브라우저 상에 캐시가 존재한 상태에서 같은 쿼리 키의 데이터 를 중복으로 호출한다면 isLoading 은 무조건 false 상태입니다.

isFetchingisLoading은 비슷하게 loading 개념을 사용하지만 기존 캐시 데이터 여부에 따라 다르게 동작합니다.

언제 구분해서 사용할까?

  • isFetching : 서버에 데이터 요청을 다시 할 경우 (캐시 데이터가 존재할 때)
  • isLoading : 서버에 데이터 요청을 처음 할 경우

notifyOnChangeProps

react query는 notifyOnChangeProps 옵션이 있습니다.
props 중 하나가 변경된 경우에만 해당 observer 에게 변경 사항을 알려주도록 observer 수준에서 설정할 수 있습니다.

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

이렇게 적용하면 useQuery의 response 값의 data 만 변경 된 경우 리렌더링이 됩니다.

Staying in Sync

export const useTodosCount = () =>
  useTodosQuery((data) => data.length, ['data'])

function TodosCount() {
  // 🚨 we are using error, but we are not getting notified if error changes!
  const { error, data } = useTodosCount()

  return (
    <div>
      {error ? error : null}
      {data ? data : null}
    </div>
  )
}

🚨 status를 이용하여 조건부 렌더링을 하는 경우 문제가 발생함
➡ status 의 변경여부를 감지할 수 없음

  • 플래그 값을 사용하는 것이 아닌, notifyOnChangeProps 목록을 컴포넌트에서 실제로 사용하고 있는 필드와 동기화하면 됩니다.

🚨 Custom Hook에서 하드코딩한 경우 실제로 무엇을 사용할지 모르기 때문에 문제가 발생
➡ 동기화 작업을 하지 않고 데이터 속성만 관찰하는 경우 오류가 표시되면, 컴포넌트가 다시 렌더링되지 않으므로 오래된 값 입니다.

Tracked Queries

💡 notifyOnChangePropstracked 로 설정하면 react query는 렌더링 중에 사용중인 필드를 추적하며, 이 필드를 사용하여 목록을 계산합니다.
➡ 목록을 수동으로 지정하는 것과 정확히 동일한 방식으로 최적화되며, 사용자가 고려할 필요가 없습니다.

  • 해당 옵션은 모든 쿼리에 전역적으로 설정할 수 있습니다.

v4 부터는 기본적으로 notifyOnChangeProps: 'tracked' 옵션이 설정되어 있습니다.
➡ notifyOnChangeProps: 'all' 설정하여 옵트아웃 할 수 있습니다.

const queryClient = new QueryClient({
   defaultOptions: {
     queries: {
         notifyOnChangeProps: 'tracked',
     },
   },
})

고려사항

구조분해(destructuring)를 쓰지마세요

일반적인 분해 할당은 괜찮지만, 그 외는 하지마세요.

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

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

추적된 쿼리는 render 중에만 작동합니다.

작동 중인 필드에만 엑세스하면 추적되지 않습니다.
🚨 종속성 배열로 인해 매우 어려운 경우

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])

필드를 한번 추적하면 컴포넌트 생명동안 추적합니다.

추적된 쿼리는 각 render에서 재설정되지 않으므로 필드를 한 번 추적하면 관찰자(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

이 기능을 사용한다면 모든 수준에서 데이터 참조 ID를 유지할 수 있습니다.

[
  { "id": 1, "name": "Learn React", "status": "active" }<,
  { "id": 2, "name": "Learn React Query", "status": "todo" }
]

다음과 같은 데이터 구조에서 첫 번째 todo를 'done' 상태로 전환하고 re-fetch를 한다면, backend에서 새로운 jsonData 값을 얻을 수 있습니다.

[
-  { "id": 1, "name": "Learn React", "status": "active" },
+  { "id": 1, "name": "Learn React", "status": "done" },
  { "id": 2, "name": "Learn React Query", "status": "todo" }
]

react query는 이전 상태와 새 상태를 비교하며, 가능한 많은 이전 상태를 유지합니다.
todo를 업데이트했기 때문에 todos 배열 데이터가 새로워집니다.
id:2 에 대한 개체는 이전 상태의 개체와 동일한 참조가 됩니다.

참조

profile
혼신의 힘을 다하다 🤷‍♂️

0개의 댓글