본문 바로가기

FrontEnd

리액트 쿼리 : 반드시 알아야 하는 상식

반응형

https://tkdodo.eu/blog/practical-react-query

 

Practical React Query

Let me share with you the experiences I have made lately with React Query. Fetching data in React has never been this delightful...

tkdodo.eu

해당 게시물을 요약정리한 글임

리액트 쿼리와 서버 상태

Apollo Client는 서버 데이터와 서버 데이터 캐시를 이용한,

서버 사이드 상태 관리의 신기원을 열었음.

 

이는 수 많은 전역 상태의 클라이언트 측 (중복) 관리 부담을 줄여줬으나,

GraphQL을 도입하기 위해선 백엔드의 조정이 가장 큰 장벽이었고,

오버페칭, 언더페칭을 포함한 다른 그래프큐엘의 장점을 살리기 위해 백엔드를 전환하는게 ROI가 나오는가가 의문이었음.

rest api에 대해 해당 기능을 제공하고자 해서 탄생한게 리액트 쿼리임


리액트 쿼리의 디폴트 동작

React Query는 기본 staleTime이 0인 경우에도 다시 렌더링할 때마다 queryFn을 호출하지 않습니다

앱은 언제든지 다양한 이유로 다시 렌더링될 수 있으므로 매번 가져오는 것은 미친 짓입니다.

항상 리렌더링을 위한 코딩을 하세요. 나는 그것을 렌더링 탄력성이라고 부르고 싶습니다.
— 태너 린슬리 : 리액트 쿼리 창시자.

예상치 못한 리페치는 refetchOnWindowFocus 때문일 수 있습니다.

사용자가 다른 브라우저 탭으로 이동했다가 다시 돌아오는 경우 백그라운드 리페치가 자동으로 트리거되고

그 동안 서버에서 변경 사항이 있으면 화면의 데이터가 업데이트됩니다.

이 모든 것은 로딩 스피너가 표시되지 않고 발생하며

데이터가 현재 캐시에 있는 것과 동일한 경우 컴포넌트가 다시 렌더링되지 않습니다.

 

개발하는 동안 이것은 아마도 더 자주 트리거될 것입니다.

특히 Browser DevTools와 앱 사이를 왔다갔다 하면서 포커싱하면 더 자주 발생합니다.


cacheTime과 staleTime

  • StaleTime: 쿼리가 최신 상태에서 stale한 상태로 전환될 때까지의 기간입니다.
    • 기본값은 즉시(0)입니다.)
    • 쿼리가 최신 상태인 한 데이터는 항상 캐시에서만 읽힙니다.
    • 네트워크 요청은 발생하지 않습니다!
    • 쿼리가 stale한 경우 경우, 캐시에서 데이터를 계속 가져오지만 특정 조건(under certain conditions)에서 백그라운드 리페치가 발생할 수 있습니다.
      • 위 문서에서 특정 조건이라 함은, 해당 쿼리를 사용하는 다른 컴포넌트가 렌더링되면, 한 번의 요청을 추가로 수행하여 stale한 쿼리를 갱신한다 함.
  • CacheTime
    • 기본값은 5분입니다. 
    • 비활성 쿼리가 캐시에서 제거될 때까지의 기간입니다. (가비지 컬렉팅 - 구현은 delete로 되어있음)
    • 등록된 옵저버가가 없는 즉시 쿼리가 비활성 상태로 전환됩니다.
      • 해당 쿼리를 사용하는 모든 컴포넌트가 마운트 해제되었을 때를 의미합니다.

즉, 대부분의 경우 staleTime을 조정해야 합니다.

나는 cacheTime을 조작할 필요가 거의 없었습니다.

문서에도 예제로 좋은 설명(explanation by example)이 있습니다. 

 


Devtool 사용하기

현재 캐시에 있는 데이터를 보여줘 좋음.

리페치 발생 시의 영향도 파악을 위해 브라우저 개발도구에서 네트워크 쓰로틀을 거는게 좋음.


쿼리 키를 종속성 배열처럼 생각하기

필터 등 쿼리 변수의 변경과 쿼리 함수 호출의 동기화

