Tanstack Query의 캐싱과 로딩 중일 때의 UI
상태 관리
2024.12.10.

Tanstack Query (React Query)

Tanstack Query v4 기준으로 작성된 글입니다.

Tanstack Query(기존의 React Query)는 흔히 웹 애플리케이션을 위한 데이터 가져오기 라이브러리로 설명되지만, 보다 기술적인 측면에서 보면 웹 애플리케이션에서 서버 상태를 쉽게 가져오기, 캐싱, 동기화 및 업데이트할 수 있게 해줍니다.

Tanstack Query 공식 홈페이지의 원문을 DeepL로 번역한 결과이다.

Tanstack Query로 서버 상태를 관리할 때, 상황에 따라 서버의 데이터와 항상 동기화 해야할 때가 있고, 적당한 시간동안 클라이언트에서 데이터를 캐싱한 뒤 특정 조건에서 서버 상태와 다시 동기화 해도 될 때가 있다.

적절한 상황에서 캐싱을 사용해 서버 상태를 관리한다면 서버 요청 비용을 절약할 수 있다.

서버 요청 비용을 절약하면 서버 부하를 줄이고 응답 속도를 높일 수 있다. 또한, 클라우드 서비스 비용을 절감하고 네트워크 대역폭 사용을 최소화해 더 원활한 사용자 경험을 제공할 수 있다.

캐싱 잘 사용하기

staleTime과 cacheTime

staleTime은 데이터가 얼마나 오래 신선한 상태로 간주될지 설정하는 속성이다. 이름 그대로, "stale"은 신선하지 않음을 뜻하며, 이 시간 이후 데이터는 신선하지 않은 것으로 판단된다.

cacheTime은 캐싱된 데이터를 얼마나 오래 메모리에 보관할지 설정하는 속성이다. 이 속성의 원래 이름은 gcTime(Garbage Collection Time)으로, 캐싱 데이터를 삭제하기 전까지의 시간을 의미한다.

cacheTime은 반드시 staleTime보다 같거나 커야 한다. 캐싱된 데이터가 메모리에 있어야만 그것이 신선한 상태인지, 신선하지 않은 상태인지 판단할 수 있기 때문이다.

이 두 속성은 비슷해 보이지만 서로 다른 역할을 한다. staleTime은 데이터를 새로 요청할 필요가 있는지 결정하고, cacheTime은 데이터를 메모리에서 유지할 시간을 결정한다.

staleTime < cacheTime

  • staleTimecacheTime보다 작은 경우 staleTime이 지나면 cacheTime이 지나면 데이터는 stale 상태로 간주된다. 그러나 cacheTime이 지나기 전까지 데이터는 메모리에 남아 있어 사용할 수 있다.

staleTime === cacheTime

  • staleTimecacheTime과 같은 경우 cacheTime 동안 fresh한 데이터를 유지한다. (invalidQueries와 같이 고의적으로 stale하게 변경하지 않는 이상)
  • 이 설정은 명확하고 직관적이며, 데이터가 일정 기간 동안만 유효하길 원하는 경우 적합하다.

staleTime > cacheTime

  • staleTimecacheTime보다 큰 경우 cacheTime이 지나면 데이터는 메모리에서 삭제된다.
  • 이 설정은 일반적으로 적합하지 않다. cacheTime이 지나 데이터가 삭제되면 이후의 staleTime은 아무런 의미가 없기 때문이다.

로딩 중일 때 보여줄 화면

stale한 데이터를 보여주는 경우

오래된 데이터를 보여주는 것이 로딩 화면 또는 빈 화면을 보여주는 것보다 더 나을 때 유용하다.

오래된 데이터를 잠깐 보여주는 것이 UI에 긍정적일지 판단한 후 적용하는 게 좋다.

  • 프레이밋 프로젝트에서 필터링 예시

  1. 필터 조건에 따라 쿼리를 캐싱하는 상황
  2. 전체 필터를 조회한 후 작가 필터를 조회함. 각 필터 조건의 리스트가 캐싱됨
  3. 작가 필터 화면에서 특정 아이템을 북마크함 + invalidateQueries 로 모든 필터 조건에 따른 데이터를 invalidate 시킴.
  4. 전체 필터 데이터를 조회함 → invalidate(북마크하기 전의) 데이터가 refetch되기 전까지 잠깐 보여짐 → refetch 완료된 후 바로 새로운 데이터가 보임

