리액트쿼리!!!!!!!!

먼지·2022년 11월 10일
1

React Query

목록 보기
3/4
post-thumbnail

드림코딩 강의 리액트 쿼리 (React Query)

대부분 웹 프론트엔드 애플리케이션은 서버와 데이터통신을 하면서 서버의 데이터를 가져와 보여주는 경우가 많음. 이때 이 데이터를 상태 관리할 수 있게 도와주는 가장 많이 쓰이는 라이브러리

커스텀훅의 문제점

Hooks은 (함수들은) 값의 재사용이 아니라 로직의 재사용을 위한 것이다

이 커스텀훅을 사용하는 컴포넌트마다 loading, error, products 등의 state를 만들고 useEffect를 호출함. 아떤 로직을 이용해 어떤 데이터를 return할 건지만 결정할 뿐 상태를 캐싱하거나 재사용하지 못함. 그러려면 전역으로 관리되는 변수를 만들어야 함

근데 직접적으로 전역 변수를 관리하는 것은 썩 좋지 않은 방법. 이 코드의 문제점은
1. cache
2. retry

React Query는 이런 문제들을 해결!
네트워크 통신도 간편하게 할 수 있고, 로딩 중인지 에러가 발생했는지 데이터를 받아왔는지 쉽게 알 수 있고 여러 컴포넌트에 걸쳐 똑같은 데이터를 네트워크 요청하지 않고 동일한 네트워크 요청이라면 얼마 동안 내가 우리 앱의 메모리상에 캐싱을 할 건지 캐시 시스템, 글로벌 시스템, 네트워크 요청이 실패했다면 조금 있다 다시 시도하는 기능, 네트워크 통신이나 비동기적으로 데이터를 관리하는 경우 유용함

공식 사이트 읽어보기

처음 라이브러리 배울 땐 무조건 공식 문서. 소개 페이지를 꼭 읽기!

강력한 "비동기 상태 관리 라이브러리"로 세부적 상태 관리 시스템, 수동적인 리패칭, 끝없는 비동기 스파게티 코드 우리에게 다 맡겨라. TanStack Query는 너에게 깔끔하고 언제나 최신의 자동으로 관리된 쿼리와 상태를 변경할 수 있는 mutations까지 제공해준다.

  • DECLARATIVE 선언적 & AUTOMATIC 자동적
    직접적으로 데이터를 패칭하는 로직을 작성하지 않아도 된다. 얼마나 신선도를 유지할 건지(캐싱) 얘기하면 나머진 다 자동적으로 될 것이다. 별도로 설정하지 않아도 오래된 데이터를 백그라운드에서 업데이트하는 것까지 해줌
  • SIMPLE & FAMILIAR
    promises와 async/await를 사용할 줄안다면 쉬울 것. global state 관리, reducers, 복잡한 설정 필요 없음
  • EXTENSIBLE
    각각 네트워크 요청별로 조금 더 세부적으로 원한느 것을 설정할 수 있음. devtools와 infinite-loading APIs, 상태 업데이트 툴까지 다 제공함

Overview

리액트에서 데이터 패칭을 해주는 것만 아니라 기술적으로 얘기하면 패칭과 캐싱. 백엔드 서버의 데이터와도 동기화해주고 업데이트해줌.

Motivation

리액트 앱에서 데이터를 패칭할 때 어떤 특정한 라이브러리를 쓰라 정해져있지 않아서 수동적으로 했는데 그때 문제점이 많음

API REFERENCE

useQuery

useQuery에 queryKey와 queryFn(어디서 데이터를 읽어와야 하는지), 옵션 을 전달해 호출하면 어떤 객체를 리턴해주는데 객체 안엔 data, isLoading 이런 키들이 있고,

import { useQuery } from '@tanstack/react-query';

export default function Products() {
  const { isLoading, error, data: products } = useQuery(['products'], async () => {
    console.log('fetching...');
    return fetch(`data/products.json`).then((res) => res.json());
  });
}

리액트 쿼리 별로 키를 제공해 주는데, 네트워크 통신별로 각각 "고유한 키" 이름 아래에 데이터를 메모리에 보관함. 그래서 여러 컴포넌트라도 "동일한 키"의 데이터가 메모리상에 있는지 확인하고 네트워크 통신을 요청함. 있으면 존재하는 캐싱된 데이터를 사용!

