본문 바로가기
React

리액트-쿼리 / TanStack Query(@tanstack/react-query) - 2

by LucetTin5 2023. 3. 16.

5. Mutations
 
- Mutations는 무엇일까?
Mutation은 개발자가 서버에서 데이터를 생성하거나 수정하거나 삭제하는 등의 데이터 변경/수정 작업을 처리할 수 있도록 해주는 React-Query의 핵심 개념이다. 리액트 애플리케이션에서 위의 작업들에 해당하는 Side-effect를 수행하고 로컬 캐시를 업데이트하여 클라이언트 측의 데이터가 서버와 동기화 상태를 유지하도록 하는 데 사용된다.
Mutation을 이해하기에 앞서 Mutation은 Query와 어떻게 다른 지 알아보면 다음과 같다. 
- 데이터 연산: Query는 데이터를 가져오고 읽는 데 중점을 두고 있으나 Mutation은 생성, 업데이트, 삭제와 같은 작업을 통해 데이터를 수정하는 역할을 담당한다.
- Idempotence(멱등성): Query는 결과를 변경하지 않고 여러 번 실행될 수 있는 멱등적인 성질을 갖는다. 반면 Mutation은 실행될 때마다 데이터가 변경되는 비-멱등적인 성질을 갖는다.
- 에러 핸들링과 재시도: Query에는 에러시 자동 재시도가 이점을 갖지만, Mutation의 경우 수동 에러 핸들링과 재시도 메커니즘을 필요로 한다.
- 캐시 관리: Query는 Query Key를 사용하여 데이터를 캐시하고 관리한다. 반면 Mutation은 선택적으로 mutation 키를 사용하여 mutation의 상태와 캐시를 관리하게 된다.
- HTTP 요청: Query는 일반적으로 데이터를 가져오기 위하여 HTTP GET 요청에 의존한다. 하지만, Mutation은 POST, PUT, PATCH, DELETE와 같은 HTTP methods를 사용하여 데이터 수정 작업을 수행하게 된다.
Query, Mutation을 구별하여 사용하는 것은 리액트 쿼리를 이용하여 작업하는 경우에 필수적이다. 리액트 쿼리 시스템 내에서 Query와 Mutation의 고유한 특성과 역할을 파악하여 사용한다면 이 라이브러리의 기능을 효과적으로 활용하여 리액트 애플리케이션에서 서버 측 상태를 관리할 수 있다.
 
- Mutation을 만들기
리액트 쿼리의 mutation을 시작하려면 useMutation 훅을 사용해야 한다. 이 훅은 mutation function을 인자로 받고 mutation에 관련된 값과 함수의 집합을 반환하게 된다.

먼저 위의 예시처럼, @tanstack/react-query 라이브러리에서 useMutation 훅을 불러와야 한다.

다음은 mutation function을 정의해야 한다. 이 함수는 일반적으로 API 호출을 통해 서버에서 실제 데이터의 수정을 처리해야 한다. 위 예시는 /api/items라는 엔드포인트에 POST요청을 보내는 addItem이라는 함수이다.

이제 우리는 addItem이라는 함수를 가지고 있다. 다음은 useMutation 훅에 이 함수를 인자로 넘겨 mutation을 생성하면 된다. 이 mutation은 mutation의 state, isLoading, error와 같은 states, mutate, mutateAsync와 같은 함수들이 포함된 객체를 반환한다. 컴포넌트 내부에서 이런 값과 함수를 이용하여 mutation 상태를 처리하고 mutation을 트리거할 수 있다.

위 예시는 새로운 Item을 추가하기 위한 간단한 form component로, useMutation 훅의 결과 오브젝트에 포함된 mutate 함수를 사용하여 양식이 제출될 시 mutation을 트리거하고 있다. 또한 mutation의 error와 isSuccess를 사용하여 mutation의 오류와 성공 메시지를 표시하고 있다.
여기까지의 예시에서는 useMutation 훅과 mutation function을 사용하여 mutation을 만드는 방식을 정리해 보았다. 이와 같은 기본 단계를 이해하면 리액트 애플리케이션에서 다양한 데이터 수정을 위한 보다 복잡한 mutation을 생성하는 것이 가능하다.
 