type State = 'all' | 'open' | 'done'
type Todo = {
  id: number
  state: State
}
type Todos = ReadonlyArray<Todo>

const fetchTodos = async (state: State): Promise<Todos> => {
  const response = await axios.get(`todos/${state}`)
  return response.data
}

export const useTodosQuery = (state: State) =>
  useQuery(['todos', state], () => fetchTodos(state))

새로운 캐시 엔트리

쿼리키는 캐시를 위한 키로 사용되기 때문에, 처음 all > done 전환 시 로딩 스피너를 보여줍니다.

이는 사용자에게 이상하게 느껴질 수 있습니다.

keepPreviousData 옵션을 사용하거나, 가능하다면 initialData를 제공합니다.

몇 줄 안되는 코드로 UX를 크게 개선할 수 있습니다.

type State = 'all' | 'open' | 'done'
type Todo = {
  id: number
  state: State
}
type Todos = ReadonlyArray<Todo>

const fetchTodos = async (state: State): Promise<Todos> => {
  const response = await axios.get(`todos/${state}`)
  return response.data
}

export const useTodosQuery = (state: State) =>
  useQuery(['todos', state], () => fetchTodos(state), {
    initialData: () => {
      const allTodos = queryClient.getQueryData<Todos>(['todos', 'all'])
      const filteredData =
        allTodos?.filter((todo) => todo.state === state) ?? []

      return filteredData.length > 0 ? filteredData : undefined
    },
  })

서버 상태와 클라이언트 상태의 분리

useQuery로 가져온 데이터를 로컬에서 관리하지 마십시오

가장 큰 이유는 이 복사된 상태가 reactQuery의 서버 상태와의 자동 동기화에서 벗어나기 때문입니다.

로컬 사본이 없으면 항상 최신 값을 볼 수 있습니다.

 

만약 form의 초기값을 서버에서 가져오고, 이후에 절대 변경할 필요가 없다면, 다음과 같이 설정합니다.

(staleTime을 infinity로)

const App = () => {
  const { data } = useQuery('key', queryFn, { staleTime: Infinity })

  return data ? <MyForm initialData={data} /> : null
}

const MyForm = ({ initialData} ) => {
  const [data, setData] = React.useState(initialData)
  ...
}

위 설정이 없으면 어떻게 동작하는지 확인하고, staleTime을 설정해 보세요


옵션의 enabled 속성은 매우 강력합니다.

아래와 같은 것을 할 수 있습니다.

  • 다른 쿼리에 의존하는 쿼리
  • 쿼리를 끄고 켜기
    • refetchInterval 덕분에 정기적으로 데이터를 폴링하는 쿼리가 하나 있을 때, 모달이 열려 있으면 화면 뒤에서 업데이트를 피하기 위해 일시적으로 일시 중지할 수 있습니다.
  • 사용자 입력을 기다리기
    • 쿼리 키에 일부 필터가 존재하지만, 사용자가 필터를 입력하지 않는 경우, 쿼리 실행을 지연합니다.
  • 일부 사용자 입력 후 쿼리 비활성화
    • 예를 들어 서버 데이터보다 우선해야 하는 draft 값이 있는 경우.
      • 위의 예를 참조하세요

queryCache를 로컬 상태 관리자로 사용하지 마세요.

queryCache(queryClient.setQueryData)를 변경하는 경우는 다음과 같습니다.
  • 낙관적 업데이트
  • 뮤테이션 후 백엔드에서 수신한 데이터 쓰기 
모든 백그라운드 리페치가 해당 데이터를 재정의할 수 있으므로 로컬 상태를 위해선 다른 도구를 사용하세요.

커스텀 훅 만들기

하나의 useQuery 호출을 래핑하기 위한 것일지라도 커스텀 훅을 만드는 것은 효과가 있습니다.
  • 실제 데이터 페칭 호출을 ui에서 분리해, useQuery와 같이 둡니다.
  • 하나의 쿼리 키(및 잠재적으로 타입 정의)의 모든 사용을 하나의 파일에 보관할 수 있습니다.
  • 일부 설정을 조정하거나 데이터 변환을 추가해야 하는 경우 한 곳에서 수행할 수 있습니다.
위의 todo 쿼리 예제를 참조하세요.
반응형