GUIDES & CONCEPTS

Query Keys

기본적으로 React Query는 키에 의존해서 캐시를 관리함. 캐싱을 적절히 사용하려면 고유한 키를 사용해야 함. A라는 키를 사용하는 모든 요청에 대해선 동일한 캐시를 사용하고, B란 키에 대해선 네트워크 요청을 또 동일하게 사용함. 키는 배열로 설정할 수 있는데 조금 더 세밀하게 키들의 조합을 만들 수 있기 때문임.

보통은 하나만 사용하지 않고 배열 형태로 원하는 조건별로 세부 상태별로 다양한 키들을 조합해서 사용

// 둘은 엄연히 다른 키로 서로 다른 캐시를 사용함
useQuery(['something'], ...)
useQuery(['something', 'B'], ...)

타입별로 다른 캐시 사용

useQuery(['todos', 5, { preview: true }], ...)
useQuery(['todos', { type: 'done' }], ...)

정확하게 어떻게 동작하는지 확인하지 않고 바로 프로젝트에 적용하면 안 됨.. 리액트 쿼리는 한 가지 함정이 있는데 만약 캐시도 다 되겠지? 하고 넘어가면 정말 캐시된 데이터를 사용하는지 네트워크 통신하는 코드가 호출이 됐는지 확인할 수 없음.

윈도우와 브라우저를 왔다갔다하면 계속해서 fetching이 일어남. 컴포넌트를 숨겼따 보여줄 때도 끊임없이 네트워크 통신이 발생

쿼리 키와 함수만 지정한다고 마법처럼 모든 캐시가 다 잘되지 않음

개발툴

  1. key
    서로 밀접하게 연결된 키들을 배열 형태로 전달하면 내부적으로는 키들을 이용해 해시키를 만들고, 그 해시키를 이용해 데이터를 보관하고 캐시에서 읽어와야 하는지 다시 네트워크에서 다시 받아와야 하는지 관리함.
  2. function
    어디서 데이터를 받아와야 하는지 로직을 작성함. 항상 네트워크에서 받아온 데이터를 반환하도록 async나 promise를 이용해 값을 바로 리턴해야 함. 우리 컴포넌트가 처음으로 마운트되거나 내부적인 상태가 변경돼서 다시 렌더가 될 때도 useQuery를 사용하므로, 키가 동일하고 캐시가 돼있다면 두 번째 인자로 전달된 함수를 통해 네트워크 요청으로 데이터를 받아오지 않고 내부적으로 캐시된 데이터를 사용함

문제는 리액트 쿼리가 너무 빈번하게 데이터를 요청하는 것

데브툴을 설치
https://tanstack.com/query/v4/docs/devtools

$ npm i @tanstack/react-query-devtools
# or
$ pnpm add @tanstack/react-query-devtools
# or
$ yarn add @tanstack/react-query-devtools

App에 추가

import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* The rest of your application */}
      <ReactQueryDevtools initialIsOpen={true} />
    </QueryClientProvider>
  )
}

  • 총 2개의 컴포넌트에서 간직하고 있다
  • stale(노란색) 캐시는 보관이 돼있는데 데이터가 조금 오래됐다고 마크
  • Observers 관찰하는 컴포넌트 개수
  • Last Updated 마지막으로 언제 업뎃됐는지 시간 정보
  • 캐시 데이터를 수동적으로 다시 패치할 건지, 다시 받아올 건지, 리셋, 삭제 가능
  • 쿼리 안에 어떤 데이터가 있는지 확인 가능
  • Qeury Explorer 자세한 옵션 사항들 (얼마 동안 캐시, 초기 상태)

모든 데이터들이 네트워크를 받자마자 stale이라고 마크되어 있음.

Important Defaults

https://tanstack.com/query/v4/docs/guides/important-defaults

