클라이언트/서버 상태
- 프론트엔드 개발시에 다루는 상태는 클라이언트 상태와 서버 상태로 크게 두가지가 있다
클라이언트 상태 - view를 위한 데이터
- 한 유저만을 위한 데이터(세션간 지속될 필요 없는 데이터)
- 렌더링에 반영하기 위한 데이터
→컴포넌트의 state를 화면에 보여줄때, input의 state, 동기적으로 저장되는 redux store의 데이터 등
서버 상태 - 서버에서 가지고 오는 데이터
- 여러 유저가 공유해야 하는 데이터(세션간 지속되어야 하는 데이터)
- DB에 저장되어 있는 데이터
→ axios로 api를 호출해서 자원을 가져오는 경우
react-query
- 서버 상태를 관리하기 위한 상태 관리 라이브러리이다. API 요청에 특화
- 서버의 값을 클라이언트 쪽에 가져오거나 캐싱, 업데이트, 에러 핸들링, infinite query과 같은 비동기 과정을 편하게 할 수 있도록 도와준다!
- Redux 같은 전역 상태관리 라이브러리들이 클라이언트 상태값에 대해서는 잘 작동하지만, 서버 상태에서는 그러지 않을 수 있다. 서버 데이터는 항상 최신 상태임을 보장하지 않기 때문이다. 네트워크 통신은 최소한으로 줄이는게 좋은데, 복수의 컴포넌트에서 최신 데이터를 받아오기 위해 fetching을 여러번 수행하는 낭비가 발생할 수 있다.
react-query 상태 관리 흐름
fetching
→ 데이터 요청 중
fresh
→ 데이터를 갓 받아온 직후 / 컴포넌트의 상태가 변하더라도 데이터 재요청 하지 않음
stale
→ fresh한 데이가 일정한 시간이 지나면 데이터 만료(상해버림) / 최신화가 필요한 데이터 여기까진 active 하다고 함!
inactive
→ 쿼리가 언마운트 된 상태 (더는 사용하지 않는 상태)
☞주의☜!
아직 캐시에서 완전히 삭제 된 상테가 아님
쿼리가 언마운트된다고 해서 비동기 요청이 취소되는 것은 아니다.
promise가 일단 만들어지고 언마운트된 거라면 데이터는 캐시에 살아 있을 수 있다. (공식문서에서 확인하기→)
delete
완전히 삭제된 상태(캐시 데이터가 메모리에서 삭제!)
주요 개념
query
→ query key + query function
→ 리덕스로 치면 store라고 볼 수 있다. 리덕스에서는 store 직접 데이터를 바꿔야 바뀌었지만 query key와 엮여있는 무언가가 변했다는 확신이 들면 query function을 다시 실행해버린다
query key
→ 쿼리를 구분하기 위한 특정 값. 문자, 배열, 딕셔너리 등을 넣을 수 있음
query function
→ 보통 api 호출
→ 서버에서 데이터를 요청하고 Promise를 리턴하는 함수(= 비동기 요청)
data
→ 쿼리 함수가 리턴한 Promise에서 resolve된 데이터
staleTime
→ 쿼리 데이터가 fresh 에서 stale로 전환되는데 걸리는 시간. 기본값은 0
cacheTime
→ unused 또는 inactive 캐시 데이터가 메모리에서 유지될 시간 기본값은 5분이며 설정한 시간을 초과하면 메모리에서 제거
react-query 사용법
1. react-query 설치
yarn add react-query
2. QueryClient / QueryClientProvider 설정
- index.js 파일에서 상태를 주입할 Provider로 감싸주고, 쿼리 클라이언트를 주입한다
- QueryClient: QueryClient는 그냥 객체이다. useQuery같은 훅을 사용하기 위해서 설정하는 것이라 생각하자.
- ReactQueryDevtools: react-query는 자체적으로 devtools를 지원한다
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
// 상태를 주입할 Provider로 감싸고 쿼리클라이언트 주입
<QueryClientProvider client={queryClient}>
{/* devtools */}
<ReactQueryDevtools initialIsOpen={true} />
<App />
</QueryClientProvider>
);
- 프로젝트를 실행하면 아래와 같은 devTools를 볼 수 있다!
3. useQuery() - 쿼리 인스턴스 만들기
- api 호출로 데이터를 가져올 때는 useQuery라는 훅을 사용한다. 데이터를 수정할 때는 useMutation을 사용(POST/PUT/DELETE)
- useQuery는 세가지 파라미터를 받는다.
- 쿼리 키: 문자열 / 배열 등을 넘길 수 있다. 이 키는 캐싱 처리하는데에 사용한다. 우리가 불러다 쓰기 위해서도 중요하다.query key와 엮여있는 무언가가 변했다는 확신이 들면 query function을 다시 실행해버린다.
ex) useMutation의 onSuccess로 캐싱된 데이터를 초기화 할 때 - 쿼리 함수: Promise를 반환하는 함수이다. API와 통신하는 함수가 주로 사용된다
- 옵션: useQuery()를 위한 옵션들을 넣어줄 수 있다
- 쿼리 키: 문자열 / 배열 등을 넘길 수 있다. 이 키는 캐싱 처리하는데에 사용한다. 우리가 불러다 쓰기 위해서도 중요하다.query key와 엮여있는 무언가가 변했다는 확신이 들면 query function을 다시 실행해버린다.
const my_query = useQuery(query_key, query_function, options)
++ useQuery 옵션들
- acheTime : 언마운트된 후 어느 시점까지 메모리에 데이터를 저장하여 캐싱할 것인지를 결정
기본 값 : 30000(5분) - staleTime : 쿼리가 fresh 상태에서 stale 상태로 전환되는 시간
기본 값 : 0 - refetchOnMount : 컴포넌트 마운트시 새로운 데이터 패칭
기본 값 : true
(false일 경우엔 마운트해도 새로운 데이터를 가지고 오지 않음!.) - refetchOnWindowFocus : 브라우저 클릭 시 새로운 데이터 패칭. 다른 탭에서 돌아오면 데이터 패칭!
기본 값 : true
(false일 경우엔 다른 탭으로 갔다가 돌아와도 데이터를 새로 가져오지 않는다.) - refetchInterval : 지정한 시간 간격만큼 데이터 패칭 / 브라우저 바깥에 있을 경우(다른 탭에 있는 등) 실행되지 않는다! 기본 값 : 0
- refetchIntervalInBackground : 브라우저에 포커스가 없어도 refetchInterval에서 지정한 시간 간격만큼 데이터 패칭
기본 값 : false - enabled : 컴포넌트가 마운트 되어도 데이터 패칭 ❌
기본 값 : true
useQuery의 리턴 값 중 refetch가 있다. enabled가 true일 때는 refetch를 통해 데이터를 패칭해야 한다. - onSuccess : 데이터 패칭 성공 시 수행할 콜백 함수
- onError : 데이터 패칭 실패 시 수행할 콜백 함수
- initialData : 초기 데이터 쿼리 생성 전이나 아직 캐싱되지 않았을 경우, 이 데이터를 default value로 사용한다
- select : 데이터 패칭 성공 시 가져온 데이터를 변환해주고 싶을 때, 원하는 데이터 형식으로 변환하기 위한 콜백
(여기서 return으로 변환한 데이터를 반환해주면 useQuery의 결과값인 data가 변한다.)
useQuery 예시
- useQuery로 쿼리 인스턴스를 생성한 후 화면에 렌더링 해보자!
- 쿼리 인스턴스의 data.data에서 서버에서 받아온 데이터를 확인할 수 있다! 또한 쿼리 인스턴스의 isLoading이나 isSuccess를 통해서
import {useQuery} from "react-query";
import React from "react";
import axios from "axios";
const App = () => {
const people_query = useQuery(
["people_list"],
() => axios.get("http://localhost:5008/people"),{
onSuccess: data => {
console.log("success", data);
}
});
console.log(people_query)
return (
<React.Fragment>
{/*peole_query가 로딩에 성공하면 렌더링*/}
{people_query.isSuccess ? (
people_query.data.data.map((person, index) => (
<div key={index}>
age: {person.age} height: {person.height}
</div>
))
) : null}
</React.Fragment>
);
}
export default App;
- 맨 처음에 useQuery가 데이터를 받아오지 않은 상태라서 isLoading이 true이고 isSuccess가 false이지만 데이터를 받아오면 useQuery의 isLoading이 false로 변하고 isSuccess로 변한 뒤에 리렌더링된다!!
- 데이터를 받아오기 전엔 people_query.data가 undefined이기 때문에 people_query.data.data를 사용하면 오류발생!
- useEffect를 쓰지 않아도 됨!!
- 위 코드는 아래 코드처럼 useQuery에서 필요한 값만 구조분해할당해서 쓸 수 있다!!
(쿼리 인스턴스 전체를 가져오기보다 data, isSuccess 등 필요한 정보만)
import {useQuery} from "react-query";
import React from "react";
import axios from "axios";
const App = () => {
// 필요한 것만 가져오기
const {data, isSuccess} = useQuery(
["people_list"],
() => axios.get("http://localhost:5008/people"),{
onSuccess: data => {
console.log("success", data);
}
});
console.log(data, isSuccess)
return (
<React.Fragment>
{/*peole_query가 로딩에 성공하면 렌더링*/}
{isSuccess ? (
data.data.map((person, index) => (
<div key={index}>
age: {person.age} height: {person.height}
</div>
))
) : null}
</React.Fragment>
);
}
export default App;
- 쿼리 인스턴스의 data와 isSuccess만 콘솔에 출력된다!
- 아래의 경우는 useQuery의 옵션이 refetchOnWindowFocus:true 이기 때문에 다른 탭에서 원래 화면으로 돌아올 때 데이터 패칭이 일어난다! 이것을 원하지 않으면 false로 바꾸자
+++ 주의사항
- 만약 위의 컴포넌트를 한번에 5개 사용하는 경우 "peole_list"라는 쿼리 키가 같기 때문에 query_function은 한번만 호출되지만 옵션에 넣은 onSuccess가 5번 실행된다!
- react-query를 사용해서 custom-hook을 만들 때 옵션에 들어가 있는 것들을 주의하자!
4. useMutation() - 데이터 수정
- useMutation은 데이터를 변경할 때 사용한다(POST/PUT/DELETE)
const peopleMutation = useMutation(
([데이터]) => axios.post([URL], [데이터]), {
onSuccess: () => {
queryClient.invalidateQueries([쿼리키])
}
});
...
people.mutate([데이터]);
- useQuery()와 같은 값 + mutate()를 리턴하는데 mutate는 Mutation을 실행하는 함수이고 파라미터로 바뀔 데이터를 넘겨주면 된다!
- 화면을 새로고침 하거나, 다른 탭에서 왔다가 오지 않아도 데이터를 화면에 바로 렌더링하려면 useQueryClient의 invalidateQueries()를 사용해서 useQuery로 만든 쿼리 인스턴스의 캐싱된 데이터를 초기화 시켜줘야 한다!
즉, useQuery로 생성한 쿼리키를 건드려서 서버에서 데이터를 자동으로 받아오게 하자!
useMutation() 예시
- 데이터를 변경한 후 화면에 바로 렌더링 해보자.
- input에서 데이터를 받아서 버튼을 클릭하면 서버에 데이터를 추가하고 useMutation의 onSuccess 옵션에서 서버에서 데이터를 받아오는 쿼리 인스턴스의 캐싱 데이터를 초기화시켜서 자동으로 데이터를 받아와서 화면에 나타나게 하자!
import {useMutation, useQuery, useQueryClient} from "react-query";
import React, {useRef} from "react";
import axios from "axios";
const App = () => {
const age_Ref = useRef();
const height_Ref = useRef();
// query 인스턴스 생성
const {data, isSuccess} = useQuery(
["people_list"],
() => axios.get("http://localhost:5008/people"), {
onSuccess: data => {
}
});
const queryClient = useQueryClient();
const peopleMutation = useMutation(
(person) => axios.post("http://localhost:5008/people", person), {
onSuccess: () => {
queryClient.invalidateQueries("people_list")
}
});
return (
<React.Fragment>
{/*peole_query가 로딩에 성공하면 렌더링*/}
{isSuccess ? (
data.data.map((person, index) => (
<div key={index}>
age: {person.age} height: {person.height}
</div>
))
) : null}
<input ref={age_Ref}/><span>age 입력</span>
<input ref={height_Ref}/><span>height 입력</span>
<button onClick={()=>{
const person = {
age: age_Ref.current.value,
height: height_Ref.current.value
}
// mutation 실행
peopleMutation.mutate(person)
}}>데이터 서버에 추가</button>
</React.Fragment>
);
}
export default App;
- 아래처럼 mutate를 구조분해할당으로 받아서 사용할 수도 있다!
import {useMutation, useQuery, useQueryClient} from "react-query";
import React, {useRef} from "react";
import axios from "axios";
const App = () => {
const age_Ref = useRef();
const height_Ref = useRef();
// query 인스턴스 생성
const {data, isSuccess} = useQuery(
["people_list"],
() => axios.get("http://localhost:5008/people"));
const queryClient = useQueryClient();
const {mutate} = useMutation(
(person) => axios.post("http://localhost:5008/people", person), {
onSuccess: () => {
//queryClient.invalidateQueries("people_list")
}
});
return (
<React.Fragment>
{/*peole_query가 로딩에 성공하면 렌더링*/}
{isSuccess ? (
data.data.map((person, index) => (
<div key={index}>
age: {person.age} height: {person.height}
</div>
))
) : null}
<input ref={age_Ref}/><span>age 입력</span>
<input ref={height_Ref}/><span>height 입력</span>
<button onClick={()=>{
const person = {
age: age_Ref.current.value,
height: height_Ref.current.value
}
mutate(person)
}}>데이터 서버에 추가</button>
</React.Fragment>
);
}
export default App;
++ 주의사항
- onSuccess 옵션으로 useQueryClient의 invalidateQueries()를 사용해서 캐싱된 데이터를 초기화시키지 않으면 바로 렌더링 되지 않고 새로고침하거나 다른 탭에서 돌아올 때 서버에서 데이터를 가져오는 쿼리 인스턴스의 콜백 함수가 실행된다
'리액트 심화 > react-query' 카테고리의 다른 글
useInfiniteQuery - 무한 스크롤 (0) | 2022.06.19 |
---|---|
react-query 모듈화 - Custom Hooks (0) | 2022.06.18 |
주니어 개발자에욤
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!