리액트 쿼리란, React 애플리케이션에서 서버 상태를 효율적으로 관리하기 위한 라이브러리 이다. 클라이언트 상태와는 달리 서버 상태는 비동기 데이터, 캐싱, 동기화, 갱신 등이 필요하기 때문에 이를 단순화하고 최적화 한다.
주요기능
1. 데이터 가져오기(Fetching)
2. 데이터 캐싱(Caching)
3. 데이터 무효화(Invalidation)
4. 자동 갱신(Auto Refetching)
5. 병렬 요청 및 의존성 관리
6. 뮤테이션(Mutation)
7. 서버 상태 동기화
리액트 쿼리를 사용하려면
npm install @tanstack/react-query
설치해준뒤,
최상위 컴포넌트로 가서
import {QueryClinet, QueryClientProvider} from 'tanstack/react-query';
const queryClient = new QueryClient;
const App() {
...
return (
<QueryClinetProvider client={queryClient}>
<HomePage />
</QueryClientProvider>
)
...
}
와 같이 해주면 된다.
QueryClientProvider는 React Context의 Provider와 유사한 역할을 한다. 그리고, React Query Devtools는 개발 중 서버 상태를 디버깅 할 수 있는 도구인데,
npm install @tanstack/react-query-devtools
로 설치가 가능하며, 이것도 최상위 컴포넌트에 import해주고,
<ReactQueryDevtools />와 같이 아무데나 쑤셔 박으면 된다. 또, initialsOpen옵션으로 Devtools기본 열림 여부 설정이 가능하다.
쿼리란, 문의하다. 질문하다.라는 뜻으로, 데이터베이스나 백엔드에 필요한 데이터를 요청하는 작업이다.
queryKey : 쿼리를 식별하는 고유 키queryFn : 데이터를 가져오는 함수(예 : API 호출)핵심 :
1. useQuery()리턴 값
const postData = data.result ?? [];와 같이 예시 :
import axios from 'axios';
const BASE_URL = 'https://example.co.kr';
export async function getPosts() {
const response = await axios.get(`${BASE_URL}/posts`);
return response.data;
}
// HomePage
import {useQuery} from '@tanstack/react-query';
import {getPosts} from './api';
function HomePage() {
const {data, isError} = useQuery({
queryKey : ['posts'] // 내가 지정
queryFn : getPosts // api함수 사용
});
if(isError) {
return <LoadingPage />
}
return (
<div>홈페이지</div>
)
}
정리된 흐름
1. API 함수 작성
2. useQuery훅 사용
핵심
1. Query Status 흐름
const {status, isPending, isError, isSuccess, error} = useQuery({
queryKey : ['posts'],
queryFn : getPosts,
});
if(isPending) return <LoadingPage />
if(isError) return <ErrorPage />
if(isSuccess) return <p>Welcome HomePage</p>
정리 :
1. Query Status
Refetch가 발생하는 상황
타임 설정
캐시 타임 설정 예제 :
const result = useQuery({
queryKey : ['posts'],
queryFn : getPosts,
staleTime : 60 * 1000 // 1분동안 fresh상태 유지
gcTime : 10 * 60 * 1000 // 10분 후 inactive상태의 데이터 삭제
})
Refetch 옵션 변경 예제
const result = useQuery({
queryKey : ['posts'],
queryFn : getPosts,
refetchOnMount : false, // 마운트 시 refetch하지 않음
refetchOnWindowFocus : true, // 창 포커스 시 refetch
refetchOnReconnect : true, // 네트워크 재연결 시 refetch
refetchInterval : 5000, // 5초마다 refetch
});
상태 변화 흐름
정리
1. 캐시 사용의 장점
['posts'] -> 전체 포스트를 식별['posts', username] -> 특정 유저의 포스트만을 식별예제 :
const {data : postsData} = useQuery({
queryKey : ['posts'], // 전체 포스트 데이터
queryFn : getPosts,
})
const {data : userData} = useQuery({
queryKey : ['posts', username], // 특정 유저의 포스트 데이터
queryFn : () => getPostsByUsername(username),
})
['posts']는 전체 포스트를 저장['posts', username] 은 특정 유저의 포스트를 저장['posts', username, {status : 'private'}]은 특정 유저의 private포스트만 저장예제 :
const {data : privatePostsData} = useQuery({
queryKey : ['posts', username, {status : 'private'}],
queryFn : () => getPrivatePostsByUsername(username),
});
const {data : postsDataByUserId} = useQuery({
queryKey : ['posts', {username}] // username이 포함된 객체 형태
queryFn : ({queryKey}) => {
const [key, {username}] = queryKey;
return getPostsByUserId(username);
}
})
1. useMutation이란?
정의
특징
1. 수동 실행
2. useMutation()기본 사용법
-- API함수 (POST, PUT, PATCH, DELETE) (예 : uploadPost [POST요청])
const uploadPostMutation = useMutation({
mutationFn : (newPost) => uploadPost(newPost)
});
const handleSubmit = (e) => {
e.preventDefault();
const newPost = {username : 'username', content};
uploadPostMutation.mutate(newPost);
setContent('');
}
요약
1. useMutation()
정의
queryClinet.invalidateQuries({queryKey : ['posts']})예제 :
const uploadPostMutation = useMutation({
mutationFn : (newPost) => uploadPost(newPost),
onSuccess : () => {
queryClient.invalidateQuries({queryKey : ['posts']}), // 쿼리무효화 및 refetch
},
onSettled : () => {
console.log('mutation has Settled');
}
});
uploadPostMutation.mutate(newPost, {
onSuccess : () => {
toast.info('업로드 성공');
}
});
주의 사항
1. useMutation() 콜백 vs mutate() 콜백
<button disabled={uploadPostMutation.isPending || !content}>업로드</button>
개념 요약
1. invalidateQueries()
Dependant Query는 어떤 데이터가 준비된 후에만 실행해야 하는 쿼리를 말한다. 예를 들어, 첫번째 쿼리의 결과가 준비되어야 두번째 쿼리를 실행할 수 있을때 이를 Depentdant Query라고 한다.
Dependant Query 예제
const {data : user} = useQuery({
queryKey : ['user', email],
queryFn : getUserEmail,
})
const userId = user?.id;
const {data : projects} = useQuery({
queryKey : ['Projects', userId],
queryFn : getProjectEmail,
enabled : !!userId // userId가 있을떄 (true)일때만 실행
})
enabled 옵션의 역할
예제 : 간단한 로그인 기능 만들기
// api.js
const BASE_URL = 'http://example.com';
export async function getUserInfo(username) {
const response = await.axios.get(`${BASE_URL}/users/${username}`);
return response.data;
}
// HomePage.js
function HomePage() {
const [currentUsername, setCurrentUsername] = useState('');
// 유저 정보 쿼리
const {data : userInfoData, isLoading, isError} = useQuery({
queryKey : ['userInfo', currentUsername],
queryFn : () => getUserInfo(currentUsername),
enabled : !!currentUsername, // currentUsername이 있을때만 실행
});
// 로그인 버튼 클릭시 유저명 설정
const handleLoginButtonClick = () => {
setCurrentUsername('exampleUser');
};
}
동작과정
1. 처음 화면
['userInfo']쿼리가 실행되지 않음enabled : !!currentUsername 조건이 참이 되어 ['userInfo']쿼리가 실행됨enabled가 왜 중요한가
한줄요약 : Dependant Query는 특정 조건이 충족될때만 실행되는 쿼리이며, 이를 설정하기 위해 enabled옵션을 사용한다.
useQuery에서 keepPreviousData로 로딩 중에도 이전 페이지 데이터를 유지할 수 있다.
구현흐름
1. API함수
export async function getPosts(page = 0, limit = 3){
const response = await axios.get(`${BASE_URL}/api/posts?page=${page}&limit=${limit}`);
return response.data;
}
const [page, setPage] = useState(0);
const INITIAL_LIMIT = 3;
const {
data : postsData,
isPending,
isError,
isPlaceholderData
} = useQuery({
queryKey : ['posts', page],
queryFn : () => getPosts(page, INITIAL_LIMIT),
placeholderData : keepPreviousData, // 다음 페이지 로딩 중이라면, 이전 페이지를 계속 보여줌
});
isPlaceholderData는 placeholderData : keepPreviousData를 설정해줬을때 사용할 수 있는데,
isPlaceholder 가 true => API호출 진행중...
isPlaceholder 가 false => API호출 완료!
이를 활용하여, API호출이 진행중일때 버튼을 비활성화 시킬 수도 있다.
...
useEffect(() => {
if(!isPlaceholderData && postsData?.hasMore) {
queryClient.prefetchQuery({
queryKey : ['posts', page + 1],
queryFn : () => getPosts(page + 1, INITIAL_LIMIT),
});
}
}, [...]);
...
핵심요약
1. keepPreviousData로 부드러운 데이터 전환이 가능하다.
2. isPlaceholderData : 임시 데이터 사용 여부
3. queryClient의 prefetchQuery로 다음 페이지에 대한 데이터를 백그라운드에서 미리 요청할 수 있다.
React Query는 queryKey를 기반으로 캐싱한다. 이 말은 캐시에 이미 queryKey와 동일한 값이 캐싱되어 있다면, 요청을 더이상 보내지않는다. 즉, queryKey안의 값이 바뀔때마다 서버에 요청을 보내는것이다.
핵심 옵션
1. queryKey
getNextPageParam : (lastPage, allPages, lastPageParam, allPageParams) =>
lastPage.hasMore ? lastPage + 1 : undefined
리턴값
1. data.pages
실제 코드 흐름
1. 데이터 가져오기 (useInfiniteQuery 설정)
const {
data : postsData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey : ['posts'],
queryFn : ({pageParam}) => getPosts(pageParam, INITIAL_LIMIT),
initialPageParam : 0,
getNextPageParam : (lastPage, allPages, lastPageParam, allPageParams) =>
lastPage.hasMore ? lastPageParam + 1 : undefined,
});
const postsPages = data?.pages ?? [];
!hasNextPage : 다음 페이지가 없을때isFetchingNextPage : 데이터를 요청중일 때<button onClick={fetchNextPage} disabled={!hasNextPage || isFetchingNextPage}>더 불러오기</button>
<ul>
{postsPages.map((postPage) => postPage.results.map((post) => <Post key={post.postId} post={post} />))}
</ul>
요약
1. useInfiniteQuery
queryFn : ({pageParam}) => ...