공격적이지만 분별력있는 기본 설정값을 가지고 있다. 때때로 이런 기본 행동들 때문에 상황을 잘 모르는 사용자들이 어려워하고 디버깅하기 힘들어 한다. 그래서 이것등를 꼭 명심했으면 좋겠다.

  • Query instances는 useQueryuseInfiniteQuery를 사용할 수 있는데 기본적으로 이 두 개를 사용하면 캐시된 데이터는 stale(오래된, 신선하지 않은)이라고 간주한다.

    이 기본적인 행동을 변경하기 위해서는, 우리가 쿼리를 사용할 때 글로벌이나 쿼리 별로 staleTime 옵션을 설정하면 됨. 한 번 네트워크 상에서 받아온 데이터를 얼마 동안 신선하다(동일한 네트워크 요청을 했을 때 또다시 새롭게 네트워크 요청을 하지 않고 캐시된 데이터를 사용) 간주할 건지

stale이라 마크된 쿼리들은 계속 백그라운드에서 자동적으로 refetch(네트워크 통신) 됨.

  • 새로운 쿼리가 만들어졌을 때(mount 됐을 때) = 컴포넌트가 다시 쿼리를 이용했을 때
  • 윈도우가 자동으로 포커스 됐을 때
  • 네트워크가 다시 연결됐을 때
  • 쿼리가 refetch interval 설정됐을 때 (몇 초 간격으로 refetch 할지 설정)
    이럴 때 refetch

이런 기본적인 쿼리 설정들을 이해해야 실수하지 않고 원하는 대로 사용할 수 있음!

만약 예상하지 않은 refetch 상황에 맞닥뜨린다면 아마 "윈도우가 다시 포커스" 돼서 리액트 쿼리가 refetchOnWindowFocus를 한다. 얘가 기본적으로 설정돼있음. 개발하는 단계에선 이런 리패치를 더 많이 목격할 텐데 왜냐면 브라우저 개발 툴과 우리의 앱을 전환하는 것도 다 윈도우 재포커스로 해석돼기 때문에 그때마다 refetch가 발생하는 것.

"이런 기본적인 행동을 바꾸려면, 컴포넌트가 마운트 될 때마다 무조건 리패치하는 refetchOnMount, 윈도우가 포커스 될 때 refetchOnWindowFocus, 인터넷이 커넥됐을 때 refetchOnReconnect, 몇 초 간격 refetchInterval 옵션을 꺼야 함"

  • 더 이상 useQueryuseInfiniteQuery를 사용하는 사람이 없다면 inactive 상태로 표기가 되는데 5분 정도 지나도 아무도 참조하지 않으면 자동으로 garbage collected가 된다. (메모리를 자동으로 청소해서 캐시에서 사라짐)

"이걸 바꾸려면, cacheTime 옵션을 1000 * 60 * 5 milliseconds 5분보다 조금 더 긴 시간으로 설정하면 됨."

Queries는 네트워크 통신을 실패하면 3번 재시도함. 근데 한 번 실패할 때마다 5초 기다렸다면 두 번째 시도는 조금 더 오래 기다리고, 세 번째 시도할 땐 조금 더 간격을 두고 재시도함.

"이것도 retryretryDelay로 설정할 수 있다."

무조건 튜토리얼만 보고 사용하지 않고 공식 사이트에서 여러 정보들을 읽고 생각하면서 개발하는 것이 좋음

그래서 앞에서 바로 stale로 바뀌는 문제는 staleTime을 조금 더 늘려주면 해결할 수 있음. 세 번째 인자로 옵션을 전달

import { useQuery } from '@tanstack/react-query';

export default function Products() {
  const { isLoading, error, data: products } = useQuery(['products'], async () => {
    console.log('fetching...');
    return fetch(`data/products.json`).then((res) => res.json());
  }, {
    staleTime: 5000 // 5초가 지나면 자동적으로 stale 됨 
  });
}

그래서 "fresh" 상태에서 아무리 요청을 많이 해도 5초 동안은 fecthing이 안 되다가 "stale"로 넘어가는 순간 패칭됨.

내가 계속 stale 하게 놔두고 싶지 않다면
1000 (1초) 60 (1분) 5 => 5분 동안 데이터를 캐싱

어떤 네트워크 요청이냐에 따라 얼마나 빈번히 업데이트되느냐에 따라서 이런 민감도를 잘 판단해 얼마나 fresh 상태를 유지할 건지, 얼마 동안 캐시를 보관할 건지, 얼마 동안 retry를 할 건지 세밀하게 설정할 수 있음

