Caching
캐싱이란 자주 사용하는 데이터나 결과를 저장하여 빠르게 접근할 수 있도록 하는 기술을 의미합니다.
서버에서 데이터를 매번 가져오지 않아도 보관된 데이터를 사용하여 불필요한 네트워크 요청을 줄일 수 있습니다.
TanStack Query
TanStack Query는 서버로부터 데이터 가져오기, 데이터 캐싱, 캐시 제어 등 데이터를 쉽고 효율적으로 관리할 수 있는 라이브러리입니다. React Query라는 이름으로 시작했으나 다른 프레임워크에서도 활용할 수 있도록 기능이 확장되면서 이름이 Tanstack Query로 변경되었습니다.
주요 기능
- 데이터 가져오기 및 캐싱
- 동일 요청의 중복 제거
- 신선한 데이터 유지
- 무한 스크롤, 페이지네이션 등의 성능 최적화
- 네트워크 재연결, 요청 실패 등의 자동 갱신
적용
// 1. 앱 최상단 - QueryClient 생성 및 Provider 연결
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<TodoList />
<TodoCount /> {/* 같은 쿼리를 다른 컴포넌트에서도 사용 */}
</QueryClientProvider>
)
}
TanStack Query를 사용하기 위해서는 QueryClientProvider를 최상단에서 감싸주고 QueryClient 인스턴스를 client props로 넣어서 애플리케이션을 연결해줍니다.
QueryClientProvider는 내부적으로 Context API를 사용하므로 모든 자식 컴포넌트들은 QueryClient에 접근이 가능해집니다.(앱 전체에서 같은 queryClient instance 공유)
이 QueryClient 안의 QueryCache에서 캐싱이 일어나게 됩니다.
// 2. TodoList 컴포넌트
function TodoList() {
const { data, isPending, isError } = useQuery({
queryKey: ['todos', { status: 'done' }], // 캐시의 주소
queryFn: () => fetchTodos({ status: 'done' }),
staleTime: 1000 * 60, // 1분간 fresh
})
if (isPending) return <div>로딩 중...</div>
if (isError) return <div>에러 발생</div>
return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>
}
// 3. TodoCount 컴포넌트 - 같은 queryKey 사용
function TodoCount() {
const { data } = useQuery({
queryKey: ['todos', { status: 'done' }], // 동일한 캐시 주소
queryFn: () => fetchTodos({ status: 'done' }),
// → fetch를 새로 하지 않고 TodoList와 같은 Query 인스턴스를 공유
})
return <span>완료된 항목: {data?.length ?? 0}개</span>
}
TanStack Query를 활용해서 데이터를 가져올 때는 항상 queryKey를 지정하게 됩니다.
queryKey는 데이터를 식별하도록 하는 고유 key값으로 캐시된 데이터와 비교해서 새로운 데이터를 가져올지, 캐시된 데이터를 사용할지 결정하게 됩니다.
queryFn은 실제 호출하고자 하는 비동기함수가 들어갑니다.
→ TodoList와 TodoCount는 queryKey가 동일하므로 QueyCache의 map에서 같은 query instance를 가리키므로 fetch가 한 번만 발생합니다.
전체 흐름
useQuery 호출
│
▼
QueryObserver 생성 (useBaseQuery 내부)
│
▼
QueryCache.build() 실행
├── queryKey → queryHash 변환 (JSON.stringify 방식)
├── #queries Map에서 해시로 검색
├── 있으면 → 기존 Query 재사용
└── 없으면 → 새 Query 생성 후 Map에 추가
│
▼
Query 상태 확인
├── 캐시 데이터 있음 + fresh → 즉시 반환
├── 캐시 데이터 있음 + stale → 즉시 반환 + 백그라운드 refetch
└── 캐시 없음 → fetch 실행 → pending 상태
│
▼
useSyncExternalStore로 QueryObserver 구독
└── Query 상태 변경 시 → observer.notify() → 컴포넌트 리렌더링
staleTime과 gcTime
데이터가 상했으면 서버에 다시 요청해서 신선한 데이터를 가져와야 합니다.
staleTime: 데이터가 상하는 데까지 걸리는 시간
이 시간까지는 같은 queryKey로 useQuery가 호출되어도 refetch하지 않고 캐시 데이터를 바로 씁니다.
기본값이 0이기 때문에 stleTime을 지정해야 불필요한 네트워크 요청을 줄일 수 있습니다.
// 변경이 거의 없는 데이터
useQuery({ queryKey: ['categories'], queryFn: fetchCategories, staleTime: 1000 * 60 * 10 }) // 10분
// 실시간 재고처럼 항상 최신이어야 하는 데이터
useQuery({ queryKey: ['stock', id], queryFn: fetchStock }) // staleTime: 0 (기본값)
국가 코드 목록, 카테고리 목록처럼 거의 변하지 않는 데이터는 staleTime: Infinity를 쓰기도 합니다.
gcTime: 비활성 캐시 데이터가 메모리에 남아있는 시간
메모리 관리가 목적일 때는 gcTime을 사용합니다. 데이터가 무거운데 재방문 가능성이 낮다면 줄이고, 반대로 유저가 페이지를 자주 방문한다면 늘려서 로딩 없이 캐시를 보여주게 합니다.
stale 판정이 나기 전에 캐시 자체가 지워지면 데이터를 재사용할 기회가 없어지기 때문에 보통
staleTime <= gcTime 관계를 유지합니다.
캐싱이 이미 가져온 데이터를 어떻게 재사용할 것인가를 다뤘다면,
Prefetching은 필요해지기 전에 미리 캐시를 채우는 역할을 합니다.
preload와 비교하자면
preload는 현재 페이지에서 즉시 필요한 리소스를 우선 로드하고
prefetch는 다음에 필요할 리소스를 백그라운드에 미리 로드합니다. (preload보다 우선순위가 낮음)
Prefetch
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
내부 동작은 캐싱과 동일합니다.
queryClient.prefetchQuery를 호출하면 동일한 QueryCache에 데이터를 미리 넣어둡니다.
이후 컴포넌트가 마운트되면 useQuery를 호출할 때 QueryCache에 이미 데이터가 있으므로 바로 데이터를 반환합니다.
일반적으로 React에서는 렌더링 시 비동기 작업 처리를 3가지 방법으로 설명합니다.
Fetch-on-render: 컴포넌트 렌더링을 먼저 시작하고 useEffect나 componentDidMount로 비동기 처리 → useEffect, useQuery
Fetch-then-render: useEffect나 componentDidMount로 화면을 그리는데 필요한 데이터를 모두 조회한 후 렌더링 시작 → loader + await
Render-as-you-fetch: 비동기 작업과 렌더링을 동시에 시작 (즉시 초기 상태 렌더링 후 비동기 작업 완료되면 다시 렌더링) → prefetch + suspense
React에서 prefetch 사용하기
1. Event Handler
function ParentComponent() {
const [show, setShow] = useState(false);
const handleMouseEnter = () => {
prefetchMockQuery(); // 마우스 올리면 미리 fetch 시작
};
return (
<div>
{show && (
<Suspense fallback={<div>Loading...</div>}>
<MockComponent /> {/* 클릭 시 이미 캐시에 있으므로 로딩 없음 */}
</Suspense>
)}
<button
onClick={() => setShow(true)}
onMouseEnter={handleMouseEnter}
>
보여주기
</button>
</div>
);
}
사용자가 버튼에 마우스를 올리는 순간! 미리 fetch를 시작하니까 클릭할 때는 데이터가 이미 캐시에 있어서 바로 가져오겠죠!
2. 컴포넌트에서 Waterfall 방지
// waterfall 발생 - post 완료 후에야 comment fetch 시작
function PostComponent({ id }) {
const { data, isPending } = usePostById(id);
if (isPending) return <div>로딩 중...</div>;
return (
<>
<div>{data.title}</div>
<Comment id={id} /> {/* 여기서 comment fetch 시작 → 직렬 */}
</>
);
}
// 병렬 처리 - 두 요청이 동시에 시작
function PostComponent({ id }) {
const { data, isPending } = usePostById(id);
useCommentById(id); // 결과는 안 쓰지만 미리 fetch 시작
if (isPending) return <div>로딩 중...</div>;
return (
<>
<div>{data.title}</div>
<Comment id={id} /> {/* 이미 캐시에 있거나 fetch 중 */}
</>
);
}
React Query는 같은 queryKey를 만나면 캐시를 공유하므로 부모에서 comment query를 미리 호출하면 자식에서 바로 사용이 가능합니다.
3. Router Integration
// queries/postQuery.ts
export const postQuery = (id: number) => ({
queryKey: ['post', id],
queryFn: () => fetchPost(id),
});
// loader 정의
const postLoader = (queryClient: QueryClient) => async ({ params }) => {
// 캐시에 있으면 그대로 사용, 없으면 fetch
await queryClient.ensureQueryData(postQuery(params.id));
return null;
};
// 라우터에 연결
const useRouter = () => {
const queryClient = useQueryClient(); // App의 queryClient 재사용
return createBrowserRouter([
{
path: '/post/:id',
element: <PostPage />,
loader: postLoader(queryClient), // 페이지 진입 전에 미리 fetch
},
]);
};
라우트 진입 전에 미리 데이터를 fetch하는 방식입니다.
loader를 정의할 때 데이터에 쿼리를 캐싱하기 위해 queryClient를 전달받습니다.
ensureQueryData에 필요한 쿼리를 넘겨서 캐싱된 데이터가 있으면 가져오고, 없으면 fetch한 결과물인 Promise 배열을 병렬적으로 처리합니다.
해당 router의 loader에 작성한 loader 함수를 넘겨줍니다.
버튼이나 링크처럼 사용자 인터랙션이 예측 가능하다면 이벤트 핸들러에서, 부모-자식 컴포넌트 간 의존적인 쿼리가 있다면 컴포넌트에서, 페이지 단위로 미리 로드하고 싶다면 Router에서 처리하면 될 것 같습니다.
참고:
https://www.heropy.dev/p/HZaKIE
TanStack Query(React Query) 핵심 정리
TanStack Query는 서버로부터 데이터 가져오기, 데이터 캐싱, 캐시 제어 등 데이터를 쉽고 효율적으로 관리할 수 있는 라이브러리로, React Query라는 이름으로 시작했지만, v4부터 Vue나 Svelte 등의 다른
www.heropy.dev
TanStack Query는 어떻게 데이터를 캐싱할까?
라이브러리를 뜯어보자
velog.io
Tanstack Query 캐싱 완벽 이해: 내부 동작부터 옵션 설정까지
들어가며React Query를 사용하다 보면 "처음엔 로딩 스피너가 보이는데 나중에는 안 보여요"라는 현상을 경험하게 됩니다. 이건 캐싱 덕분인데, 정확히 어디서 어떻게 되는 걸까요?이 글에서는 React
20002100.tistory.com
프론트엔드가 알아야 할 리액트 쿼리로 캐싱하기
📌 시작하기 앞서 프로젝트를 하다보니 요청수가 잦고 바뀌지않는 데이터를 항상 서버에 요청하는게 불필요하다고 느꼇었는데 이번에 리액트 쿼리에서 제공하는 캐시를 이용해서 문제를 해결
velog.io
https://bichoninthefront.tistory.com/112#%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C%20refetch%3F%C2%A0-1
TanStack Query를 활용하여 Route 기반 Prefetching하기(feat.Render-as-you-fetch)
이전에 React Query meets React Router라는 글을 읽고 데이터를 효율적으로 가져올 수 있는 방법이라고 생각되어 노션에 정리해 두었는데, 최근 사용자 경험을 개선하는 작업을 하면서 적용해 보았습니
bichoninthefront.tistory.com
https://homebody-coder.tistory.com/entry/React-Query%EC%9D%98-Prefetch%EB%9E%80
React Query의 Prefetch란?
특정 데이터가 필요하다는 것을 알고 있는 경우 또는 예측할 수 있는 경우 prefetching를 통해, 미리 해당 데이터로 캐시를 채워 더 빠른 유저 경험을 제공할 수 있습니다. 몇 가지 `Prefetch` 패턴이
homebody-coder.tistory.com
'CS' 카테고리의 다른 글
| [JavaScript] 객체/프로퍼티/프로토타입 (1) | 2026.05.23 |
|---|---|
| [JavaScript] this/클로저 : 코드로 이해하기 (2) | 2026.05.22 |
| [JavaScript] 실행 컨텍스트 (1) | 2026.05.10 |
| JavaScript가 변수와 함수를 다루는 방법 (2) | 2026.05.03 |
| 불필요한 DOM을 줄이자: Virtualization (2) | 2026.05.03 |