안녕하세요!
오늘은 제가 실무에서 쓰고 있는 react-query에 대해 글을 써볼까 합니다
지금 실무에서 쓰고는 있지만 늘 같은 형태로만 쓰고 있고,
아무것도 모른 상태에서 다른 동료가 작성한 코드를 토대로만 공부(?) 했기 때문에
모자른 부분이 굉장히 많아서... 공부 겸 내용 정리를 해보려고 합니다
블로그 글 중에 react-query에 대해 잘 정리해주신 분이 계셔서 링크 남깁니다
https://maxkim-j.github.io/posts/react-query-preview
1. react-query와 redux 차이점
사실 저는 redux만 알고 있던 상태에서 지금 회사에 들어간 후 쭉 react-query만 사용하게 되었습니다
저는 reacut-query가 redux와 같은 기능을 하는 라이브러리라고 생각했는데
생각보다 결이... 다르다고 해야하나? 그래서 둘의 차이점에 대해 검색해보게 되었습니다
- react-query는 서버 상태를 다룬다
- react-query는 전역 상태관리 라이브러리가 아니라 서버와 클라이언트 간의 비동기 작업을 쉽게 해주는 라이브러리
- react-query는 데이터 캐싱을 아주 쉽게 해결해준다
- redux에서는 action으로 조작해야했던 isLoading의 상태를 react-query에서는 직접 선아하고 조작할 필요가 없다
- redux, mobx 등은 클라이언트 상태를 다룬다
(redux-thunk, redux-sagea : 비동기 작업을 수행 / 데이터를 store에 저장한 뒤 컴포넌트에서 사용)
만약 react-query를 도입했는데, 서버 데이터와 관계없이 클라이언트에서 전역적으로 다뤄하는 데이터가 있다면?
Ex. 테마 다크 모드 등....?
context나 전역 상태관리 라이브러리를 따로 사용해야한다
(그래서 react-query + recoil 같은 조합을 많이 사용하는걸까?)
(react-query로 클라이언트 상태관리에 관한 블로그 글 👉 https://blog.hyunmin.dev/23)
저는 초반에 react-query와 redux가 대조되는 개념이라고 생각을 해서인지...
무조건 둘 중 하나만 써야한다! 라고 생각을 했는데요
실제 실무에서는 두 라이브러리를 섞어서 쓰는 회사도 많은가봅니다
(https://ridicorp.com/story/how-to-use-redux-in-ridi/)
2. 클라이언트 상태? 서버 상태?
Server State ≠ Client State 둘은 완전 다릅니다!
- Client-State : 세션간 지속적이지 않는 데이터, 동기적, 클라이언트가 소유, 항상 최신 데이터로 업데이트
(렌더링에 반영) Ex. 리액트 컴포넌트의 state, 동기적으로 저장되는 redux store의 데이터 - Server-State : 세션간 지속되는 데이터, 비동기적, 세션을 진행하는 클라이언트만 소유하는게 아니고 공유되는 데이터도 존재하며 여러 클라이언트에 의해 수정될 수 있음.
클라이언트는 서버 데이터의 스냅샷만을 사용하기 때문에 클라이언트에서 보이는 서버 데이터는 항상 최신임을 보장할 수 없음
Ex. 백엔드 DB에 저장되어있는 데이터
3. react-query의 장점
- 캐싱, 리패칭을 알아서 해줌 (최신 데이터를 참조할 수 있게 알아서 보장)
- 캐싱이란? → 자주 사용하는 데이터의 복사본을 저장한다 (앱 처리 속도를 높여줌)
- 리패칭이란 → 데이터를 다시 가져옴
- 불필요한 네트워크 통신을 줄일 수 있다!
(그러나 이 부분은 옵션을 어떻게 해주냐에 따라 다를 것 같다. 개인적으로 나는 아직 react-query 다루는게 미숙해서인지 리패칭이 필요한 순간이 아님에도 불구하고 다른 컴포넌트가 리렌더링될때 같이 리패칭 되는게 있음)
- Context API를 제공
- useQuery를 통해 만들어진 query는 고유한 key로 구분되어 여러개의 query를 컴포넌트 곳곳에다가 흩뿌려 놓아도 key만 같으면 동일한 query에 접근할 수 있다
(saga처럼 비동기 관련 성공, 실패 액션 하나하나를 모두 선언할 필요가 없음) - 선언적으로 프로그래밍이 가능하다 => 확실히 redux보다 알아보기 더 편하다고 생각합니다
(react-query에 대해 잘 몰라도 무슨 기능을 하는지 단번에 이해가 가능한 편!)
4. 사용법
$ yarn add react-query
최상위 루트 파일에서 Context Provider로 컴포넌트를 감싸고 queryClient를 내려보내준다
// root 파일
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
// 해당 부분을 console로 찍어보면 캐싱된 내용들을 확인할 수 있음
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
...
</QueryClientProvider>
);
}
- useQuery가 반환하는 객체의 프로퍼티는 4개이며, 어떤 상태인지 확인이 가능하다
- fresh : 새롭게 추가된 쿼리 인스턴스
- active 상태의 시작, 기본 staleTime이 0이기 때문에 아무것도 설정을 안해주면 호출이 끝나고 바로 stale 상태로 변한다(캐싱 기능을 활용할 수 없음)
✅ stale란? → 최신화가 필요한 데이터라는 의미 (오래된 데이터) - staleTime을 늘려줄 경우 fresh한 상태가 유지되는데, 이때는 쿼리가 다시 마운트(렌더링)되도 패칭이 발생하지 않고 기존의 fresh한 값을 반환한다 (네트워크 통신 낭비 방지)
- active 상태의 시작, 기본 staleTime이 0이기 때문에 아무것도 설정을 안해주면 호출이 끝나고 바로 stale 상태로 변한다(캐싱 기능을 활용할 수 없음)
- fetching : 요청을 수행하는 중인 쿼리
- stale : 인스턴스가 존재하지만 이미 패칭이 완료된 쿼리
- 특정 쿼리가 stale된 상태에서 같은 쿼리 마운트를 시도한다면 캐싱된 데이터를 반환하면서 리패칭을 시도한다
- 데이터를 받아오는 즉시 stale 하다고 판단하며 캐싱 데이터와 무관하게 계속해서 fetching을 수행한다
- inactive : active 인스턴스가 하나도 없는 쿼리
- inactive된 이후에도 cacheTime 동안 캐시된 데이터가 유지된다 → cacheTime이 지나면 GC(가비지 컬렉션) 된다 (기본값은 5분 뒤에 메모리에서 삭제)
- fresh : 새롭게 추가된 쿼리 인스턴스
✅ 위 상태들은 react-query devtool로 확인이 가능합니다! (별도 설치)
const {status, data, error, isFetching, isPreviousData } = useQuery(
['projects', page],
() => fetchProjects(page),
[keepPreviousData: true, staleTime: 5000}
);
// 실무 코드
// 'faqList' 유니크 key 이름
// courseId -> fetchCourseFAQList 함수 인자
// fetchCourseFAQList는 통신을 통해 서버에서 받아온 response data (콜백함수 영역)
const { data: faqListResponse }: any = useQuery(['faqList', courseId, fetchCourseFAQList]);
- useQuery hook의 인자로 2개가 필요하다
- 쿼리의 유니크한 key → 한번 fresh가 되었다면 계속 추적이 가능하다.
리패칭, 캐싱, 공유 등을 할때마다 참조되는 값이다
주로 배열을 사용하고, 배열의 요소로 쿼리의 이름을 나타내는 문자열과 프로미스를 리턴하는 함수의 인자로 쓰이는 값을 넣는다 - 프로미스를 리턴하는 함수
- 3번째 인자는 옵션이다
- 쿼리의 유니크한 key → 한번 fresh가 되었다면 계속 추적이 가능하다.
- useQuery의 반환 값
- 객체
- 요청의 상태를 나타내는 몇가지 프로퍼티
- 요청의 결과나 에러 값을 갖는 프로퍼티
- isLoading : 로딩중
- isError : 에러
- isSuccess : 요청 성공
- isIdle : 쿼리 data가 하나도 없고 비었을때 {enabled : false} 상태로 쿼리가 호출되었을때 이 상태로 시작된다
- status
- error, data, isFetching
- [keepPreviousData: true, staleTime: 5000} 의미
- 주요 쿼리 옵션이며 이 구간에 옵션 설정이 가능함 (다양한 옵션이 있으니 공식 문서 참조)
쿼리 요청 함수에서 queryKey에 접근할 수 있다
function fetchTodoList({ queryKey }) {
const [_key, { status, page }] = queryKey;
return new Prmise();
}
// 실무 코드
const fetchData = useCallback(async ({ querykey }) => {
const [_key, {pageIndex, pageSize }] = queryKey;
const page = pageIndex + 1;
const response = await fetchCourseList(page, 50);
return response.data;
}, []);
useQueries : 쿼리 여러개가 동시에 수행해야 하는데, 렌더링이 거듭되는 사이사이에 계속 쿼리가 수행되어야 한다면 쿼리를 수행하는 로직이 hook 룰에 위배될 수도 있다. 그때 사용하는 hook이다
function App({ users }) {
const userQueries = useQueries(
users.map(user => {
return {
querykey: ['user', user.id],
queryFn: () => fetchUserById(user.id),
}
})
)
}
useMutations : useQuery와는 다르게 create, update, delete할때 사용되며 Server-State에 사이드 이펙트를 일으키는 경우에 사용한다
✅ 사이드 이펙트란? 👉 컴포넌트가 화면에 렌더링된 이후에 비동기로 처리되어야 하는 부수적인 효과들
Ex. 외부 API 호출 후, 일단 화면에 렌더링 먼저 해주고 실제 데이터는 비동기로 가져오는 것
- useMutation으로 mutation 객체를 정의하고, mutate 메서드를 사용하면 요청 함수를 호출해 요청이 보내진다
- useMutation이 반환하는 객체 프로퍼티로 제공되는 상태값은 useQuery와 동일하다
- mutation.reset : 현재의 error와 data를 모두 지울 수 있다
- 두번째 인자로 콜백 객체를 넘겨줘서 라이프사이클 인터셉터 로직을 짤 수도 있다
// mutation이 성공하면 해당 데이터를 리패칭
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos');
queryClient.invalidateQueries('reminders')
},
});
- invalidateQueries는 query key를 날려버릴때 사용하면 유용하다
- 대부분 post 요청이나 delete요청에서 query key를 변화시키기 위해 강제 리프레쉬 기능으로 사용된다
// 서버에서 받은 값이 갱신된 새로운 데이터일 경우
const mutation = useMutation(editTodo, {
onSuccess: data => queryClient.setQueryData(['todo', { id: 5 }], data),
});
mutation.mutate({
id: 5,
name: 'Do the laundry'
});
const { status, data, error } = useQuery(['todo', { id: 5 }], fetchTodoById);
5. Refetching이 일어나는 경우
이 부분을 잘~ 고려해서 옵션을 설정해주면 불필요한 통신을 낭비할 수 있을 것 같다
- 런타임에 stale인 특정 쿼리 인스턴스(객체)가 다시 만들어졌을때
- 페이지를 이동했다가 왔을때
- window가 다시 포커스가 되었을때 (옵션 설정 가능)
- 네트워크가 다시 연결되었을 때 (옵션 설정 가능)
- refetch interval(리패칭 간격)이 있을때
- 요청 실패한 쿼리는 기본으로 3번 더 백그라운드단에서 요청하며, retry, retryDelay 옵션으로 간격과 횟수를 커스텀 가능하다
6. 마무리
사실 저도 늘 쓰던 형태만 쓰고 그 형태에서 벗어난 적이 없기에 뭐라 더 설명할 방법이 없네요
나중에 좀 더 프로젝트를 고도화 하면서 다양한 옵션, hook을 써볼 것이고
그러면서 따라오는 시행착오 내용들을 추가할 예정입니다
'Frontend' 카테고리의 다른 글
이벤트 리스너(event listener)를 제거 해야하는 이유 (1) | 2024.08.30 |
---|---|
React Query와 Redux의 차이, 장단점 (1) | 2024.08.27 |
React Hook Form | 내가 실무에서 쓰는 법 (0) | 2022.07.28 |
React | Hook 정리 + 18 버전 새로운 Hook (0) | 2022.07.28 |
react-router-dom v6 업데이트 후 달라진 점 (ft. Prompt 창 띄우는 법) (5) | 2021.12.22 |
댓글