- Mutation 함수와 인자
동적인 동작을 정의해야 하거나, 연산에 필요한 추가 데이터가 제공될 필요가 있는 경우 등의 경우에 mutation function에 변수를 전달해야 할 수 있다. 리액트 쿼리를 이용할 때 useMutation 훅의 결과에 포함된 함수를 사용하면 전달해야 할 변수를 mutation function에 쉽게 전달할 수 있다. 위의 mutation 생성 예시에 이어서, form을 제출할 때 각 항목에 카테고리를 추가한다고 가정하면, 카테고리에 대한 새로운 인수를 받아 POST 요청에 포함하도록 addItem 함수를 변경해야 한다.

mutation function인 addItem이 category라는 두번째 인자를 필요로 하게 되었다.

AddItemForm 컴포넌트는 또한 category라는 새로운 state를 가지게 되었고, handleSubmit 또한 newItem과 category를 전달하고 있다. 이제 form이 제출될 때 addItem muation function은 newItem과 category를 인자로 받아 사용하게 된다.
위처럼, 리액트 쿼리를 사용하면 useMutation 훅의 결과가 포함하는 함수를 통해 변수를 mutation function에 간단하게 전달할 수 있다. 이를 통해 리액트 애플리케이션에서 mutation을 사용한 작업을 진행할 때 보다 큰 유연성을 가질 수 있고 다양한 동적 동작을 구현할 수 있다.
 
- Mutation의 에러와 성공을 핸들링하기
Mutation을 진행할 때는 오류를 처리하고 작업의 성공에 대한 피드백을 제공하는 것이 필수적이다. 리액트 쿼리를 이용하면 useMutation 훅이 제공하는 여러 값과 콜백을 이용하여 mutation의 error와 성공 상태를 쉽게 관리할 수 있다. 앞선 예시를 업데이트하며 오류와 성공 상태를 효과적으로 처리하는 방법에 대해 알아보겠다.
AddItemForm 컴포넌트에서 오류와 성공 처리 방식을 개선해보겠다. 오류와 성공의 경우 메시지를 직접 표시하는 대신, useMutation 훅이 제공하는 onError, onSuccess 콜백을 이용해 보자. 이러한 콜백은 mutation에서 오류가 발생하거나 성공적으로 완료될 때 실행되게 된다.

위 예시에서는 오류와 성공 메시지를 콘솔에 로그로 나타내고 있다. Toast 알림을 표시하거나 애플리케이션의 상태를 업데이트 하는 등의 side effect로 이를 대체하여 사용할 수 있다.
또한, 컴포넌트 내부에 피드백을 표시해야 하는 경우 useMutation 훅에서 제공하는 error 혹은 isSuccess 값에 액세스 하는 방식을 이용할 수 있다. 하지만 onErorr, onSuccess 콜백을 이용하면 mutation의 상태가 변경될 때 side effect가 한 번만 실행되도록 할 수 있다.
리액트 쿼리의 에러와 성공의 처리 기능을 활용하면 리액트 애플리케이션에서 mutation 작업을 할 때 보다 사용자 친화적인 경험을 제공할 수 있다.
 
- 낙관적 업데이트와 롤백
낙관적 업데이트는 mutation이 성공할 것으로 가정하여 UI를 즉시 업데이트하는 방식으로 애플리케이션의 체감 성능을 개선하는데 유용한 기법이다. mutation이 실패한다면 UI는 이전 상태로 롤백되게 된다. 리액트 쿼리는 useMutation 훅에서 onMutate, onError, onSuccess 콜백을 제공하고 이를 통해 낙관적 업데이트와 롤백을 간단하게 구현할 수 있도록 한다. 예시를 다시 변형하면서 낙관적 업데이트와 롤백을 어떻게 활용할 수 있는지 알아보려 한다.
먼저, 로컬 캐시를 새로운 아이템으로 최적으로 업데이트하는 방법이 필요하다. 애플리케이션의 Item 목록을 가져오고 관리하기 위하여 리액트 쿼리를 사용하고 있으며 아이템을 가져오기 위한 Query Key가 items라고 가정하자. 로컬 캐시를 최적으로 업데이트하기 위하여 queryClient 인스턴스와 setQueryData 메서드를 이용한다.

