리액트에서는 상태관리라는 것을 통해서 렌더링을 하게 된다.
말만 어렵지 그냥 변수에 값(데이터) 할당하고 그에 맞게 렌더링 한다는 뜻이다.
useState으로 지정하고 setState로 수정하고 어쩌고지만 사실 그냥 변수에 값 할당하는건데 불변성 등등 때문에 getter, setter처럼 사용하는데 이런 State를 상태라고 한다. (스벨트는 state고 나발이고 직관적으로 변수에 넣던데...)
유저가 true면 이렇게~ false면 저렇게~ 여기서 유저가 상태다.
아니 그래서 리액트 쿼리가 뭐냐면 서버 상태 관리 라이브러리이다. 서버 상태 관리 -> 서버 데이터 관리 -> 서버에서 받아온 데이터 관리 라이브러리. 즉 서버에서 데이터 받아오고 관리하는걸 도와주는 녀석이다. 사실 알게된건 예전에 알았는데 조금 거들떠보고 '그냥 useEffect에 fetch() 호출하면 될 것을 굳이?' 라고 생각하고 접었는데 현 프로젝트에서 사용해서 학습해보니 굉장히 유용하다는 것을 깨달았다.
Redux가 왜 점점 배척되어 가는가?
너무 더럽기 때문이다.
Redux-toolkit? Vuex 따라한거 같은데 그래도 여전히 복잡하다.
그래서 점점 인기는 식어가고 다른 대체재들이 떠오르고 있다.
React Query의 장점중 하나는 사용시 코드가 굉장히 깔끔하다는 것이다.
한번 얼마나 깔끔해지는지 보자.
import { useState, useEffect } from 'react'
import './App.css'
import axios from 'axios';
import { useQuery } from 'react-query';
type PostType = {
id: string,
title: string
}
const getPosts = async () => {
const { data } = await axios.get('http://localhost:4000/posts');
return data;
}
function App() {
const { isLoading, isFetching, isError, data } = useQuery<PostType[], unknown>('posts', getPosts);
if (isLoading) {
return (
<div>Loading...</div>
)
}
if (isError) {
return (
<div>Error...</div>
)
}
return (
<div>
{
data?.map(post => {
return (
<div key={post.id}>
{
post.title
}
</div>
)
})
}
</div>
)
}
export default App
너무 깔끔하지 않은가?
useEffect, useState 등도 안써도 된다.
속이 편안해진다.
useQuery<PostType[], any>('posts', getPosts,{});
제네릭 안에 첫번째 인자는 Data 타입, 두번째 인자는 에러 타입이다.
useQuery의 첫번째 인자는 query key값으로 이 값을 통해서 각 query들을 구분한다.
이게 뭔말이냐면 <div key={post.id}></div>
처럼 각 쿼리 키 값을 구분을 해줘야 된다는 말이다.
상품 조회 쿼리처럼 id값에 따라 조회를 한다면 배열로 (['product',productId]);
이렇게 쿼리값을 넣어주면 된다.
세번째 인자는 옵션을 줄 수 있는데 이 옵션이 가장 중요하므로 뒷부분에서 다루겠다.
React Query는 처음 데이터를 fetching 해올 때에는 isLoading이 true이다.
말 그대로 로딩중이니까.
하지만 한번 fetching을 해오고 나면 default 값인 5분동안 캐싱을 해놓는데 이 시간동안에는 다시 방문해도 원래 받아온 데이터를 보여주게 된다. 따라서 이때에는 isLoading이 false로 설정해둔 로딩 애니메이션등은 렌더링 되지 않는다.
하지만 이미 캐싱된 데이터가 다시 들어갔을때 만약 바뀌어져 있다면? 걱정할 필요없다. 백그라운드에서 fetching 해오면서(이때는 isFetching이 true) 변한 부분을 업데이트 해준다. 즉 5분가량은 다시 접속시에 캐싱된 데이터 보여주고 그 다음에 백그라운드에서 작업을 거쳐 변한부분을 업데이트 하는 것이다.
캐싱 시간을 설정해 줄 수 있다.
기본 값은 5분 가량으로 밀리세컨즈로 원하는 시간만큼 설정이 가능하다.
Infinity로 설정하면 한번 받아온 값을 무한대로 캐싱해놓는다.
{
cacheTime: Infinity
}
staleTime을 설정해주면 그 시간만큼 백그라운드에서 data fetching을 하지 않는다.
왜 백그라운드에서 fetching을 하지 않게 설정해주냐고?
유저가 이곳저곳 계속 왔다갔다하는데 그럴때마다 백그라운드에서 네트워크 요청하면 자주 안바뀌는 페이지는 비용낭비일것 아님? 그럴때 사용하면 된다. Default는 0이여서 항상 백그라운드에서 fetching 해오게 설정되어 있다.
{
staleTime: 6000
}
refetchOnMount는 백그라운드에서 fetching 해올지 말지를 정하는 옵션이다.
캐쉬한번 하고 나면 그냥 계속 캐쉬된 값 보여주고 다시 fetching 해오지 않을거면 사용하면 된다.
Default는 true
{
refetchOnMount: false
}
처음 react query 사용할 때 이게 가장 신기했었는데 유저가 페이지에서 포커스를 잃었다가 다시 들어오면 fetching을 다시 해온다.
이게 뭔말이냐면 내가 A라는 웹사이트를 보고 있다가 카톡하려고 알트(커맨드)탭으로 카톡하고 다시 A사이트로 돌아오면 refetching을 시작한다. 새탭을 열어서 그 탭에서 브라우징 하다가 다시 돌아와도 마찬가지다. 그래서 다시 돌아왔을때 데이터가 변경 되있으면 업데이트 된 데이터를 보여준다.
Default는 true
{
refetchOnWindowFocus: false
}
이 옵션은 useQuery가 useEffect마냥 처음에 무조건 실행할 것인지를 설정하는 옵션이다.
Default는 true로 무조건 실행이 되는데 유저의 클릭이벤트 등에 사용할때 false
로 설정 후 refetch
를 이용해 사용할 수 있다.
const { isLoading, isError, data, refetch } = useQuery<PostType[], unknown>('posts', getPosts,{
enabled: false
});
<div onClick={()=>refetch()}>Fetch</div>
각각 api호출 성공, 실패시에 하고싶은 것들을 해줄 수가 있다.
{
onError: (err) => {
console.log(err.message);
router.push('/error');
}
}
select 옵션을 통해서 각 데이터 변형이 가능하다.
{
select: data => data.reverse()
}
아까 말했듯이 query key의 데이터를 처음 fetching할때 isLoading이 true이다.
근데 isLoading 대신에 유저가 다른곳에서 받아온 방금 전 데이터를 그냥 보여주고 fetching해와서 업데이트 하고 싶다면 이 옵션을 사용하면 된다.
아니 이런 옵션을 대체 왜 필요하고 어디서 씀? 이라고 생각할 수 있는데 pagination 클릭했을때 안에 목록만 쓰윽 바꾸고 싶을때 사용하면 된다. 유저가 2페이지 보고 있다가 3페이지를 눌렀을때 로딩 애니메이션을 보여주는게 아니라 2페이지의 데이터를 보여주다가 새롭게 받아온 3페이지의 데이터로 쓰윽 갈아치울때 사용하면 된다. Gmail 같은 느낌 생각하면 될듯.
Default는 false
{
keepPreviousData: true
}
데이터를 가져올때 초기값을 설정해 줄 수 있다.
이것도 초기값을 왜 설정하지? 라고 생각할 수 있는데 이걸 활용해서 UX를 조금이나마 향상시킬 수 있다.
일단 우리는 특정 페이지에 어떤 값이 들어갈지 유추가 가능한 경우가 있다.
예를 들어 "제 3기 결산공고"를 클릭해서 들어가면 공고 내용이 나올거 아님?
근데 타이틀은 거의 무조건 똑같이 "제 3기 결산공고" 하고 밑에 내용이 나올테니까 이런경우에 initialData를 설정해 줄 수 있다. 이커머스 상품페이지도 마찬가지일듯.
{
initialData: () => {
useQueryClient().getQueryData('리스트 목록 쿼리 키');
}
}
스크롤이 하단에 다다랐을때 우리는 새롭게 fetching을 해와야 할 때가 있다.
인스타 돋보기 처럼 일정부분되면 fetching 해오고 싶을 때 또는 load more 등의 버튼을 눌러서 새롭게 업데이트 할 때이다.
useInfiniteQuery는 사용법이 좀 다르고 약간 복잡해서 추후 정리해서 다시 업데이트 할 예정이다.
앞에서는 데이터를 가져오는 방법과 옵션들을 살펴봤는데 데이터를 보내는데에도 사용이 가능하다.
즉 CRUD에서 CUD에도 사용이 가능함.
useMutation을 사용하면 되는데 특이한 점은 query key값이 필요하지 않다는 것이다.
const postUserInfo = async (userInfo: UserType) => {
try {
const { data } = await axios.post('http://localhost:4000/user', userInfo);
return data;
} catch (err: any) {
throw new Error(err.response.status);
}
}
function App() {
const inputRef = useRef<HTMLInputElement>(null!);
const {mutate, isLoading, isError, error} = useMutation(postUserInfo);
return (
<div>
<input type="text" ref={inputRef} />
<button
disabled={isLoading}
onClick={() => mutate({ name: inputRef.current.value })}
>
Send
</button>
</div>
)
}
너무 간단하다..
여러 API 호출이 필요할 때 어떻게 할까?
그냥 여러개 호출하면 됨.
Alias로 구분해서 사용하면 된다.
const {
isLoading: isPostLoading,
isError: isPostError,
data: postData,
error: postError
} = useQuery<PostType[], any>('posts', getPosts);
const {
isLoading: isProductLoading,
isError: isProductError,
data: productData,
error: productError } = useQuery<ProductType[], any>('products', getProducts);
순서대로 API 호출이 필요할 때는 어떻게 할까?
enabled: data!!
로 꼼수를 부리면 된다.
1번 API 호출 성공후 2번 API 해야되면 1번 API 데이터값을 2번 enabled에 넣어준다.
{
enabled: postData!!
}
useMutation 사용시에 팁이다.
To-Do List를 간단하게 구현한다고 가정했을때 useMutation으로 post후 보낸값을 바로 렌더링하고 싶으면 어떻게 해야할까?
const queryClient = useQueryClient();
const { mutate } = useMutation(postUserInfo, {
onSuccess: (data) => {
queryClient.invalidateQueries('users');
}
});
const queryClient = useQueryClient();
const { mutate } = useMutation(postUserInfo, {
onSuccess: (data) => {
queryClient.setQueryData('posts', (oldQueryData: any) => {
return [
...oldQueryData,
data
]
})
}
});
실제 사용시에 폴더 구조에 대해서 알아보자.
보통 폴더를 하나 만들고 관련 API에 맞는 파일을 만든다.
한 파일안에 axios 및 useQuery에 관련된 코드를 넣고 useQuery 결과값을 return한다.
사용시에는 컴포넌트에서 import해서 사용한다.
이렇게 하면 axios 코드따로 useQuery로직 따로 왔다갔다 안해도 되고 import 해오는 파일에서 한눈에 확인이 가능하다.
import { useQuery } from "react-query";
import axios from 'axios';
export const getPosts = async () => {
try {
const { data } = await axios.get('http://localhost:4000/posts');
return data;
} catch (err) {
throw new Error(err.response.status);
}
}
export const useGetPostsData = (onError, onSuccess) => {
return useQuery('posts', getPosts, {
onError,
onSuccess
})
}
import { useGetPostsData } from './hooks/usePostsData';
export default function App(){
const {isLoading, isError, data, error} = useGetPostsData();
}
React Query를 쓰면 코드가 굉장히 간단해지는 것만 해도 충분히 사용 가치가 있다고 생각든다.
뿐만 아니라 여러 옵션들로 특정 기능을 구현해야할때 간편하게 구현할 수 있다는 이점이 있다.
@geometry dash This was somewhat satisfactory yet still felt awkward. It seems that React-query is often substituted.
Redux를 사용하자니 너무 복잡하고 Redux-toolkit을 사용했는데 나름 괜찮았지만 그래도 여전히 불편한감이 없지 않아 있었는데.. React-query가 대체재로 많이 사용되나 보네요
좋은 글 감사합니다!