Frontend

React-Query 정리

pebblepark 2022. 8. 5. 13:45

React Query는 리액트 애플리케이션에서 global state 없이 서버 데이터 가져오기, 캐싱, 동기화 및 업데이트를 쉽게 만들어 주는 서버 상태 관리 라이브러리다.

 

React Query의 등장

React 는 서버에서 데이터를 가져오거나 업데이트하는 명확한 방법을 제공하지 않는다. 따라서 개발자들은 데이터를 가져오기 위한 처리를 따로 해주어야 했다.

일반적으로 컴포넌트에서 hook을 사용하여 데이터를 가져와 상태를 관리하거나, 전역 상태 관리 라이브러리를 사용하여 store에 비동기 데이터를 저장하고 가져왔다.

이때 store 는 전역 상태를 관리하는 저장소로 themelocale 과 같은 client state 관리에는 적합하지만, 서버에서 가져오는 데이터 server state 관리에는 적합하지 않다. 그 이유는 다음과 같다.

Server State는 다음과 같은 특징을 가진다.

  • client 에서 제어하거나 소유하지 않는 원격의 공간에서 관리되고 유지된다.
  • Fetching 이나 Updating 을 위한 비동기 API가 필요하다.
  • 다른 사용자와 공유되고 있으며 사용자가 모르게 다른 사람이 변경할 수 있다.
  • 신경쓰지 않을 경우 애플리케이션이 잠재적으로 오래된 상태(out of date)가 될 수 있다.

Client State와의 비교

- Client에서 소유하며 온전한 제어가 가능하다.
- 초기값 설정이나 변경에 제약이 없다.
- 다른 사람들과 공유되지 않는다.
- Client 내에서 UI/UX 흐름이나 사용자 인터렉션에 따라 변할 수 있다.
- 항상 Client 내에서 최신 상태로 관리된다.

따라서 해당 state 값을 store에서 관리하게 된다면 문제가 발생할 수 있다. 스토어에서 관리하기 어렵거나 애매한 값에 대해 예를 들면 다음과 같다.

  • 캐싱
  • 반복되는 isFetching, isError 등 API 관련 상태값과 비슷한 구조의 통신 코드
  • 동일한 데이터에 대한 여러 요청을 단일 요청으로 중복 제거
  • 백그라운드에서 out of date 데이터 업데이트
  • 최대한 빠르게 업데이트된 데이터 반영
  • 페이지 처리나 지연 로딩 데이터와 같은 성능 최적화
  • 구조적 공유를 통한 쿼리 결과 메모이제이션

위와 같은 문제를 해결하기 위해 React Query를 사용할 수 있다. React Query는 구성없이 즉시 사용이 가능하고 원한다면 config를 커스텀 할 수 있다.

 

React Query 설치

npm

$ npm i react-query
# or
$ yarn  add react-query

CDN

<script src="https://unpkg.com/react-query/dist/react-query.production.min.js"></script>

 

Quick Start

다음은 React Query를 사용하는 간단한 예제이다. 해당 예제에서는 React Query의 3가지 핵심 개념을 간략하게 살펴볼 수 있다.

  • Queries
  • Mutations
  • Query Invalidation
import {
    useQuery,
    useMutation,
    useQueryClient,
    QueryClient,
    QueryClientProvider,
  } from 'react-query'
  import { getTodos, postTodo } from '../my-api'

  // QueryClient를 생성한다.
  const queryClient = new QueryClient()

  function App() {
    return (
      // QueryClientProvider에 생성한 client를 넘겨준다.
      <QueryClientProvider client={queryClient}>
        <Todos />
      </QueryClientProvider>
    )
  }

  function Todos() {
    // useQueryClient hook을 통해 클라이언트에 접근할 수 있다.
    const queryClient = useQueryClient()

    // Queries
    const query = useQuery('todos', getTodos)

    // Mutations
    const mutation = useMutation(postTodo, {
      onSuccess: () => {
        // 해당 쿼리 키 결과 쿼리를 무효화시키고 다시 호출한다.
        queryClient.invalidateQueries('todos')
      },
    })

    return (
      <div>
        <ul>
          {query.data.map(todo => (
            <li key={todo.id}>{todo.title}</li>
          ))}
        </ul>

        <button
          onClick={() => {
            mutation.mutate({
              id: Date.now(),
              title: 'Do Laundry',
            })
          }}
        >
          Add Todo
        </button>
      </div>
    )
  }

  render(<App />, document.getElementById('root'))

React Query 를 사용하기 위해서는 QueryClientProvider로 감싸주어야 한다. 이때 넘기는 client 의 값은 QueryClient 의 인스턴스이다. QueryClient 의 자세한 정보는 공식문서를 살펴보길 바란다.

 

