데브코스 1차 팀프로젝트를 시작하며 기술 스택에 대해 이야기하고 있던 중 나는 팀원들에게 React Query 도입을 제시했다.
개발 기간은 기획부터 시작해 한 달 정도의 시간이 있었고 좀 빠듯하다고 생각해서 React Query를 편리한 캐싱 기능, 코드 단축, 서버 상태 관리 등을 고려하여 React Query 라이브러리 필요성을 느꼇다.
하지만 팀원들이 사용 경험이 없어 많이 고민했지만 나는 이전에 사용 경험이 있어 더 적극적으로 추천하고 싶었다. 물론 사용에 있어 큰 숙련도를 요구한다고 생각하지도 않았고 오히려 한 번 사용에 익숙해지면 후에는 더 빠른 속도로 개발이 진행 될 수 있다고 생각했다.
문제는 나도 오랜만에 사용해서 찾아봤는데 이상한 이름으로 바뀌어 있었다. Tanstack Query?? 그리고 사용방법도 이전과는 미세하게 달라졌다. 인수를 객체형태로 전달하는 것, 쿼리 키가 반드시 배열 형태로 전달되어야 하는 등... 그래서 이 참에 바뀐 Tanstack Query를 정리해볼려고 한다.
본 포스팅은 자세한 설명보다는 사용방법 기준의 글로 작성할 예정이다.
React.. 아니 이젠 Tanstack Query로 서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 매우 쉽게 만들 수 있는 라이브러리다.
쉽게 말하면 서버 데이터를 관리하기 위해 원래는 useEffect, useState(data, isLoading, isError) 등 많은 작업이 따라오지만 Tanstack Query는 하나의 훅으로 관리에 필요한 모든 작업을 쉽게 도와준다.
그리고 새로운 리액트 문서에서도 서버 데이터 페칭을 해야하는 경우 useEffect 보다는 라이브러리 사용을 권장하고 있다. 관련 문서
useQuery는 비동기 데이터를 요청할 때 사용된다.
이전 버전에서는 인수의 형태가 key, function, option 이렇게 하나씩 받는 형태였다. 그리고 key 형태도 일반 문자열도 가능했었다. 다만 배열을 형태를 권장하긴 했지만.
const { data } useQuery([key], ansyncFn, options);
하지만 현재 v5 버젼에서는 하나의 객체에서 프로퍼티 키를 통해 모든 설정을 작성해야 했고 키 값도 배열 형태로 고정되었다.
이 바뀐 부분을 처음에 찾지 못해서 정말 애먹었다...
const { data: searchUserList, isLoading: searchIsLoading } = useQuery({
queryKey: [QUERY_KEYS.SEARCH_USER_LIST, values.userName],
queryFn: () => searchUsers(values.userName),
enabled: isSubmit && !errors.userName,
});
확실히 가독성이 더 좋아진 것 같긴하다 다른 사람이 보기엔 각 라인마다 어떤 역할을 하는지 확실히 알 수 있는 객체 형태이긴 하다.
많은 옵션이 있지만 자주 사용되는 몇 옵션만 살펴보자. TanStack 문서에 가면 더 많은 옵션을 볼 수 있다.
필수 값으로 쿼리에 사용할 쿼리 키며 배열형태이다. 해당 쿼리 키는 키 값에 데이터를 캐싱한다.
필수 값으로 데이터 패치할 함수이며 반드시 오류를 발생시키는 Promise를 반환해야한다. 데이터는 될 수 없다.
boolean 자료형으로 기본 값은 true다. 쿼리가 자동으로 실행되지 않도록 할려면 이 옵션을 통해 설정할 수 있다. false일 때는 쿼리가 실행되지 않으며 true일 때만 해당 쿼리가 실행된다.
기본 값은 3이며 쿼리가 실패하면 재시도를 해당 숫자를 충족할 때까지 수행한다. false는 재시도 하지 않고 true면 무한히 재시도 한다.
기본 값은 0이며 데이터가 오래된 것으로 간주하는 시간이다. 쿼리는 데이터가 오래된 것으로 판단되면 다시 데이터를 패칭하여 키를 업데이트 하며 데이터를 반환한다.
만약 아직 신선한 데이터 시간이면 항상 캐싱 된 데이터를 바로 사용하며 데이터 패칭이 이루어지지 않는다.
기본 값은 5 60 1000 (5분)이며 캐싱된 데이터의 유효 시간이다. 캐싱 시간이 지나면 가비지에 수집되어 제거된다.
해당 시간 동안 쿼리를 호출해도 데이터를 패치하지 않고 캐시된 결과를 사용한다.
새로고침하면 캐시는 사라지고 메모리에 캐싱된 데이터가 있다하더라도 stale 오래된 상태라면 새로 데이터를 패칭하여 캐시를 덮어버린다.
Infinity설정시 가비지 수집되지 않음.
함수 형태로 반환받은 데이터를 인자로 받아 data 변수에 저장하기 전에 데이터를 가공하는 목적을 가진 옵션이다.
예를 들어 특정 데이터만 filter하고 싶을 때나 등 사용할 수 있다. 그리고 캐싱된 데이터에는 영향을 주지 않고 반환 값에만 영향을 준다.
첫 호출 시 한 번 실행되며 초기 반환 값 데이터다. data가 처음엔 undefined인데 initalData로 덮어쓸 수 있다.
정수 자료형으로 일정한 간격으로 쿼리를 자동으로 요청하는 기능이다. 설정된 밀리초 주기마다 자동으로 데이터를 다시 요청한다.
이 옵션으로 1차 프로젝트 실시간 기능을 polling 방식으로 해결했다.
처음에는 좀 이해하기 힘들었는데 첫 마운트 때 실행되는 쿼리 패칭이랑 캐싱된 데이터가 있고 후에 백그라운드에서 진행되는 쿼리 패칭을 분리하면 쉽게 이해할 수 있었다.
기본 값은 undefined(패치가 이루어지지 않았을 때)으로 쿼리가 성공적으로 수행되면 반환받는 데이터 값
기본 값은 null이며 오류가 발생한 경우 쿼리에 대한 오류 정보 객체
쿼리를 수동으로 다시 가져오는 함수이며 특정 로직에서 데이터를 다시 가져와야할 때 refetch를 호출하면 된다.
status(string) - 첫 쿼리 데이터 패칭
isPending(boolean)
status 편의를 위해 pending 값을 boolean 값으로 제공
status 편의를 위해 success 값을 boolean 값으로 제공
status 편의를 위해 error 값을 boolean 값으로 제공
fetchStatus 편의를 위해 fetching 값을 boolean 값으로 제공
fetchStatus 편의를 위해 paused 값을 boolean 값으로 제공
fetchStatus 편의를 위해 paused 값을 boolean 값으로 제공
백그라운드에서 다시 쿼리 패칭을 진행 중일 때 발생하며 초기 쿼리 패칭에는 해당하지 않는다.
첫 쿼리 패칭이 아닌 백그라운드 쿼리 패칭할 때 실패한 경우
기본 값은 false이며 첫 쿼리 데이터 패칭이 진행 중인 경우 true 값
첫 쿼리 데이터 패칭이 실패한 경우
useMuation은 서버 데이터를 변경할 때 사용한다.
바뀐 useMutation은 반환 값이 좀 달라져서 당황했다. 다른 부분은 이전과 거의 비슷했다.
데이터를 저장하지 않으므로 별도의 key 값을 필요하지 않으며 isFetching 기능도 없다.
const { mutate } = useMutation({
mutationFn: readNotifications,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.NOTIFICATION_LIST],
});
},
});
자세한 옵션은 관련 문서 참고
필수 값으로 Promise를 반환하는 비동기 함수이며 mutate에게 전달될 객체이다.
mutate 함수가 실행되기 전에 실행되는 함수이며 mutate 함수가 반환 받는 동일한 변수가 전달된다.
이 기능은 낙관적 업데이트 수행하는데 유용하다. 이 함수에서 반환된 값은 에러 발생 시 모두 onError 함수에 전달되며 낙관적 업데이트를 롤백하는데도 사용한다.(onSettled)
mutate 함수가 성공하면 실행되는 함수이며 응답 받은 데이터가 전달된다.
mutate 함수에 오류가 발생하면 실행되는 함수고 오류가 전달된다.
mutate함수가 성공하거나 오류가 발생해도 반드시 실행되는 함수이며 데이터 또는 오류가 전달된다.
데이터를 변경하는 트리거 함수이다. 호출 시 선택적으로 추가 콜백 옵션을 연결할 수 있다.
mutate({
onSuccess: () => {},
...
})
mutateAsync는 비동기 Promise의 형태의 mutate 함수다. 기본 mutate는 Promise를 실행한 결과를 반환하는 함수이지 실제 Promise 함수는 아니다.
기본값은 undefined이며 mutate가 성공적으로 응답받은 데이터다.
오류가 발생한 경우 쿼리에 대한 오류 객체 데이터다.
사실 이번 프로젝트를 통해 처음 사용해본 기능이며 제일 기억에 남았다. 무한 스크롤을 구현하면서 일반 useState로 구현하다가 잘 해결이 되지 않았는데 useInfinityQuery를 사용하면서 정말 쉽고 가독성 좋게 해결했다.
더 많은 사용방법은 관련 문서를 참조하면 된다.
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: [QUERY_KEYS.USER_LIST],
queryFn: ({ pageParam }) => getUsers({ offset: pageParam, limit }),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) =>
lastPage?.length ? allPages.length * limit : null,
select: ({ pages }) =>
pages.flatMap(
(page) => page?.filter((user) => user?.role !== 'SuperAdmin') ?? [],
),
});
먼저 옵션 기능을 살펴보자
useQuery와 동일하게 배열 형태로 지정해주면 된다.
데이터를 가져오는 함수이며 인자로 객체 안에 pageParam 값이 있다. 이 값이 현재 페이지를 나태는 데이터며 비동기 실제 페이지네이션이나 무한 스크롤의 시작 값이라 보면 된다.
초기 pageParam 값을 설정할 수 있다.
다음 pageParam을 설정하는 함수이며 현재 페이지가 마지막인 경우를 null을 반환하면 판단할 수 있다.
데이터를 가공하는 함수로써 나는 현재까지 조회된 모든 페이지를 불러와 어드민 부분을 필터링 하도록 사용했다.
data
데이터가 조회된 모든 페이지 정보를 가지고 있다.
fetchNextPage,
다음 페이지 데이터를 패칭하는 함수이며 호출 시 다음 페이지를 불러오는 비동기 함수가 호출된다.
hasNextPage,
다음 페이지의 존재 여부를 반환하는 데이터이며 boolean 값이다.
isFetchingNextPage,
다음 페이지를 패칭하는 중 여부를 반환하는 데이터이며 boolean 값이다.