[React-query] next 프로젝트에 react-query(tanstack query) 도입하기

Gyuhan Park·2024년 2월 1일
7

Tech

목록 보기
1/3

react 외에 다른 프레임워크도 지원하는 것을 나타내기 위해 v4버전부터 tanstack query 로 이름이 변경되었다.

💭 TMI (react query 도입 이유)

다양한 서버 데이터를 호출하여 비동기 데이터를 React state로 관리하다보니 컴포넌트의 복잡성이 높아졌다. useEffect로 외부 데이터와 동기화를 시키는 코드가 많아지면서 코드를 파악하기 어려워졌다. 해당 state가 클라이언트 데이터인지 서버 데이터인지 로직을 모두 확인해야 알 수 있었다.

또한 같은 API를 호출하면서 매번 호출한다는 점이 아쉬웠다. 그렇다고 SSG로 만들기엔 위키 특성상 데이터가 변할 수 있는 상황이 많았고 이를 즉각적으로 보여주는 게 사용성에 좋다고 생각했다.

react query를 사용하면 비동기 데이터 로직을 분리 하여 컴포넌트의 북잡성을 줄이며, 서버 데이터를 캐싱 하여 API 호출횟수를 줄인다는 점에서 위의 2가지 고민을 해결할 수 있었다. 이와 같은 이유로 팀원들을 설득하여 함께 react query를 도입하기로 결정하였다. 각자가 맡은 도메인은 팀원들이 더 잘 알기도 하고 전체적으로 프로젝트를 개선해보고자 내가 공부한 내용을 정리해보려고 한다.

📘 프로젝트 적용하기

  1. 최상위 컴포넌트에 QueryClient 생성하여 주입
  2. next app directory 사용 시 prefetch를 위해 QueryClient 인스턴스 주입
  3. 데이터 조회(GET) → useQuery
  4. 데이터 수정(POST, PUT, DELETE) → useMutation

✅ useQuery

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,
})

✅ useQuery 적용 예시

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로 인한 로직 분리

기존에 서버 데이터 불러오는 로직을 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]);
	  ...

✅ useMutation

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;

📘 background refetch

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

profile
단단한 프론트엔드 개발자가 되고 싶은

2개의 댓글

comment-user-thumbnail
2024년 2월 1일

깔끔한 정리 감사합니다! 도움이 많이 됐어요

1개의 답글