본문 바로가기

FrontEnd

리액트 쿼리 : 상태 관리 라이브러리

반응형

원문 보기 : https://tkdodo.eu/blog/react-query-as-a-state-manager

 

React Query as a State Manager

Everything you need to know to make React Query your single source of truth state manager for your async state

tkdodo.eu

 

TL;DR

리액트 쿼리는 데이터 페치 라이브러리가 아니라, 상태관리 라이브러리 입니다.


비동기 상태 관리 도구

React Query는 비동기 상태 관리자입니다.

Promise를 리턴하는 한, 모든 형태의 비동기 상태를 관리할 수 있습니다.

QueryKey를 통해 해당 상태를 식별하며, 데이터 페칭을 추상화합니다.

export const useTodos = () => useQuery(['todos'], fetchTodos)

function ComponentOne() {
  const { data } = useTodos()
}

function ComponentTwo() {
  // ✅ will get exactly the same data as ComponentOne
  const { data } = useTodos()
}

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <ComponentOne />
      <ComponentTwo />
    </QueryClientProvider>
  )
}
해당 컴포넌트는 컴포넌트는는트리의 모든 위치에 있을 수 있습니다.
동일한 QueryClientProvider 아래에 있는 한 동일한 데이터를 가져옵니다.
React Query는 동시에 발생할 요청도 중복 제거하므로
위의 시나리오에서는 두 컴포넌트가 동일한 데이터를 요청하더라도 네트워크 요청은 하나만 있을 것입니다.

데이터 동기화 도구

React Query는 비동기 상태(또는 데이터 가져오기 측면에서: 서버 상태)를 관리하기 때문에
프런트엔드 애플리케이션이 데이터를 "소유"하지 않는다고 가정합니다.
즉, API에서 가져온 데이터를 화면에 표시하면 해당 데이터의 "스냅샷"만 표시됩니다.
 
따라서 우리가 스스로에게 던져야 할 질문은 다음과 같습니다.
데이터를 가져온 후에도 여전히 정확합니까?
답은 전적으로 우리의 문제 영역에 달려 있습니다.
좋아요와 댓글이 모두 포함된 Twitter 게시물을 가져오면 매우 빠르게 구식(오래된) 게시물이 될 수 있습니다.
매일 업데이트되는 환율을 가져오면 다시 가져오지 않아도 데이터가 얼마 동안은 매우 정확할 것입니다.

React Query는 뷰를 실제 데이터 소유자인 백엔드와 동기화하는 수단을 제공합니다.

이제 에러는 너무 자주 업데이트해서가 아니라, 충분히 자주 업데이트하지 않아 발생합니다.


리액트 쿼리 이전

데이터 가져오기에 대한 두 가지 접근 방식은 React Query와 같은 라이브러리가 구출되기 전에 꽤 일반적이었습니다.

fetch once, distribute globally, rarely update

redux로 많이 해온 일입니다.
전역 상태 관리자에 저장하여 애플리케이션의 모든 위치에서 액세스할 수 있습니다.
우리가 백엔드에 POST 요청을 하면 그때 "최신" 상태를 가져오고, 그 전까지 데이터를 다시 가져오지 않습니다.

fetch on every mount, keep it local

Modal Dialog에서만 필요하므로 Dialog가 열릴 때 바로 가져오지 않겠습니까?
이미 방법을 알고 있습니다: useEffect, 빈 의존성 배열(비명을 지르면 eslint-disable 던지기), setLoading(true) 등....

 

이 두 가지 접근 방식은 모두 차선책입니다.
 
첫 번째는 로컬 캐시를 충분히 자주 업데이트하지 않습니다.
두 번째는 잠재적으로 너무 자주 다시 가져오고 두 번째로 가져올 때 데이터가 없기 때문에 좋지 않은 ux를 제공합니다.
 
그렇다면 React Query는 이러한 문제에 어떻게 접근할까요?

Stale While Revalidate

이전에 들어본 적이 있을 것입니다. React Query가 사용하는 캐싱 메커니즘입니다. 새로운 것은 아닙니다.
오래된 콘텐츠에 대한 HTTP 캐시 제어 확장(HTTP Cache-Control Extensions for Stale Content here)에 대해 읽을 수 있습니다.
데이터가 없다는 것은 일반적으로 로딩 스피너를 의미하고
이것은 사용자에게 "느린" 것으로 인식되기 때문에
오래된 데이터가 데이터가 없는 것보다 낫다는 것입니다.
 
