기존 캐싱 vs Tanstack Query 캐싱

강연주·2025년 1월 21일

📚 TIL

목록 보기
130/186

🦭 기존 캐싱 구현

캐시스토어

캐시스토어는 개발자가 직접 만드는 전역 상태 저장소로, 구현 방식은 크게 3가지.

1. 메모리 저장 방식

  • JavaScript 객체나 Map을 전역 변수로 선언하여 사용
  • 앱이 실행되는 동안만 데이터가 유지됨
  • 페이지 새로고침하면 데이터가 사라짐
  • 주로 Redux, Recoil 같은 상태관리 라이브러리를 활용

2. 브라우저 저장 방식

  • localStoragesessionStorage를 사용
  • 브라우저를 닫거나 새로고침해도 데이터가 유지됨
  • 용량 제한이 있음 (보통 5-10MB)
  • 직렬화/역직렬화 과정이 필요

3. 인메모리 캐시와 브라우저 저장소 혼합 방식

  • 빠른 접근이 필요한 데이터는 메모리에 저장
  • 유지가 필요한 데이터는 localStorage에 저장
  • 앱 시작 시 localStorage의 데이터를 메모리로 불러옴

캐시스토어 위치

캐시스토어는 애플리케이션의 어디서나 접근 가능해야 하므로, 보통 다음 위치 중 하나에 구현한다.

  • 별도의 store 파일이나 폴더
  • 상태관리 라이브러리의 store 내부
  • 전역 상태 관리 컨텍스트
  • 유틸리티 클래스나 모듈

(탠스택 쿼리는 이런 복잡한 캐시 관리를 자동화, 직접 구현할 필요가 없게 해준다.)


예시

React + 일반 상태관리

🖥️ javascript

// 캐시를 위한 전역 상태 관리
const cacheStore = {
  todos: {
    data: null,
    lastFetched: null,
    isLoading: false,
    error: null
  }
};

function TodoList() {
  const [todos, setTodos] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchTodos = async () => {
      // 캐시가 있고 5분이 지나지 않았다면 캐시 사용
      if (cacheStore.todos.data && 
          Date.now() - cacheStore.todos.lastFetched < 300000) {
        setTodos(cacheStore.todos.data);
        return;
      }

      setIsLoading(true);
      try {
        const response = await axios.get('/todos');
        const data = response.data;
        
        // 캐시 저장
        cacheStore.todos.data = data;
        cacheStore.todos.lastFetched = Date.now();
        
        setTodos(data);
      } catch (err) {
        setError(err);
        cacheStore.todos.error = err;
      } finally {
        setIsLoading(false);
        cacheStore.todos.isLoading = false;
      }
    };

    fetchTodos();
  }, []);

  // 수동으로 모든 에러 상태 처리
  if (error) return <div>Error: {error.message}</div>;
  if (isLoading) return <div>Loading...</div>;
  if (!todos) return null;

  return (/* JSX */);
}

수동 방식의 문제점

  • 캐시 무효화 로직을 직접 구현해야 함
  • 여러 컴포넌트에서 동일한 데이터 요청 시 중복 호출 발생
  • 윈도우 포커스, 네트워크 재연결 시 자동 갱신 없음
  • 로딩/에러 상태 관리가 번거로움
  • 컴포넌트마다 비슷한 로직을 반복 작성

🐊 Tanstack Query 캐싱

QueryCache를 사용. 쿼리캐시는 Tanstack Query가 기본적으로 메모리 내에 가지고 있는 내부 캐시 저장소로, QueryClient 인스턴스를 통해 관리된다.

동작 원리 코드

기본 캐시 설정

🖥️ javascript

// QueryClient 생성 시 캐시 설정 가능
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5000,
      cacheTime: 300000,
    }
  }
});

캐시 구조

🖥️javascript

// 내부적으로 이런 형태로 저장됨
{
  queries: {
    ['todos']: {
      data: [...],
      dataUpdateCount: 1,
      dataUpdatedAt: 1642512948299,
      error: null,
      errorUpdateCount: 0,
      errorUpdatedAt: 0,
      fetchStatus: 'idle',
      status: 'success'
    }
  }
}

