일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 멘션 추천 기능
- Next/Image 캐싱
- 부스트캠프
- 코딩테스트
- 스택
- 파이썬
- Image 컴포넌트
- 자바 프로젝트
- 파이썬 웹크롤링
- 파이썬 코딩테스트
- react
- 네이버 부스트캠프
- git checkout
- 비디오 스트리밍
- React ssr
- PubSub 패턴
- 프로그래머스
- 네이버 부캠
- 자바스크립트
- 브라우저 동작
- 자바스크립트 컴파일
- 네이버 부스트캠프 멤버십
- Next.js
- React.js
- beautifulsoup
- 웹크롤링
- c++
- 자바스크립트 객체
- Server Side Rendering
- 씨쁠쁠
- Today
- Total
코린이의 개발 일지
React Query 살펴보기 본문
설치
yarn add react-query
React-Query가 주장하는 Global State 개념
- Global State라는 말을 쓰지 말자: 전역 state는 Client와 Server로 분류할 수 있고, 이 두 State는 다른 방식으로 다뤄져야 효율적인 앱을 만들 수 있다.
- Server State
- 세션간 지속되는 데이터
- 비동기적
- 세션을 진행하는 클라이언트만 소유하는게 아니고 공유되는 데이터도 존재하며 여러 클라이언트에 의해 수정될 수 있음.
- 클라이언트에서는 서버 데이터의 스냅샷만을 사용하기 때문에 클라이언트에서 보이는 서버 데이터는 항상 최신임을 보장할 수 없다.
- ex) 비동기 요청으로 받아올 수 있는, 백엔드 DB에 저장되어 있는 데이터
- Client State
- 세션간 지속적이지 않은 데이터
- 동기적
- 클라이언트가 소유
- 항상 최신 데이터로 업데이트 (렌더링에 반영)
- ex) 리액트 컴포넌트의 state, 동기적으로 저장되는 데이터
만들어진 이유
- 서버 데이터는 항상 최신임을 보장하지 않는다. 명시적으로 fetching을 수행해야만 최신 데이터로 전환된다.
- 네트워크 통신은 최소한으로 줄이는게 좋은데, 복수의 컴포넌트에서 최신 데이터를 받아오기 위해 fetching을 여러번 수행하는 낭비가 발생할 수 있다.
Server State 모델링
우선 리액트 쿼리에서 상태 모델링은 다음과 같다.
- fresh
- 새롭게 추가된 쿼리 인스턴스
- active 상태의 시작
- default staleTime은 0이기 때문에 옵션을 설정해주지 않으면 api 호출이 끝나고 바로 stale 상태로 변한다.
- staleTime을 늘려줄 경우, 그 시간동안 fresh한 상태가 유지된다. 이때는 쿼리가 다시 마운트되어도 패칭이 발생하지 않고, 기존의 fresh한 값을 반환한다.
- fetching:
- 요청을 수행하는 중인 쿼리
- stale: 데이터가 오래된 상태, Fetching을 통해 Fresh 상태로 유지해줘야함.
- 특정 쿼리가 stale된 상태에서 같은 쿼리 마운트를 시도한다면 캐싱된 데이터를 반환하면서 리패칭을 시도한다.
- inactive
- active 인스턴스가 하나도 없는 쿼리, inactive된 이후에도 cacheTime동안 캐시된 데이터가 유지된다.
- React Query에서 브라우저 캐시를 관리하는 가비지 컬렉터에 의해 삭제된다고 함
- deleted: inactive 상태의 데이터가 캐시에서 삭제된 상태
inactive 상태와 stale 상태의 차이를 모르겠어서 좀 찾아봤는데,
- stale상태는 렌더링에 사용되는 패칭을 이미 보낸 오래된 상태.
- 리액트 쿼리는 이전에 패칭한 데이터중 이제 사용하지 않을 데이터 (렌더링 하지 않을 데이터)는 inactive 상태로 바꾼다.
fetch를 해온 데이터중에 다시 렌더링 하지 않을 데이터라는 것은 무엇일까?
예시로 페이지네이션과 토글을 들 수 있다.
페이지네이션의 경우 이전에 불러온 페이지는 현재 렌더링할 대상이 되는 데이터가 아니기 때문에 inactive상태로 전환된다.
또다른 예시로는
const [show, setShow] = useState(true);
//...
{
show && <GitUsers />;
}
위의 코드에서 show라는 상태 토글에 따라 GitUser라는 컴포넌트가 보이거나 안보이는 상황이라고 할때,
show가 false라서 GitUsers 컴포넌트가 안보이는 상황일 경우, 리액트 쿼리는 git user 데이터가 렌더링 되지 않을 데이터라는 걸 알고 상태를 inactive로 바꾼다.
그러니까 순서대로 생각해보면
- 컴포넌트 렌더링이 발생하면, react query는 git user 목록을 fetch 해오고 렌더링. (fresh 상태)
- 정해놓은 staleTime이 지나면 git user 목록은 stale 상태가 된다.
- 사용자가 버튼을 눌러 show가 false가 되면 git user목록은이제 렌더링 되지 않을 (사용되지 않을)데이터 이기 때문에 inactive상태가 된다. (inactive가 상태가 되어도 cacheTime동안 react query in-memory cache에 저장된다.)
- 여기서 핵심은 만약 git user 목록이 여러 컴포넌트에서 사용이 되며, 모든 쿼리 인스턴스가 언마운트 되고 사용되지 않을 상황이어야 해당 쿼리가 inactive 상태가 된다.
- 만약 5분이 지나지 않은상태에서 git user 쿼리가 다시 마운트 되면, 쿼리는 즉시 캐시된 데이터를 리턴하고 띄운 후, 그동안 fetch함수를 background에서 실행해 fresh value를 가져온다.
- inactive 상태에서 5분이 지났다면 해당 쿼리는 삭제된 후 garbage collected된다.
Refs.
캐싱관련 React Query 공식문서: Caching Examples
Stale state와 Inactive State 차이점: What Is The Difference Between Stale State and Inactive State in React Query
음 각각의 상태는 좀더 공부를 해봐야 잘 이해가 될거 같다.
일단 사용을 해보자
사용전에 refetching 조건을 살펴 보면 아래 4가지 조건이 있다.
Refetching이 일어나는 조건
- refetchOnMount: 데이터가 stale 상태일 경우 마운트할 때(옵션으로 끄기 가능, default는 true)
- refetchOnWindowFocus: window가 다시 포커스 되었을 때(옵션으로 끄기 가능, default는 true)
- refetchOnReconnect: 네트워크가 다시 연결되었을 때(옵션으로 끄기 가능, default는 true)
- retry: refetch interval이 있을때 ⇒ 요청 실패한 쿼리는 default로 3번 더 백그라운드 단에서 요청하며, retry, retryDelay 옵션으로 간격과 횟수를 커스텀 할 수 있다.
React Query 사용
우선 가장 상단 컴포넌트를 QueryClientProvider로 감싼다.
- QueryClientProvider는 비동기 요청을 처리하기위한 Context Provider로 동작
import React from 'react';
...
import { QueryClient, QueryClientProvider } from 'react-query';
function App() {
const queryClient = new QueryClient();
return (
<ThemeProvider theme={AppTheme}>
<QueryClientProvider client={queryClient}>
<Global styles={globalStyle} />
<AppStyle>
<Router>
...
</Router>
</AppStyle>
<Modal />
</QueryClientProvider>
</ThemeProvider>
);
}
그럼 이제 하위 컴포넌트 들에서 React Query를 사용할 수 있다.
useQuery
- 리액트 쿼리는 GET 요청을 보낼 때 useQuery훅을 사용한다.
- 서버 데이터를 바꿀 수 있는 create update delete 요청이라면 mutation 쓰는게 더 추천된다. (useQuery로도 가능하긴하다)
아래의 코드는 투두리스트를 띄우는 로직이다.
import React, { useState } from 'react';
import styled from '@emotion/styled';
import { useQuery } from 'react-query';
import type todoResponseType from '../../types/TodoResponse';
import TodoInputBox from './TodoInputBox';
import TodoItemBox from './TodoItemBox';
import { httpGet } from '../../util/http';
import Loading from '../Loading';
import useModal from '../../hooks/useModal';
function TodoList() {
const [currentTodoList, setCurrentTodoList] = useState<todoResponseType[]>([]);
const { setContent, closeModal } = useModal();
const { isLoading } = useQuery(['todos'], () => httpGet('/todos'), {
refetchOnWindowFocus: true,
staleTime: 60 * 1000,
onSuccess: (data) => setCurrentTodoList([...data]),
onError: (error) => setContent(`${error}`, [{ name: '확인', handler: closeModal }]),
});
return (
<Wrapper>
<Header>Todo List</Header>
<ListBox>
<TodoInputBox />
{isLoading ? <Loading /> : currentTodoList.map((item) => <TodoItemBox key={item.id} currentTodo={item} />)}
</ListBox>
</Wrapper>
);
}
const { isLoading } = useQuery(['todos'], () => httpGet('/todos'), {
refetchOnWindowFocus: true,
staleTime: 60 * 1000,
onSuccess: (data) => setCurrentTodoList([...data]),
onError: (error) => setContent(`${error}`, [{ name: '확인', handler: closeModal }]),
});
useQuery 부분만 따로 떼어서 보면, 인자로 세개가 들어갔다.
- unique key
- 한 번 fresh가 되었다면 계속 추적이 가능하다.
- 리패칭, 캐싱, 공유 등을 할때 참조되는 값.
- 요청 함수가 특정 변수에 의존할 때, 쿼리 키 배열에 객체로 같이 넣어주면 요청 함수 내에서 인자로 객체를 받을 수 있다.
function Todos({ todoId }) { const result = useQuery(['todos', todoId], () => fetchTodoById(todoId)); } function Todos({ status, page }) { const result = useQuery(['todos', { status, page }], fetchTodoList); } // 쿼리 요청 함수에서 queryKey에 접근할 수 있다 function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey; return new Promise(); }
- 프로미스를 리턴하는 api 요청 함수 (이 함수는 반드시 resolve promise를 리턴하거나 에러를 throw해야 한다.
// /util/http.ts
export async function httpGet(url: string): Promise<ReturnType> {
const token = localStorage.getItem('token');
if (token === null) {
throw new Error('token error');
}
const response: ResponseType = await axios.get(url, {
headers: {
'Content-Type': 'application/json',
Authorization: token,
},
});
return response.data.data;
}
- 옵션
- 옵션은 진짜 많다. 필요 옵션은 찾아서 사용하자
- 필수적인 부분은 staleTime이다. 반드시 지정해야 요청 횟수를 줄일 수 있다.
useMutations
- useQuery와는 다르게 create, update, delete 하며 server state에 사이드 이펙트를 일으키는 경우에 사용
const updateTodoMutate = useMutation(
'updateTodo',
(todo: todoItemType) => httpPut(`/todos/${currentTodo.id}`, todo),
{
onSuccess: () => {
queryClient.invalidateQueries(['todos']);
},
onError: (error: errorResponseType) => {
setContent(`${error.response.status}: ${error.response.statusText}\\nmessage: ${error.response.data.message}`, [
{ name: '확인', handler: closeModal },
]);
},
}
);
- useMutation으로 mutation 객체 정의 후, mutate메서드를 사용해야 요청 함수를 호출해 요청이 보내진다. (useQuery와 가장 큰 차이점)
- useQuery를 사용할때처럼 실패시 retry가 디폴트는 아니지만, retry 옵션을 줄 수는 있다.
invalidation
- stale 쿼리 폐기
- invalidateQueries 메소드를 사용하면 개발자가 명시적으로 query가 stale되는 지점을 지정할 수 있다.
- 해당 메소드가 호출되면 쿼리가 바로 stale되고 리패치가 진행된다.
// 캐시가 있는 모든 쿼리들을 invalidate한다.
queryClient.invalidateQueries();
// 'todos'로 시작하는 모든 쿼리들을 invalidate한다.
queryClient.invalidateQueries('todos');
- Mutation이 일어날 때, 관련 query도 invalidate되어야 하기 때문에, mutation 생명주기 콜백 안에서 invalidate 해주는 경우가 많다.
setQueryData
- 또한 mutation으로 요청 후 서버에서 받는 response값이 갱신된 새로운 데이터일 경우도 있다.
- 이럴때는 mutation을 성공했을 때 쿼리 데이터를 명시적으로 바꿔주는 queryClient 인스턴스의 setQueryData 메소드를 사용하면 좋다.
const queryClient = useQueryClient();
const mutation = useMutation(editTodo, {
onSuccess: (data) => queryClient.setQueryData(['todo', { id: 5 }], data),
});
mutation.mutate({
id: 5,
name: 'Do the laundry',
});
// 뮤테이션의 response 값으로 업데이트된 data를 사용할 수 있다.
const { status, data, error } = useQuery(['todo', { id: 5 }], fetchTodoByID);
Optimistic Update(낙관적 업데이트)
- mutation시, invalidation이나 setQueryData 방식이 아닌 낙관적 업데이트라는 방식도 있다.
공식문서: Optimistic Updates
낙관적 업데이트 구현한 글: React Query(리액트 쿼리) 개념 및 예제(6) | Kkiri Blog
쿼리 함수 처리 방식: 병렬 처리
- 쿼리가 여러개 선언되어 있는 상황에 쿼리 함수들은 그냥 병렬로 요청되서 처리된다.
function App () {
// 이렇게 주루륵 있을 때 병렬로 처리
const usersQuery = useQuery('users', fetchUsers)
const teamsQuery = useQuery('teams', fetchTeams)
const projectsQuery = useQuery('projects', fetchProjects)
...
기타
스크롤 복원이 자동으로 된다
- react나 next로 개발을 하다보면 스크롤 복원시키가 까다로운데, react query를 사용하면, cache가 유지되는 동안(디폴트 5분)은 자동으로 스크롤 복원이 된다고 한다.
관련 내용 공식문서: Scroll Restoration
SSR도 지원한다
- 서버에서 prefetching한 데이터를 queryClient로 전달하는 방식을 2가지 지원한다.
- initialData 사용하기
- Hydration 사용하기
관련 내용 공식문서: SSR
이외에도 react query는 관련 내용, api, 옵션들이 정말 많아서 필요한 부분 공식문서 찾아보는 것이 좋을 거 같다.
Refs:
https://react-query-v3.tanstack.com/
https://maxkim-j.github.io/posts/react-query-preview/
https://backbencher.dev/react-query-what-is-inactive-query-state
https://velog.io/@kimhyo_0218/React-Query-리액트-쿼리-시작하기-useQuery
https://devkkiri.com/post/7fafd5b1-f034-47a6-8f4b-201701f8f991
'웹 (web) > 프론트엔드' 카테고리의 다른 글
React로 Server Side Rendering 구현하기 - 3. Data fetching (0) | 2023.03.30 |
---|---|
React로 Server Side Rendering 구현하기 - 2. 서버 구축 (1) | 2023.03.09 |
React로 Server Side Rendering 구현하기 - 1. 웹팩 설정 (0) | 2023.03.09 |
[Next.js] Next 동작 원리를 알아보자 (0) | 2022.12.03 |
[React] 리액트에서 Canvas API로 애니메이션 구현하기 (0) | 2022.10.21 |