먼저, AddItemForm 컴포넌트에서 queryClient 인스턴스를 이용하기 위해 라이브러리가 제공하는 useQueryClient 훅을 이용하여 queryClient 인스턴스를 불러온다.

다음으로 useMutation을 업데이트하여 mutation이 실행될 시 호출될 onMutate 콜백을 포함한다. 해당 콜백에서 먼저 previousItems에 mutation 실패 시 사용할 기존 값을 저장한다. 다음으로 setQueryData 메서드를 사용하여 로컬 캐시를 낙관적 업데이트를 진행한다. 그리고 기존 값을 반환하여 onError 콜백에서 사용될 수 있도록 한다. 다음으로, onError와 onSuccess 콜백을 업데이트하여 롤백과 캐시 업데이트의 경우에 대응하도록 설정한다.

onError 콜백에서는 로컬 캐시에 있는 previousItems를 복원하여 낙관적 업데이트를 롤백하고, onSuccess 콜백에서는 'items' 쿼리를 무효화하고 refetch를 진행하여 로컬 캐시를 서버 데이터와 동기화하도록 하고 있다.
리액트 쿼리를 통해 낙관적 업데이트와 롤백을 구현하게 되면 애플리케이션의 체감 성능을 향상시킬 수 있고 mutation 작업을 진행할 때 보다 원활한 사용자 경험을 제공할 수 있다.
 
- Mutation을 취소하기
사용자가 페이지에서 다른 곳으로 이동하거나 form 제출을 취소하는 경우와 같이 진행 중인 mutation을 취소해야 하는 경우가 존재할 수 있다. 리액트 쿼리의 useMutation 훅은 cacel 함수를 제공하여 mutation 취소를 위한 지원을 제공한다.
먼저 AbortController를 사용하여 cancel을 사용할 수 있도록 예시의 addItem 함수를 변경해보겠다. AbortController를 사용하면 AbortSignal을 발생시켜 fetch 요청을 취소할 수 있다.

다음은 cancel 함수를 사용하기 위하여 useMutation 함수를 변경하겠다.

onCancel 콜백에서 AbortController의 abort 메소드를 호출하면 fetch 요청을 취소할 수 있다.
이제 취소가 필요한 경우 useMutation 훅이 제공하는 cancel 함수를 사용할 수 있다. 예를 들어 AddItemForm 컴포넌트에 mutation 요청을 취소할 수 있는 Cancel 버튼을 추가한 경우를 생각해 보자.

여기에 추가된 Cancel 버튼은 mutation이 진행 중일 때만 활성화되고(isLoading에 의존) 사용자가 필요시 mutation을 취소할 수 있도록 하게 한다.
리액트 쿼리의 mutation에 대해 취소를 지원하도록 구현하게 되면 애플리케이션의 응답성을 향상시킬 수 있다. 또한 사용자가 mutation 작업 중에 생각이 바뀌거나 페이지에서 이탈하게 되는 경우 등에 대해 보다 나은 사용자 경험을 제공할 수 있게 된다.
 
6. 실제 활용과 결론
실제 리액트 애플리케이션에서 리액트 쿼리를 사용하는 경우의 몇 가지 활용 사례와 리액트 쿼리 개발자 도구, 다른 라이브러리와의 통합을 알아보겠다.
 
- 리액트 쿼리의 성능 최적화
애플리케이션의 성능 최적화는 사용자 경험의 원활한 제공을 위해 매우 중요한 부분이다. 리액트 쿼리를 사용할 때 성능을 최적화하기 위하여 몇 가지 기술을 사용할 수 있다. query caching, data prefetching, debouncing과 throttling에 대해 알아보자.
a. Query Caching: 리액트 쿼리는 쿼리의 결과를 자동으로 캐싱하여 네트워크 요청 횟수를 줄여 애플리케이션의 성능을 향상시킨다. 최신의 데이터를 유지하는 것과 네트워크 요청을 줄이는 것 사이의 균형을 맞추기 위하여 cacheTime, staleTime과 같은 캐싱 설정을 구성할 수 있다.
cacheTIme은 쿼리가 사용 중인 컴포넌트가 없는 비활성 상태가 된 후 캐시에 얼마나 오랫동안 유지될지를 결정한다. staleTime은 쿼리의 데이터가 최신의 데이터로 간주되는 기간을 지정한다. 지정된 staleTime이 지나고 해당 쿼리가 다시 활성화될 때 리액트 쿼리는 백그라운드에서 새로운 데이터를 가져오려고 시도하게 된다. 이와 같은 설정을 조정하여 애플리케이션이 새 데이터를 얼마나 적극적으로 가져오도록 할지 제어할 수 있다.

