What is "React Query"?
React Query는 데이터 Fetching, 캐싱, 동기화, 서버 쪽 데이터 업데이트 등을 쉽게 만들어 주는 React 라이브러리이다.
→ 서버 상태 관리 라이브러리중 하나이다.
(전역)상태 관리 라이브러리
- redux
- middleware
- redux-saga
- redux-thunk(redux-toolkit에 내장)
- redux-observable
- middleware
- recoil
- mobx
- jotai
- zustand
- xstate
서버 상태 관리 라이브러리
- react-query
- swr
- rtk-query(redux-toolkit에 내장)
Why "React Query"?
등장 배경
기존에 Redux, Mobx, Recoil과 같은 다양하고 훌륭한 상태 관리 라이브러리들이 있긴 하지만, 클라이언트 쪽의 데이터들을 관리하기에 적합할 순 있어도 서버 쪽의 데이터들을 관리하기에는 적합하지 않은 점들이 있어서 등장하게 되었다.
ex)
- Client side's Data: input의 state등
- Server side's Data: DB에서 가져온 데이터
물론, Redux Middleware (Thunk, Saga, Observable) 등을 활용해서 서버 쪽의 데이터를 관리할 수는 있지만, thunk나 saga를 쓰다보면 보일러 플레이트가 많아, redux가 비대해지고 유지보수에 좋지 않다는 생각이 든다.
다시 한번 정리
개발을 하다보면 의문이 든다!
- 왜 API 호출을 다 redux 미들웨어를 활용해서 하려는 걸까?
- 특정 컴포넌트에서만 호출되는 API를, 왜 여러 컴포넌트에서 사용하려고 만든 전역 상태 라이브러리인 redux에서 관리해?
- 그렇다고 안넣자니 중구난방, 통일성에 좋지않음
→ 서버 데이터만 따로 관리할 수 있는 게 없을까?
사용하는 이유
저의 경우 서버로 부터 값을 가져오거나 업데이트 하는 로직을 store 내부에 개발하는 경우가 많습니다. 그렇다보니 store는 클라이언트 state를 유지해야하는데 어느 순간부터 store에 클라이언트 데이터와 서버 데이터가 공존 하게 됩니다. 그리고 그 데이터가 서로 상호작용하면서 서버 데이터도 클라이언트 데이터도 아닌 끔찍한 혼종(?)이 탄생하게 됩니다. (예를 들면 서버에는 이미 패치된 데이터가 클라이언트에서는 패치되기 전 데이터가 유저에게 사용되고 있는 것이라고 볼 수 있습니다.)
그래서 react-query를 사용함으로 서버, 클라이언트 데이터를 분리합니다. 이 개념에 대해 동의 하지 않아도 아래의 장점을 보신다면 사용하고 싶은 생각이 드실 것입니다.
react-query 장점
여러가지 장점이 있지만 주로 아래와 같이 프론트 개발자가 구현하기 귀찮은 일들을 수행합니다.
- 캐싱
- get을 한 데이터에 대해 update를 하면 자동으로 get을 다시 수행한다. (예를 들면 게시판의 글을 가져왔을 때 게시판의 글을 생성하면 게시판 글을 get하는 api를 자동으로 실행 )
- 데이터가 오래 되었다고 판단되면 다시 get (invalidateQueries)
- 동일 데이터 여러번 요청하면 한번만 요청한다. (옵션에 따라 중복 호출 허용 시간 조절 가능)
- 무한 스크롤 (Infinite Queries (opens new window))
- 비동기 과정을 선언적으로 관리할 수 있다.
- react hook과 사용하는 구조가 비슷하다.
React Query Set-Up
react-query 설치
yarn add react-query
src/App.js
import React from "react";
import Router from "./shared/Router";
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Router />;
</QueryClientProvider>
); };
export default App;
간혹 index.js에 provider을 제공하는 경우가 있는데, 오류가 발생할 수 있으니 App.js에 Set-up하자.
useQuery
- 데이터를 get 하기 위한 api이다. (post, update는 useMutation을 사용한다.)
- 첫번째 파라미터로 unique Key(===query keys)가 들어가고, 두번째 파라미터로 비동기 함수(api호출 함수)가 들어간다.
(당연한 말이지만 두번째 파라미터는 promise가 들어가야한다.) - 첫번째 파라미터로 설정한 unique Key는 다른 컴포넌트에서도 해당 키를 사용하면 호출 가능하다. unique Key는 string과 배열을 받습니다. 배열로 넘기면 0번 값은 string값으로 다른 컴포넌트에서 부를 값이 들어가고 두번째 값을 넣으면 query 함수 내부에 파라미터로 해당 값이 전달됩니다.
- return 값은 api의 성공, 실패여부, api return 값을 포함한 객체입니다.
- useQuery는 비동기로 작동합니다. 즉, 한 컴포넌트에 여러개의 useQuery가 있다면 하나가 끝나고 다음 useQuery가 실행되는 것이 아닌 두개의 useQuery가 동시에 실행됩니다. 여러개의 비동기 query가 있다면 useQuery보다는 밑에 설명해 드릴 useQueries를 권유드립니다.
- enabled를 사용하면 useQuery를 동기적으로 사용 가능합니다. 아래 예시로 추가 설명하겠습니다.
DB통신과 관련한 로직을 한곳에 모아놓는다.
src/api/todos.js or src/axios/todos.js
import axios from "axios";
const getTodos = async () => {
const response = await axios.get("http://localhost:3000/todos");
return response;
};
export { getTodos };
유지보수를 위해 DB통신관련 로직은 한 곳에 모아놓는 것이 좋다.
useMutation
- useQuery 와 다르게 mutation은 데이터를 생성 / 업데이트 / 삭제 할 때 사용 된다.
- 내가 리액트 쿼리를 좋아하는 이유 중 큰 비중을 차지하는 queryClient 의 invalidateQueries 메소드 및 setQueryData 메소드랑 같이 사용하면 최고의 효율을 낼 수 있다 너어무 좋다
- mutation 의 성공을 바라며 미리 UI부터 변화시켜주는 optimistic update 기능도 사용자에게 정말 좋은 경험을 제공할 수 있다.
- 전에 useQuery 편 처럼 useMutation 을 커스텀 훅으로 만드는 내용까지 시리즈로 모두 기록할 예정 !
import { useMutation } from "react-query";
// 더 많은 return 값들이 있다.
const { data, isLoading, mutate, mutateAsync } = useMutation(mutationFn, options);
mutate(variables, {
onError,
onSettled,
onSuccess,
});
Options
mutationFn (variables: TVariables) => Promise<TData>
- Required
- 비동기 작업을 수행하고 프로미스를 반환하는 함수이다. (쉽게 말해 api 요청하는 함수)
- variables 는 mutate가 전달하는 객체이다.
onSuccess, onError, onSettled
일반적으로 서버에 데이터 변경 요청을 하게 되면 변경 요청이 성공할 경우에 추가적인 액션을 할 수 있도록 코드를 작성하고는 합니다.
이런 상황은 useMutation을 사용할 때도 동일하게 적용이 됩니다.
async/await을 사용할 때는 보통 다음과 같이 결괏값이 있는지를 확인한 뒤 추가 작업을 수행할 수 있는 코드를 작성합니다.
try {
const res = await axios.post('http://localhost:8080/savePerson', person);
if(res) {
console.log('success');
}
} catch(error) {
console.log('error');
} finally {
console.log('finally');
}
하지만 useMutation을 사용하면 다음과 같이 좀더 세련되게(?) 표현해줄 수 있습니다.
// 1
const savePerson = useMutation((person: Iperson) => axios.post('http://localhost:8080/savePerson', person), {
onSuccess: () => { // 요청이 성공한 경우
console.log('onSuccess');
},
onError: (error) => { // 요청에 에러가 발생된 경우
console.log('onError');
},
onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
console.log('onSettled');
}
});
// 2
const savePerson = useMutation({
mutationFn: (person: Iperson) => axios.post('/savePerson', person),
onSuccess: () => { // 요청이 성공한 경우
console.log('onSuccess');
},
onError: (error) => { // 요청에 에러가 발생된 경우
console.log('onError');
},
onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
console.log('onSettled');
}
})
- onSuccess는 요청이 성공되었을 때 실행되는 구간입니다.
- onError는 에러가 발생될 경우 실행되는 구간입니다.
- onSettled는 finally 구문처럼 요청이 성공하든 에러가 발생되든 상관없이 마지막에 실행되는 구간입니다.
'React-Query' 카테고리의 다른 글
Guides&Concepts_(Window Focus Refetching & Disabling/Pausing Queries) (0) | 2023.03.21 |
---|---|
Guides&Concepts_(Dependent Qeuries & Background Fetching indicators ) (0) | 2023.03.21 |
Guides&Concepts_(Queries Functions & Parallel Qeuries) (0) | 2023.03.20 |
Guides&Concepts_(Queries & Qeury Keys) (0) | 2023.03.20 |
Guides&Concepts_(Important Defaults) (0) | 2023.03.20 |