React Query는 데이터가 더 이상 최신 상태가 아닐지라도 데이터(stale한)를 캐시하고 필요할 때 제공합니다.
동시에 해당 데이터의 유효성을 다시 확인하기 위해 백그라운드 다시 가져오기를 시도합니다.

Smart refetches

캐시 무효화는 꽤 어렵습니다.

그렇다면 백엔드에 새 데이터를 다시 요청할 때라고 결정하는 시점은 언제입니까?

확실히 useQuery를 호출하는 컴포넌트가 다시 렌더링될 때마다 이 작업을 수행할 수는 없습니다.

그것은 현대 기준으로도 엄청나게 비쌀 것입니다.

React Query는 리페치를 트리거하기 위한 전략적 지점을 선택합니다.
 

refetchOnMount

ruseQuery를 호출하는 새 컴포넌트가 마운트 될 때마다
React Query는 revalidation을 수행합니다.

refetchOnWindowFocus

브라우저 탭에 초점을 맞출 때마다 다시 가져옵니다. revalidation을 수행할 때 가장 좋아하는 시점입니다.

개발 중에 브라우저 탭을 매우 자주 전환하므로 리페치가 "너무 많다"고 인식할 수 있습니다만,

프로덕션에서는 탭에서 앱을 열어 두었던 사용자가 이제 메일을 확인하거나 트위터를 읽고 다시 돌아왔음을 나타냅니다.

이 상황에서 그들에게 최신 업데이트를 보여주는 것은 완벽합니다.

refetchOnReconnect

네트워크 연결이 끊어졌다가 다시 연결될 떄,
화면에 표시되는 내용을 다시 확인하는 좋은 지점입니다.

마지막으로 앱 개발자가 적절한 시점을 알고 있다면 queryClient.invalidateQueries를 통해 수동 무효화를 호출할 수 있습니다.

이것은 mutation을 수행한 후에 매우 편리합니다.


Letting React Query do its magic

 

 
기본 설정(these defaults)은 네트워크 요청의 양을 최소화하기 위한 것이 아니라 최신 상태를 유지하기 위한 것입니다.
이는 주로 staleTime의 기본값이 0으로 설정되어 있기 때문입니다.
새 컴포넌트 인스턴스를 마운트하면 백그라운드에서 다시 가져옵니다.
특히 동일한 렌더 주기에 있지 않은 짧은 연속 마운트의 경우 이 작업을 많이 수행하면 네트워크 탭에서 많은 가져오기를 볼 수 있습니다.
React Query는 아래와 같은 상황에서 중복을 제거할 수 없기 때문입니다.
function ComponentOne() {
  const { data } = useTodos()

  if (data) {
    // ⚠️ mounts conditionally, only after we already have data
    return <ComponentTwo />
  }
  return <Loading />
}

function ComponentTwo() {
  // ⚠️ will thus trigger a second network request
  const { data } = useTodos()
}

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <ComponentOne />
    </QueryClientProvider>
  )
}
2초 전에 데이터를 가져왔습니다. 다른 네트워크 요청이 발생하는 이유는 무엇인가요? 
— React Query를 처음 사용할 때 정상적인 반응
이럴 경우
  • props를 통해 데이터를 전달하거나,
  • props drilling을 피하기 위해 React 컨텍스트에 넣거나
  • refetchOnMount / refetchOnWindowFocus 플래그를 끄는 것이 좋습니다.
일반적으로 데이터를 props로 전달하는 데 아무런 문제가 없습니다.
그러나 좀 더 실제 상황에 맞게 예제를 약간 수정하면 어떻게 될까요?
function ComponentOne() {
  const { data } = useTodos()
  const [showMore, toggleShowMore] = React.useReducer(
    (value) => !value,
    false
  )

  // yes, I leave out error handling, this is "just" an example
  if (!data) {
    return <Loading />
  }

  return (
    <div>
      Todo count: {data.length}
      <button onClick={toggleShowMore}>Show More</button>
      // ✅ show ComponentTwo after the button has been clicked
      {showMore ? <ComponentTwo /> : null}
    </div>
  )
}
이 예에서 우리의 두 번째 컴포넌트(또 todo 데이터에 의존함)는 사용자가 버튼을 클릭한 후에만 마운트됩니다.
이제 사용자가 몇 분 후에 해당 버튼을 클릭한다고 상상해 보십시오.
 