b. Prefetching Data: 프리페칭은 데이터의 필요 이전에 데이터를 가져오는 것으로, 애플리케이션의 체감 성능을 향상시킬 수 있다. 사용자가 데이터를 필요로 할 것으로 예상되는 경우 prefetch 메서드를 사용하여 데이터를 미리 가져오도록 할 수 있다. 사용자가 탐색 링크나 버튼에 마우스를 가져가는 경우에 prefetch를 통해 미리 데이터를 가져오는 경우를 생각할 수 있다. 아래 예시는 버튼에 마우스가 진입하게 되는 경우 prefetch를 작동시킨다.

c. Debouncing and Throttling: 사용자 입력 등의 빈번한 업데이트를 처리하는 경우에 디바운스 혹은 쓰로틀링 기술을 사용하면 실행되는 쿼리의 수를 줄이는 데 도움이 될 수 있다. 디바운싱은 마지막 호출 이후 일정 시간이 경과할 때까지 함수 실행을 지연시키는 방식이고, 쓰로틀링은 지정된 시간 간격마다 함수의 실행을 한 번으로 제한하는 방식이다. 이와 같은 방식을 사용하면 서버의 부하를 줄이고 애플리케이션의 성능을 전반적으로 향상시킬 수 있다.
쿼리를 진행할 때 디바운스나 쓰로틀링을 사용하려면, Lodash와 같은 다른 라이브러리를 이용하거나 사용자 정의의 디바운스/쓰로틀링 함수를 구성하는 방식이 있다. Lodash를 이용한 debounce는 다음과 같다.

d. Parallel queries: 여러 쿼리를 병렬로 가져올 수 있는 방식으로 여러 소스에서 한 번에 데이터를 로드해야 하는 경우 성능 향상을 기대할 수 있다. 이를 위해서는 useQueries 훅을 이용한다.
e. Dependent queries: 다른 쿼리의 결과에 종속되는 쿼리가 있을 수 있다. 모든 데이터를 한 번에 가져오는 대신에 종속 옵션을 활성화하여 종속성이 해결된 경우에만 쿼리를 가져올 수 있게 할 수 있다. 
f. Pagination and Infinite Quries: 대규모의 데이터를 처리할 때는 pagination 혹은 Infinite query를 사용하여 데이터를 작은 단위의 청크로 가져올 수 있다. 이러한 방식은 한 번에 가져오는 데이터의 양을 줄이고 성능과 응답성을 향상시킬 수 있다.
g. Suspense: 리액트 쿼리는 리액트의 서스펜스 기능을 지원하여 데이터 불러오기와 로딩 상태를 보다 잘 처리할 수 있다. Suspense를 사용하면 Loading fallback을 렌더링 하고 layout shift를 방지하여 체감 성능을 개선할 수 있다.
 
- 리액트 쿼리 개발자 도구
리액트 쿼리 개발자 도구는 개발 단계에서 query와 mutation을 디버깅하고 모니터링하는 데 도움이 되는 도구이다. 이를 이용하면 데이터가 어떻게 가져와지고 캐시 되고, 실시간으로 업데이트되는지를 확인할 수 있다.
리액트 쿼리 개발자 도구를 사용하려면 먼저 @tanstack/react-query/devtools 패키지를 설치해야 한다. 다음은 ReactQueryDevtools 컴포넌트를 불러오고 QueryClientProvider 컴포넌트의 자식으로 위치시켜야 한다.