Default Config

React Query의 lifecycle

데이터 상태주기 : isFetching -> fresh -> stale -> inActive -> GC

  1. A 쿼리 인스턴스 mount
  2. 데이터 fetch 후 쿼리 캐시에 A라는 키로 저장
    • 해당 데이터는 fresh 상태에서 staleTime 이후 state 상태로 변경
    • 이때 staleTime은 이 데이터가 최신 데이터임을 증명해주는 시간
  3. A 쿼리 인스턴스 unmount
  4. 쿼리 캐시로 저장된 값은 cacheTime만큼 유지되다가 가비지 콜렉터로 수집된다.
    • 이때 cacheTime이 지나기 전, A 쿼리 인스턴스가 다시 mount되면 fetch가 실행되어 fresh한 값을 가져오기 전까지 캐시 데이터를 보여준다.

StaleTime과 CacheTime

  • staleTime
    • 데이터가 fresh 에서 stale 상태로 변경되는데 걸리는 시간
    • 해당 시간이 지난 데이터는 오래된 데이터로 간주된다.
    • 해당 시간동안에는 쿼리 인스턴스가 새롭게 mount 되어도 데이터 fetch가 일어나지 않는다.
  • cacheTime
    • 쿼리 인스턴스가 unmount 되면서 데이터가 inactive 상태로 변경되었을 때 캐싱 결과로 남아 있는 시간
    • cacheTimestaleTime 과 관계없이 무조건 inactive 된 시점을 기준으로 삭제를 결정한다.
  • default
    • staleTime: 0
    • cacheTime 5 min

refetch 수행

  • 아래의 경우 발생 시 자동으로 refetch 수행
  • default: true
    • refetchOnMount : 마운트됨
    • refetchOnWindowFocus : 창에 다시 초점 맞춤
    • refetchOnReconnect : 네트워크 재연결

retry 수행

  • 실패한 쿼리는 자동으로 3번 재시도, delay는 1000초에서 실패한 횟수와 비례해서 증가
  • default
    • retry: 3
    • retryDelay: attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)

 

QueryKey

Effective React Query Keys 참고

  • array 또는 string
  • 일관성을 유지하기 위해 항상 배열 사용을 권장
  • react-query는 내부적으로 Array로 변환함
// 🚨  결과적으로 ['todos'] 로 변환됨
useQuery("todos");
// ✅
useQuery(["todos"]);

쿼리키 구조

  • 일반적인 쿼리 키에서 구체적인 쿼리 키로 구조화하기
['todos', 'list', { filters: 'all' }]
['todos', 'list', { filters: 'done' }]
['todos', 'detail', 1]
['todos', 'detail', 2]
  • 해당 구조 사용시 모든 목록이나 세부 정보 ['todos']를 무효화 시킬 수 있을 뿐만 아니라 특정 대상 ['todos', 'detail', 1]만 지정할 수 있음 -> Muation Response의 업데이트를 유연하게 처리 가능

쿼리키 비교

  • 쿼리키는 해시값으로 관리
  • 객체의 순서에 관계 없이 모두 동일 해시값
  • 배열의 항목 순서가 바뀌면 다른 해시값
// 모두 동일
 useQuery(['todos', { status, page }], ...)
 useQuery(['todos', { page, status }], ...)
 useQuery(['todos', { page, status, other: undefined }], ...)
// 모두 상이
 useQuery(['todos', status, page], ...)
 useQuery(['todos', page, status], ...)
 useQuery(['todos', undefined, page, status], ...)

쿼리 키 팩토리

  • 쿼리 키를 수동적으로 선언
  • 쿼리 키 팩토리 사용해서 관리 (추천)
const todosKeys = {
  all: ["todos"] as const,
  lists: () => [...todosKeys.all, "list"] as const,
  list: (filters: object | string) =>
    [...todosKeys.lists(), { filters }] as const,
  details: () => [...todosKeys.all, "detail"] as const,
  detail: (id: number | string) => [...todosKeys.details(), id] as const,
};
// 🕺 'todos'와 관련된 모든 쿼리 제거
queryClient.removeQueries(todoKeys.all);

// 🚀 모든 리스트 관련 쿼리 무효화
queryClient.invalidateQueries(todoKeys.lists());

// 🙌 단일 쿼리 prefetch
queryClient.prefetchQueries(todoKeys.detail(id), () => fetchTodo(id));

 

Query Basics

  • 모든 쿼리 결과는 쿼리 키에 종속적 -> QueryClient를 통해 캐시된 값 가져오기 가능
  • isLoading, isError, isSuccess 상태 반환 (status로 받는 것도 가능)
    • isLoading : status === loading
    • isError : status === isError
    • isSuccess : status === isSuccess
  • error, data 정보 반환

 

