프로그래밍

[라이브러리] React-query

고래강이 2024. 2. 15. 15:57

📋 개요

  • 비동기 상태 라이브러리를 채택하는 이유
  • Get Started
  • 페이징 처리 기법
  • Optimistic Update 구현

 

 


✅ 비동기 라이브러리를 채택하는 이유

 

1️⃣  등장 배경

  • 기존의 상태 관리 라이브러리는 클라이언트 상태 작업에 적합하지만 서버 상태는 다르기에 비동기, 서버 상태 작업에 적합한 라이브러리가 필요해졌다.
  • 데이터를 비동기적으로 가져오고, 캐싱, 동기화, 업데이트 관리의 복잡성을 해결하고, 사용자 UI와의 상태를 동기화하는 과정을 보다 쉽고 효율적으로 하기 위해 등장하게 되었다.

 

 

2️⃣ React-Query

TanStack 팀에서 만든 라이브러리로 데이터 캐싱 및 기능적으로 더 많은 제어가 필요한 경우에 좋다.(기존 React 뿐만아니라 다양한 프레임워크에서 사용이 가능해 TanStack Query로 명칭이 변경되었다.

 

📦 특징 및 장점

  • 서버 데이터를 관리하는 핵심 객체인 QueryClient에 대한 초기 설정이 필요하다.
  • Root 파일에서 QueryClientProvider 하위 컴포넌트를 감싸 적용할 수 있다.
  • 광범위한 쿼리 무효화 및 업데이터 옵션을 제공하여 데이터 업데이트 전략에 있어 더 많은 제어와 유연성을 제공한다.
  • 개발자 도구를 통해 현재 캐시된 쿼리, 쿼리 상태, 백그라운드 업데이트 등을 시각적으로 확인할 수 있다.

 

📦 단점

  • 다소 복잡하고 많은 API의 개념으로 인해 러닝 커브가 상대적으로 높다.
  • 복잡한 캐싱 전걍이나 데이터 동기화를 사용하는 경우, 많은 설정과 구성을 필요로 할 수 있다.

 

 


✅ Get Started

 

1️⃣ istall

📦 NPM

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

 

📦 권장 사항 🔗

$ npm i -D @tanstack/eslint-plugin-query
# or
$ pnpm add -D @tanstack/eslint-plugin-query
# or
$ yarn add -D @tanstack/eslint-plugin-query
  • 자체적으로 제공되는 eslint-plugin으로 일반적인 실수를 방지하는데 도움이 되며, 확장 기능을 추가해서 사용해야 한다.
{
  "extends": ["plugin:@tanstack/eslint-plugin-query/recommended"]
}

 

2️⃣ Quick Start 

📦 Query

서버에서 데이터를 가져오기 위해 Promise 기반 메서드와 함께 사용할 수 있으며 고유 키에 비동기 데이터 소스를 연결하여 종속성을 가진다. 데이터를 수정하는 경우 Mutations를 사용하는 것이 권장된다.

 

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

function App() {
  const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}
  • 쿼리키, fetcher함수, Option 인자를 사용할 수 있다.
  • 이렇게 useQuery를 통해 생성된 개체에는 생산성을 높이는 몇가지 상태(status)가 포함되어 있으며, 그 외 쿼리 상태에 따른 더 많은 정보도 포함되어 있다.
[Status]
isPending:
아직 데이터가 없다. (기존 isLoading)
isError: 쿼리에 오류가 발생.
isSuccess: 데이터를 사용할 수 있다.
isFetching: fetcher함수가 실행될 때마다 true를 반환
 

data: isSuccess를 통해 사용할 수 있는 데이터
error: isError를 통해 알 수 있는 오류

 

🎉 자주 쓰일 Option 🔗

더보기

enabled: 특정 조건을 만족했을 때 fetcher를 실행시키게끔 할 수 있다. (조건부 데이터 가져오기)

initialData: 쿼리가 아직 생성되지 않았거나 캐시되지 않은 경우 초기값으로 사용된다.

staleTime: 기본값이 0으로 데이터가 오래된 것을 간주된 후의 시간으로 infinity로 설정시 오래된 것으로 간주하지 않습니다.

refethOnWindowFocus: 기본값이 true로 false로 설정하면 브라우저 창을 전환시에 데이터를 다시 가져오지 않는다.

 

📦 Mutation

일반적으로 데이터를 생성 / 업데이트 / 삭제 또는 서버 사이드 이펙트를 수행하는데 사용된다. 

 

function App() {
  const {mutate, isPending, isError, isSuccess, error} = useMutation({
    mutationFn: (newTodo) => {
      return axios.post('/todos', newTodo)
    },
  })

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

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

          <button
            onClick={() => {
              mutate({ id: new Date(), title: 'Do Laundry' })
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  )
}
  • 쿼리와 비슷하게 다양한 상태 및 정보를 가지고 있다. 

 

📦 Invalidate

특정 쿼리를 무효화하여 data를 stale상태로 만들어 data의 refecthing을 일으키게 하는 경우

 

import { useState } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { updateProfile } from './api';

const Profile = () => {
  const queryClient = useQueryClient();
  const [profile, setProfile] = useState({ nickName: 'jiyaho' });
  const userId = 'yourUserId'; // userId 값을 적절히 설정

  const handleEditProfile = async () => {
    // 프로필 업데이트 로직 (예: 서버 API 호출 또는 상태 업데이트)
    await updateProfile(profile);

    // 해당 유저의 데이터를 무효화(invalidate) 시키고 다시 가져오기(refetch)
    queryClient.invalidateQueries(`/user/${userId}`);
  };

  return (
    <div>
      {/* 프로필 수정 UI */}
      <input
        type="text"
        value={profile.nickName}
        onChange={(e) => setProfile({ nickName: e.target.value })}
      />
      <button onClick={handleEditProfile}>Edit Profile</button>
      
      {/* 여기에서 유저 정보를 출력하거나 UI 표시 가능 */}
    </div>
  );
};

export default Profile;

 

📦 revalidate

사용자가 캐시된 쿼리의 데이터에 대해서 업데이트를 하기 위한 목적으로 무효화전략이 아닌 재검증 전략을 통해 불필요한 네트워크 요청을 추가로 할 필요 없이 즉각 업데이트를 할 수 있다. 다만 서버에 직접 반영이 되는 것이 아닌 클라이언트 캐시에만 영향을 준다.

 

import { useState } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { updateProfile } from './api';

const Profile = () => {
  const queryClient = useQueryClient();
  const [profile, setProfile] = useState({ nickName: 'jiyaho' });
  const userId = 'yourUserId'; // userId 값을 적절히 설정

  const handleEditProfile = async () => {
    // 프로필 업데이트 로직 (예: 서버 API 호출 또는 상태 업데이트)
    await updateProfile(profile);

    // 해당 유저의 프로필 업데이트
    queryClient.setQueryData(`/user/${userId}`, profile);
  };

  return (
    <div>
      {/* 프로필 수정 UI */}
    </div>
  );
};
export default Profile;

 

 

 


✅ 페이징 처리 기법

 

TanStack Query의 페이징처리 기법을 통해 페이지네이션 처리와 더 나아가 인피니티 스크롤을 구현하기 위해 사용되는 hook인 useInfiniteQuery 기본적인 사용법을 알아보자.

 

1️⃣ useInfiniteQuery

const {
	data,
	hasNextPage,
	fetchNextPage,
    fetchPreviousPage,
    hasPreviousPage,
	isFetching,
} = useInfiniteQuery(고유키, fetcher함수, {
	initialPageParam: 1,
	getNextPageParam: (lastPage)=> lastPage.nextCursor,
    getPreviousPageParam: (firstPage) => firstPage.PrevCursor,
})
  • useQuery와 받는 인자는 동일하다.
  • 옵션에서 getNextPageParam과 getPreviousPageParam을 통해 페이지에 대한 정보를 전달함으로써 이전 혹은 이후의 페이지에 대한 데이터를 가져올 수 있게 된다.
  • 이후의 값을 불러오기 위해 fetchNextPage를 호출하는 로직을 통해 이후의 값을 가져올 수 있으며 fetchPreviousPage를 호출하는 로직을 통해 이전의 값을 가져올 수 있다.

 

2️⃣ 페이지네이션과 인피니티 스크롤의 차이점

함수를 호출하는 Trigger의 차이라고 생각되며 Observer pattern을 통해 인피니티 스크롤을 구현하는 방법이 있다.

 

 


✅ Optimistic Update

 

단순히 Mutations를 통해서 update를 하게된다면 사용자의 요청이 완료되고 업데이트 된 데이터가 화면에 그려지는 순서로 이루어지는데 이렇게되면 요청시간만큼의 딜레이가 UX를 떨어뜨리게 되므로 사용자의 인터렉션에 대해 즉각적으로 화면에 반영된 후 요청의 성공 / 실패 여부에 따라 이전 상태로 되돌릴지 현재를 유지할지 결정하는 방법으로 낙관적으로 성공할 것을 생각하고 미리 UI에 반영한다는 의미이다.

 

1️⃣ useMutation + queryClient

useMutation 훅을 통해 데이터를 수정하는 API를 호출하여 낙관적 업데이트 로직인 onMutate를 통해 임시로 UI를 업데이트 하고, 롤백로직인 onError를 통해 에러가 발생했을 시 이전 상태로 롤백한다. 이후 성공시에 캐시 데이터를 수정해야하므로 onSettle 또는 onSuccess를 통해서 캐시를 새롭게 고친다.

 

useMutation({
  mutationFn: updateTodo,
  // onMutate를 통해 낙관적으로 보여줄 UI에 대해서 처리를 한다
  onMutate: async (newTodo) => {

	// 재요청(refech)중인 작업이 있다면 이를 취소해서 낙관적 업데이트 된 UI 상태를 덮어쓰는 것을 방지 
    await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })

    // reject시에 되돌려야하므로 이전 값을 저장
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])

    // 낙관적으로 값을 업데이트 한다.(캐시 데이터를 바꾼다)
    queryClient.setQueryData(['todos', newTodo.id], newTodo)

    // 이전 값과 새로운 값을 포함하는 컨텍스트를 반환한다. 추후 onError나 onSettled에 사용하기 위해
    return { previousTodo, newTodo }
  },
  // 요청이 실패시 onMutate를 통해 반환받은 값을 사용해서 이전 상태로 되돌린다
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(
      ['todos', context.newTodo.id],
      context.previousTodo,
    )
  },
  // 에러나 성공 후에는 항상 재요청을 하며 이부분을 onSuccess로 바꿔도 상관없다.
  onSettled: (newTodo) => {
    queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] })
  },
})
  • queryClient는 캐시된 쿼리 데이터를 관리하는 객체로 캐시데이터를 업데이트 함으로 낙관적으로 업데이트 된다.
  • 이후 마지막 invalidatedQueires를 통해 현재 캐시 데이터를 무효화 시켜 새롭게 서버로 데이터를 받아오게끔함으로 서버 데이터와 캐시 데이터를 일치시킨다.

 

 

 

 

 

 

 

 

📌 reference

- TanStack Query Docs 

- React-query vs SWR