이렇게 하면 개발자 도구를 이용하여 여러 기능을 활용할 수 있다.
- active/inactive query의 상태 보기(error, fetching state를 포함)
- 각 query에 대한 캐시 구성과 데이터 검사
- 개발자 도구에서 수동으로 query refetching
- mutation state와 진행 상태 관찰
...
 
- 리액트 쿼리와 다른 라이브러리의 통합
리액트 쿼리는 다른 라이브러리들과 원활하게 통합할 수 있는 다용도 라이브러리이다. 이러한 유연성 덕분에 개발자는 서버 상태 관리에는 리액트 쿼리를 사용하는 동시에 클라이언트 상태 관리 혹은 다른 추가 기능에는 다른 라이브러리의 기능을 사용할 수 있다.
a. Redux: 리덕스는 자바스크립트 애플리케이션에 널리 이용되는 상태 관리 라이브러리이다. 리액트 쿼리를 서버 상태 관리에, 리덕스를 클라이언트 상태를 관리하는 데 사용할 수 있다. 두 라이브러리를 함께 이용하게 되면 애플리케이션을 위한 종합적인 상태 관리 솔루션을 제작할 수 있다. 이와 같이 사용한다면 데이터 불러오기, 캐싱, 동기화는 리액트 쿼리가 나머지 클라이언트의 상태는 리덕스가 관리하게 된다.
b. GraphQL Clients (Apollo, Relay, ...): 아폴로, 릴레이와 같은 GraphQL 클라이언트는 GraphQL API와 함께 작동하도록 설계되어 캐싱, 구독 등의 기능을 제공한다. 이러한 라이브러리들과 함께 리액트 쿼리를 사용하여 GrpahQL이 아닌 API 데이터를 처리하거나 추가적인 캐싱, fetching을 제공할 수 있다. 또한 사용자 정의 fetcher 함수를 사용하여 GraphQL API에서 데이터를 가져오고 업데이트하기 위하여 리액트 쿼리의 훅들을 사용할 수도 있다. 이를 통해 데이터에 대한 리액트 쿼리의 캐싱 및 상태 관리 기능을 이용할 수 있다.
c. Formik, React Hook Form 등의 Form 라이브러리: 복잡한 form을 작성하게 될 때 form 라이브러리들을 이용하면 form의 state, 유효성 검사, submit 등을 관리하는 데 도움이 될 수 있다. 리액트 쿼리와 이들을 통합하면 서버 측 데이터와 원활하게 상호 작용하는 강력하고 효율적인 form을 제작할 수 있다.
 
 
 
 
 
https://developer.mozilla.org/ko/docs/Web/API/AbortController

AbortController - Web API | MDN

AbortController 인터페이스는 하나 이상의 웹 요청을 취소할 수 있게 해준다.

developer.mozilla.org

https://tanstack.com/query/latest

TanStack Query | React Query, Solid Query, Svelte Query, Vue Query

Powerful asynchronous state management, server-state utilities and data fetching for TS/JS, React, Solid, Svelte and Vue

tanstack.com

https://velog.io/@y_jem/%EC%93%B0%EB%A1%9C%ED%8B%80%EB%A7%81throttling%EA%B3%BC-%EB%94%94%EB%B0%94%EC%9A%B4%EC%8B%B1debouncing

[react] 쓰로틀링(throttling)과 디바운싱(debouncing)

쓰로틀링(throttling)과 디바운싱(debouncing)

velog.io

https://www.mrlatte.net/code/2020/12/15/lodash-debounce

lodash로 간단하게 debounce 사용하기 - 라떼군 이야기

Problemdebounce를 이용 하고자 하면 아래처럼 타이머 핸들을 이용해서 사용하는 것을 생각할 수 있을 것이다.clearTimeout을 이용하기 때문에 같은 함수가 빠르게 호출되어도 실제로는 타임아웃 간격

www.mrlatte.net

https://tanstack.com/query/v4/docs/react/devtools

Devtools | TanStack Query Docs

Wave your hands in the air and shout hooray because React Query comes with dedicated devtools! 🥳 When you begin your React Query journey, you'll want these devtools by your side. They help visualize all of the inner workings of React Query and will like

tanstack.com