개인적으로는 위와 같은 깜빡임은 없는 게 나아보인다. 이런 개발과 UI의 간극은 기획자 혹은 디자이너와의 적절한 소통을 통해 메꾸는 게 중요한 것 같다.

stale한 데이터를 화면에 보여주고 싶지 않을 경우

  • isStale 속성 사용

    isStale은 쿼리 데이터가 stale한지 확인할 수 있다. stale할 때 화면에 로딩 스피너를 보여주고 싶다면 다음과 같이 코드를 작성할 수 있다.

    const {
      data,
      isLoading,
      isStale,
    } = useRecruitmentsQuery(filter);
    
    /* 중략 */
    
    return (
    	<>
    	  {isLoading || isStale ? (
    	    <LoadingSpinner />
    	  ) : (
    		  <div>{쿼리 데이터 컴포넌트}</div>
    	  )}
      </>
    );
    
  • cacheTimestaleTime 을 동일하게 + removeQueries 사용

    만약 cacheTimestaleTime 이 동일하게 설정되어 있는 상황일 때는 invalidateQueries 가 실행되기 전까지 캐싱 데이터가 fresh한(stale하지 않은) 상태로 살아있다. 이럴 때 stale한 데이터가 필요 없다면 removeQueries로 캐시 데이터를 삭제할 수 있다.

keepPreviousData

keepPreviousData 는 같은 쿼리의 쿼리키가 변경될 경우 이전에 요청했던 데이터를 새로운 데이터를 받기 전까지 보여준다.

keepPreviousData쿼리 키가 변경될 때 이전 데이터를 새로운 데이터가 도착할 때까지 유지한다.

필터링, 정렬 등의 변화로 쿼리 키가 변경되었을 때, 이전 데이터를 보여주어 UI가 깜빡이거나 불필요한 로딩 스피너를 표시하는 상황을 방지할 수 있다.

이것도 마찬가지로 로딩 화면이 적당할지, 다른 쿼리키의 데이터가 보여져도 괜찮을지 판단이 필요하다.

export const useRecruitmentsQuery = (filter: IRecruitFilter) => {
  return useQuery({
    queryKey: [RecruitmentQueryKey.RECRUIT_ANNOUNCEMENTS, filter],
    queryFn: async () => {
      const recruitData = await getRecruitAnnouncements(filter);
      return recruitData.map(decodeRecruitResToRecruitProject);
    },
    staleTime: 60 * 1000 * 5, // 5분
    // keepPreviousData: false,
    // keepPreviousData: true,
  });
};
  • keepPreviousData: false인 경우 (기본값)

  • keepPreviousData: true인 경우

백그라운드 데이터

refetchType

invalidateQueries를 호출하면 특정 쿼리를 stale 상태로 표시하고, 필요 시 refetch를 실행할 수 있다. 기본적으로 refetch는 staleTime과 관련된 동작을 따르므로, 즉시 데이터를 다시 가져오려면 추가 옵션을 설정해야 한다.

queryClient.invalidateQueries(['queryKey'], {
  refetchType: 'active', // 'active', 'inactive', 'all' 중 선택
});

refetchType 옵션은 다음과 같다.

  • 'active' (기본값): 현재 활성화된 쿼리(사용 중인 쿼리)만 refetch.
  • 'inactive': 비활성화된 쿼리만 refetch.
  • 'all': 활성화 및 비활성화된 모든 쿼리를 refetch.

비활성화된 쿼리를 refetch할 때는 해당 쿼리가 다시 요청될 가능성을 신중히 판단해야 한다. 예를 들어, N개의 필터가 각각 M개의 선택지를 가진다면, 가능한 모든 조합의 경우의 수는 M^N이다. 이러한 모든 쿼리를 한꺼번에 refetch하면 네트워크에 과도한 부하가 발생해 비효율적일 수 있다.

마무리

Tanstack Query를 사용하며 서버 상태를 쿼리라는 개념으로 관리하는 방법을 이해하게 되었다.

캐시 데이터를 어떻게 관리하느냐에 따라 UI와 사용자 경험이 크게 달라질 수 있다는 점이 특히 흥미로웠다. 이를 통해 서버 상태 관리가 단순히 데이터를 가져오고 저장하는 과정이 아니라, 사용자에게 더 나은 경험을 제공하기 위한 중요한 설계 요소임을 깨닫게 되었다.

앞으로도 이러한 경험을 바탕으로 더 효율적인 데이터 관리와 매끄러운 UI를 구현하는 방법을 고민하고 발전시켜 나갈 생각이다.