Stale에 대해

우리 애플리케이션에서 서버에 빈번히 수동적으로 네트워크 요청을 하는 것이 아닌 ui react 앱에서 쿼리를 이용해 네트워크 통신을 하도록 만들었음

export default function Products() {
  const { isLoading, error, data: products } = useQuery(['products'], async () => {
    return fetch(`data/products.json`).then((res) => res.json());
  }, {
    staleTime: 5000
  });
}

리액트에서 쿼리에게
UI 에서 key는 products고 제품이 있는지 쿼리를 했는데 ->
Query 에 캐시 된 데이터가 없다면 서버에 네트워크 통신을 해서 제품의 정보를 요청하고 ->
Server 서버로부터 받아온 데이터가 있다면 쿼리 내부 캐시 시스템에 저장함
저장할 땐 key라는 이름에 해당하는 데이터를 저장하고, 별도로 우리가 설정하지 않는 이상 쿼리 내부에 기본적으로 설정된 stale time은 0초, 캐시가 유지되는 cache time은 5m
그러고 받아온 데이터를 ui 상에 보여줌

몇 초가 흐르고..
다시 UI 상에서 products 있어? 쿼리에게 물어보면
Query 안에 key: products인 캐시된 데이터가 있으면 바로 캐시된 데이터를 ui 상에서 업데이트해줌.
보여주긴 했지만 신선하지 않다고 표기된 stale 마크가 있음. 그래서 쿼리 자체적으로 백그라운드에서 네트워크에게 제품을 달라고 요청함.
서버로부터 받아온 데이터를 캐시 데이터에 업데이트. 그리고 0초 있다 또 stale 마크
그리고 업데이트된 데이터가 있다면 ui상으로 다시 업데이트됐어! 하고 보여줌

정리하자면
특정 key에 해당하는 쿼리가 이뤄졌을 때 Query에 해당되는 캐시된 데이터가 있고 fresh 상태면 캐시 상의 데이터를 즉각적으로 ui 상으로 업데이트해주고, stale 이면 네트워크 상으로 요청해서 신선한 데이터를 받아와서 바로 ui에 알려줌

우리가 리액트를 사용하기 때문에 처음에 받앗던거와 네트워크로 다시 받아온 데이터 중에 차이점이 있다면 차이점이 있는 부분만 즉각적으로 화면 업데이트되고, 이전 상태와 동일하다면 ui 상으로 업뎃되지않음

업데이트하기

staleTime을 증가시켜서 오랫동안 우리의 애플리케이션에 캐시를 하게 만들어뒀음. 이런 걸 선택할때도 무조건 길게하기보단 데이터가 백엔드에서 빈번히 업데이트되는지, 사용자가 오래 캐시를 해도 괜찮은지 등을 잘 판단해서 근거해 시간을 적절히 지정해줘야 함

만약 이 제품을 새로운 제품을 추가했다면 네트워크에 POST 요청을 해야 함. 그럼 화면상 데이터는 오래된 것. 이럴 땐 방금 데이터가 업뎃됐으니 이거랑 연관있는 모든 캐시는 invalidate 해달라 명령할 수 있음.

버튼을 하나 만들어서 클릭하면 캐시를 전부 invalidate하게 만들기

import { useQuerym, useQueryClient } from '@tanstack/react-query';

export default function Products() {
  const { isLoading, error, data: products } = useQuery(['products'], async () => {
    return fetch(`data/products.json`).then((res) => res.json());
  }, {
    staleTime: 5000
  });
  
  // App에서 QueryClientProvider 우산을 쓰는 모든 자식 컴포넌트들에서
  // useQueryClient를 이용해서 client를 가져올 수 있음
  const client = useQueryClient();
  
  return (
    <main>
      ...
      <button 
        onClick={() => {
          // 이거와 관련된 모든 쿼리는 invalidate 해줘
          client.invalidateQueries(['products', false]);
      	}}
      >
        정보가 업데이트 되었음!
      </button>
    </main>
  );
}

우리가 서버에 어떤 특정한 정보를 업데이트했다면 해당하는 키에 한해서 캐시를 새로고침할 수 있음. 키의 역할이 정말 중요

profile
꾸준히 자유롭게 즐겁게

0개의 댓글