useQuery

일반적으로 데이터를 조회하는 경우 사용

function usePosts() {
  return useQuery(["posts"], async () => {
    const { data } = await axios.get("https://jsonplaceholder.typicode.com/posts");
    return data;
  });
}

function Posts({ setPostId }) {
  const queryClient = useQueryClient();
  const { status, data, error, isFetching } = usePosts();

  return (
    <div>
      <h1>Posts</h1>
      <div>
        {status === "loading" ? (
          "Loading..."
        ) : status === "error" ? (
          <span>Error: {error.message}</span>
        ) : (
          <>
            <div>
              {data.map((post) => (
                <p key={post.id}>
                  <a
                    onClick={() => setPostId(post.id)}
                    href="#"
                    style={
                      // post 정보가 캐시에 존재하면 스타일 변경
                      queryClient.getQueryData(["post", post.id])
                        ? {
                            fontWeight: "bold",
                            color: "green",
                          }
                        : {}
                    }
                  >
                    {post.title}
                  </a>
                </p>
              ))}
            </div>
            <div>{isFetching ? "Background Updating..." : " "}</div>
          </>
        )}
      </div>
    </div>
  );
}

Enabled option

Disabling/Pausing Queries 참고

  1. enabled: false로 설정
    • refetch로 가져오기 (상태 disabled로 유지됨)
  2. enabledstate 변수 사용
    • enabled 조건에 state변수 사용 -> Lazy Query

상태가 disabled로 유지되면 새 인스턴스가 마운트될 때 쿼리가 백그라운드에서 자동으로 다시 가져오지 않는다. 또한 queryClient의 일반적으로 쿼리를 다시 가져오는 호출인 invalidateQueriesrefetchQueries를 무시한다.


Lazy Queries

function Todos() {
  const [filter, setFilter] = React.useState('')

  const { data } = useQuery(
    ['todos', filter],
    () => fetchTodos(filter),
    {
      // ⬇️ filter가 비어있을 경우에만 disabled
      enabled: !!filter
    }
  )

  return (
      <div>
        // 🚀 filter 적용시 enable 되면서 쿼리 실행
        <FiltersForm onApply={setFilter} />
        {data && <TodosTable data={data}} />
      </div>
  )
}

 

useMutation

일반적으로 데이터를 생성/업데이트/삭제하는 경우 사용

function App() {
    const mutation = useMutation((newTodo) => {
      return axios.post('/todos', newTodo);
    });

    return (
      <div>
        {mutation.isLoading ? (
          'Adding todo...'
        ) : (
          <>
            {mutation.isError ? (
              <div>An error occurred: {mutation.error.message}</div>
            ) : null}

            {mutation.isSuccess ? <div>Todo added!</div> : null}

            <button
              onClick={() => {
                // mutate 호출 했을 때 인자로 넘겨준 함수 실행됨
                mutation.mutate({ id: new Date(), title: 'Do Laundry' });
              }}
            >
              Create Todo
            </button>
          </>
        )}
      </div>
    );
  }

Mutation Side Effects

useMutation(addTodo, {
   onMutate: variables => {
     // mutate 호출되기 전
     // 리턴 값은 context에서 사용할 값 반환
     return { id: 1 }
   },
   onError: (error, variables, context) => {
     // 에러 발생 시
     console.log(`rolling back optimistic update with id ${context.id}`)
     // context.id 의 값은 onMutate의 반환 값에서 가져옴
   },
   onSuccess: (data, variables, context) => {
     // 요청 성공 시
   },
   onSettled: (data, error, variables, context) => {
     // 요청이 성공 또는 에러 발생 시
     // onError/onSuccess 호출 -> onSettled 호출
   },
 })

Optimistic Updates

const queryClient = useQueryClient()

 useMutation(updateTodo, {
   // mutate 함수 호출 시
   onMutate: async newTodo => {
     // 낙관적 업데이트를 덮어쓰지 않도록 쿼리 패치 취소
     await queryClient.cancelQueries('todos')

     // 기존 값 스냅샷
     const previousTodos = queryClient.getQueryData('todos')

     // 새로운 값으로 낙관적 업데이트
     queryClient.setQueryData('todos', old => [...old, newTodo])

     // 스냅샷 값을 담은 컨텍스트 객체를 반환
     return { previousTodos }
   },
   // mutation 실패 시, 컨텍스트 값으로 onMutate에서 한 낙관적 업데이트 롤백
   onError: (err, newTodo, context) => {
     queryClient.setQueryData('todos', context.previousTodos)
   },
   // 성공하거나 실패 시 쿼리 refetch
   onSettled: () => {
     queryClient.invalidateQueries('todos')
   },
 })

 

 

댓글수0