react 외에 다른 프레임워크도 지원하는 것을 나타내기 위해 v4버전부터
tanstack query
로 이름이 변경되었다.
다양한 서버 데이터를 호출하여 비동기 데이터를 React state로 관리하다보니 컴포넌트의 복잡성이 높아졌다. useEffect로 외부 데이터와 동기화를 시키는 코드가 많아지면서 코드를 파악하기 어려워졌다. 해당 state가 클라이언트 데이터인지 서버 데이터인지 로직을 모두 확인해야 알 수 있었다.
또한 같은 API를 호출하면서 매번 호출한다는 점이 아쉬웠다. 그렇다고 SSG로 만들기엔 위키 특성상 데이터가 변할 수 있는 상황이 많았고 이를 즉각적으로 보여주는 게 사용성에 좋다고 생각했다.
react query를 사용하면 비동기 데이터 로직을 분리
하여 컴포넌트의 북잡성을 줄이며, 서버 데이터를 캐싱
하여 API 호출횟수를 줄인다는 점에서 위의 2가지 고민을 해결할 수 있었다. 이와 같은 이유로 팀원들을 설득하여 함께 react query를 도입하기로 결정하였다. 각자가 맡은 도메인은 팀원들이 더 잘 알기도 하고 전체적으로 프로젝트를 개선해보고자 내가 공부한 내용을 정리해보려고 한다.
- 최상위 컴포넌트에
QueryClient
생성하여 주입- next app directory 사용 시 prefetch를 위해 QueryClient 인스턴스 주입
- 데이터 조회(GET) →
useQuery
- 데이터 수정(POST, PUT, DELETE) →
useMutation
GET
요청과 같이 서버에 저장되어 있는 상태를 불러올 때 유용
queryKey와 queryFn을 제외한 나머지는 optional 이지만, 중요한 개념과 유용하게 사용한 기능만 작성해보았다.
required
queryKey
: 이 Query 요청에 대한 응답 데이터를 캐시할 때 사용할 Unique Key
queryFn
: 이 Query 요청을 수행하기 위한 Promise를 Return 하는 함수
optional
staleTime
: 쿼리가 신선한 상태에서 신선하지 않은 상태로 변할 때 까지의 소요 시간 (default : 0)
gcTime(cacheTime)
: 비활성화된 쿼리가 캐시로부터 제거되기까지의 소요 시간 (default : 5분)
select
: 서버에서 불러온 데이터를 가공할 경우 select 옵션 사용. data가 존재할 때에만 호출됨.
enabled
: enabled가 true여야 query 실행. 이전 쿼리에 영향을 받을 경우 사용 (default : true)
// Get the user
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
TkDodo님 블로그에 따르면 커스텀 훅을 생성하는 것을 권장한다.
데이터 불러오는 로직을 컴포넌트와 분리할 수 있고, 쿼리에 대한 모든 설정을 하나의 파일에서 처리할 수 있기 때문이다.
useSearchQuery라는 커스텀 훅을 만들어 useQuery를 적용하였다.
검색결과를 캐싱하기 위해 검색어를 파라미터로 받고 쿼리키에 포함하였다. enabled로 검색어가 존재할 때만 쿼리가 동작하며, 쿼리에 대한 결과값을 60초간 fresh하도록 유지하였다.
export const useSearchQuery = (searchKeyword: string) => {
const { data: searchResult } = useQuery({
queryKey: ['search', searchKeyword],
queryFn: () => getSearchResult(searchKeyword),
enabled: !!searchKeyword,
staleTime: 60 * 1000,
});
return searchResult;
};
기존에 서버 데이터 불러오는 로직을 useQuery로 개선한 코드다.
fetch 로직에 해당하는 코드만 가져왔는데 복잡했던 컴포넌트가 간단해진 것을 확인할 수 있다.
Before
import { getSearchResult } from '@/apis/docs';
import { ISearchResult } from '@/types/request';
import Loading from '../common/Loading';
const SearchBodySection = () => {
const [searchResult, setSearchResult] = useState<ISearchResult[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (searchKeyword) {
getSearchResult(searchKeyword).then((res) => {
if (Array.isArray(res)) {
setSearchResult(res);
} else if (res.titleMatched) {
let encodedTitle = encodeURIComponent(searchKeyword);
router.push(`viewer?title=${encodedTitle}`);
}
});
}
}, [router, searchKeyword]);
...
After
import { useSearchQuery } from '@/hooks/useSearchQuery';
const SearchBodySection = () => {
const searchResult = useSearchQuery(searchKeyword);
useEffect(() => {
if (searchResult && searchResult.kind === 'searchResult') {
const encodedTitle = encodeURIComponent(searchKeyword);
router.push(`viewer?title=${encodedTitle}`);
}
}, [searchResult]);
...
POST, PUT, DELETE
요청과 같이 서버에 Side Effect를 발생시켜 서버의 상태를 변경시킬 때 사용
mutationFn
: 이 Mutation 요청을 수행하기 위한 Promise를 Return 하는 함수
invalidateQueries('queryKey')
: useTodosQuery에서 불러온 API Response의 Cache를 초기화
invalidateQueries를 사용하여 캐싱된 쿼리를 초기화해야 데이터를 새로 불러오면서 변경된 서버 상태를 가져올 수 있다.
useMutation은 적용해보지 않아서 잘 설명되어 있는 카카오페이 블로그의 예시를 그대로 가져왔다.
// quires/useTodosMutation.ts
import axios from 'axios';
import { useMutation, useQueryClient } from 'react-query';
import { QUERY_KEY as todosQueryKey } from './useTodosQuery';
// useMutation에서 사용할 `서버에 Side Effect를 발생시키기 위해 사용할 함수`
// 이 함수의 파라미터로는 useMutation의 `mutate` 함수의 파라미터가 전달됩니다.
const fetcher = (contents: string) => axios.post('/todos', { contents });
const useTodosMutation = () => {
// mutation 성공 후 `useTodosQuery`로 관리되는 서버 상태를 다시 불러오기 위한
// Cache 초기화를 위해 사용될 queryClient 객체
const queryClient = useQueryClient();
return useMutation(fetcher, {
// mutate 요청이 성공한 후 queryClient.invalidateQueries 함수를 통해
// useTodosQuery에서 불러온 API Response의 Cache를 초기화
onSuccess: () => queryClient.invalidateQueries(todosQueryKey),
});
};
export default useTodosMutation;
react-query는 stale한 쿼리를 자동으로 백그라운드에서 재요청한다.
이러한 옵션을 사용하여 쿼리가 언제 다시 요청되어야 하는지를 조정할 수 있다. 하지만 보통 기본값으로 두고, 쿼리가 자주 요청되는 경우 staleTime을 조정하는 것을 권장한다고 한다.
새로운 쿼리 인스턴스가 마운트될 때 :
refetchOnMount
창이 다시 포커스될 때 :refetchOnWindowFocus
네트워크가 다시 연결될 때 :refetchOnReconnect
쿼리가 선택적으로 재요청 간격을 설정했을 때 :refetchInterval
refetchOnWindowFocus
: 데이터가 stale 상태일 경우 윈도우 포커싱 될 때 마다 refetch를 실행하는 옵션 (default : true)
refetchOnMount
: 데이터가 stale 상태일 경우 마운트 시 마다 refetch를 실행하는 옵션 (default : true)
refetchOnReconnect
: 데이터가 stale 상태일 경우 재 연결 될 때 refetch를 실행하는 옵션 (default : true)
refetchInterval
: stale 상태를 기준으로 판단하는 위의 3가지 옵션과 달리 일정 시간을 기준으로 refetch를 실행하는 옵션. 대신 브라우저가 포커싱 되어야함. (default : false)
refetchIntervalInBackground
: 포커싱하지 않아도 백그라운드에서 refetch하는 옵션 (default : false)
https://tanstack.com/query/latest/docs/framework/react/overview
https://tkdodo.eu/blog/practical-react-query
https://tech.kakaopay.com/post/react-query-1/
https://velog.io/@rmaomina/useQuery-refetch-options
https://velog.io/@kimhyo_0218/React-Query-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%BF%BC%EB%A6%AC-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-useQuery
깔끔한 정리 감사합니다! 도움이 많이 됐어요