-
[라이브러리] 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
-
'프로그래밍' 카테고리의 다른 글
[Docker] Docker의 개념 및 활용 (0) 2024.05.15 [git] 팀 프로젝트를 하기 위한 git 사용 전략 (0) 2024.04.18 [Zustand] Zustand란? (0) 2024.03.23