캐시 조작 방법

🖥️ javascript

// 캐시 직접 접근
const cachedData = queryClient.getQueryData(['todos'])

// 캐시 수동 설정
queryClient.setQueryData(['todos'], todos)

// 캐시 무효화
queryClient.invalidateQueries(['todos'])

// 캐시 삭제
queryClient.removeQueries(['todos'])

➡️ 별도의 캐시스토어를 만들 필요 없이 QueryClient가 모든 캐시 관리를 담당한다. 브라우저를 새로고침하면 메모리 캐시는 초기화되는데, 필요한 경우 localStorage나 다른 영구 저장소와 연동할 수도 있다.


사용 예시

🖥️ javascriptC

function TodoList() {
  const { data: todos, isLoading, error } = useQuery({
    queryKey: ['todos'],
    queryFn: () => axios.get('/todos').then(res => res.data),
    staleTime: 300000 // 5분
  });

  if (error) return <div>Error: {error.message}</div>;
  if (isLoading) return <div>Loading...</div>;

  return (/* JSX */);
}

Tanstack Query 장점

캐싱 자동화

  • staleTime으로 캐시 유효 기간 간단히 설정
  • 동일한 queryKey로 여러 곳에서 요청해도 한 번만 API 호출
  • 백그라운드에서 자동으로 데이터 리프레시

에러 처리 간소화

  • 에러 상태 자동 감지 및 관리
  • 재시도 로직 내장
  • 에러 발생 시 이전 캐시 데이터 유지

자동 동기화

  • 브라우저 탭 전환 시 자동 리프레시
  • 네트워크 재연결 시 자동 재시도
  • mutation 후 관련 쿼리 자동 무효화

❓mutation 후 관련 쿼리 자동 무효화? (useMutation은 자동 캐싱X)

➡️ mutation 후 관련 쿼리 자동 무효화와 자동 캐싱은 다른 개념이다.

  • useMutation은 자동 캐싱X
    : mutation은 데이터를 변경하는 작업이므로, 매번 새로운 요청 필요.
    따라서 결과를 캐시에 저장하지 않음

  • mutation 후 관련 쿼리 무효화

🖥️ javascript

const mutation = useMutation({
  mutationFn: addTodo,
  onSuccess: () => {
    // 여기서 관련 쿼리를 수동으로 무효화
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  }
});
  • mutation 성공 후 관련된 useQuery의 캐시를 무효화(invalidate)하는 것
  • 캐시가 무효화되면 해당 useQuery가 자동으로 리페칭됨
  • 이는 개발자가 onSuccess 등에서 직접 설정해야 하는 부분
  • 그럼 '자동' 무효화가 아니네?
  • ☀️네, 정확히 말씀하셨습니다. 제가 앞서 설명한 "mutation 후 관련 쿼리 자동 무효화"라는 표현은 부정확했습니다

❗️useMutation은 자동으로 쿼리를 무효화하지 않습니다.
개발자가 onSuccess 콜백에서 queryClient.invalidateQueries()를 호출하여 수동으로 무효화 처리를 해야 합니다. mutation 성공 후 관련 쿼리의 캐시를 수동으로 무효화하면, 그 때 해당 쿼리가 자동으로 리페칭됩니다.


캐시 제어 옵션

🖥️ javascript

const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 5000,        // 5초 동안 신선한 상태 유지
  cacheTime: 300000,      // 5분 동안 캐시 유지
  refetchOnMount: false,  // 마운트 시 재요청 비활성화
  refetchOnWindowFocus: false // 윈도우 포커스 시 재요청 비활성화
});

복잡한 데이터 캐싱과 상태 관리 로직을 추상화, 비즈니스 로직에 집중 가능.
특히 실시간성이 중요한 애플리케이션에서 데이터의 일관성을 유지하면서도
성능을 최적화하는 데 도움.

profile
아무튼, 개발자

0개의 댓글