해당 상황에서 항상 background refetch를 사용해 새 데이터를 가져오는게 적절해 보이나요?
생각보다 조금 늦게 마운트되면, 다시 가져오게 하는게 적절하지 않을까요?
 
앞서 언급한 우회 접근 방식 중 하나를 선택한 경우에는 무조건 다시 가져오지 않습니다.
위 방법을 사용하는 경우 지금의 디폴트 설정으로는 반드시 다시 가져옵니다.

Customize staleTime

해결책은 staleTime을 특정 사용 사례에 대해 편안한 값으로 설정하는 것입니다. 알아야 할 핵심 사항은 다음과 같습니다.

데이터가 최신 상태인 한 항상 캐시에서만 가져옵니다.
즉 데이터를 몇번이고  리액트 쿼리에서 가져와도, 네트워크 요청을 하지 않습니다.
staleTime에 대한 "올바른" 값은 없습니다. 많은 상황에서 기본값이 정말 잘 작동합니다.
개인적으로 요청을 중복 제거하기 위해 최소 20초로 설정하고 싶지만 전적으로 사용자에게 달려 있습니다.

 


Bonus: using setQueryDefaults

v3부터 React Query는 QueryClient.setQueryDefaults를 통해 쿼리 키별로 기본값을 설정하는 훌륭한 방법을 지원합니다.
따라서 #8: Effective React Query Keys에서 설명한 패턴을 따른다면,
쿼리 키의 세분성(granularity)에 맞게 option을 적용할 수 있습니다.
예를 들어 쿼리키를 Query Filters로 다음과 같이 사용할 수 있습니다.
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // ✅ globally default to 20 seconds
      staleTime: 1000 * 20,
    },
  },
})

// 🚀 everything todo-related will have a 1 minute staleTime
queryClient.setQueryDefaults(todoKeys.all, { staleTime: 1000 * 60 })

관심사의 분리에 대한 노트

useQuery와 같은 훅를 앱의 모든 계층의 컴포넌트에 추가하면
컴포넌트가 수행해야 하는 작업의 책임이 뒤섞인 것처럼 보이는 것은 합당한 우려입니다.
예전에는 "스마트 대 바보", "컨테이너 대 프리젠테이션" 컴포넌트 패턴이 어디에나 있었습니다.
 
프레젠테이션 컴포넌트가 "props를 가져오기" 때문에 명확한 분리, 분리, 재사용 및 테스트 용이성을 약속했습니다.
또한 많은 props drilling, 보일러플레이트, 정적 타이핑이 어려운 패턴(👋 HOC) 및 임의 컴포넌트 분할이 발생했습니다.
 
훅이 등장하면서 많이 바뀌었습니다.
 
이제 모든 곳에서 컨텍스트, useQuery 또는 useSelector(redux를 사용하는 경우)를 사용할 수 있으므로
컴포넌트에 종속성을 주입할 수 있습니다.
 
그렇게 하면 컴포넌트에 결합도가 증가한다고 주장할 수도 있습니다만,
하지만 해당 컴포넌트를 앱에서 자유롭게 이동할 수 있고, 컴포넌트가 자체적으로 동작하기 때문에
이제 더 독립적이라고 말할 수도 있습니다.
해당 영상을 추천합니다 : Hooks, HOCS, and Tradeoffs (⚡️) / React Boston 2019
요약하면 모두 절충안입니다.
공짜 점심은 없습니다. 한 상황에서 작동할 수 있는 것이 다른 상황에서는 작동하지 않을 수 있습니다.
재사용 가능한 Button 컴포넌트가 데이터 가져오기를 수행해야 합니까? 아마 아닐 것입니다.
대시보드를 DashboardView와 데이터를 전달하는 DashboardContainer로 나누는 것이 합리적입니까?
이 또한 아마 아닐 것입니다.
 
따라서 장단점을 파악하고 올바른 작업에 올바른 도구를 적용하는 것은 우리의 몫입니다.

요점

React Query는 전역적으로 비동기 상태를 관리하는 데 탁월합니다.
  • 사용 사례에 이해가 되는 경우에만 다시 가져오기 플래그를 끄고,
  • 서버 데이터를 다른 상태 관리자와 동기화하려는 충동을 억제하십시오.

일반적으로 staleTime을 커스터마이징하는 것이

백그라운드 업데이트가 발생하는 빈도를 제어하는 ​​동시에 훌륭한 ux를 얻는 데 필요한 전부입니다.


참고

the OnlineManager

retrying offline mutation

반응형