TanStack Query 수동 캐시 정규화: 목록·상세 동기화와 선택 기준

2026.09.15·약 6분·작성: 해비·블로그 소개
TanStack Query 수동 캐시 정규화: 목록·상세 동기화와 선택 기준 학습 표지

이 글은 프론트엔드 라이브러리 연결 과정의 일부입니다. React 컴포넌트·props·state·이벤트와 TypeScript 객체·배열 타입을 먼저 익혀 주세요. 원본 강의의 문장을 옮기는 대신 독립적인 예제와 확인 과제를 구성했습니다.

학습 목표: 수동 정규화의 구조와 비용을 이해하고 기본 Query 캐시와 비교해 선택합니다.

예상 학습 시간: 40분. 개인별 차이가 있으며 설치 시간은 제외합니다.

이 과정의 선택 심화입니다

서버 Todo를 완성하기 위해 수동 정규화가 필수인 것은 아닙니다. TanStack Query의 기본 Query 캐시는 쿼리 키별 응답을 보관합니다. 목록에 Todo 객체가 있고 상세 캐시에도 같은 Todo가 있을 수 있습니다. 이 구조는 쿼리별 수명과 요청 조건을 독립적으로 관리한다는 장점도 있으므로 중복이 있다는 이유만으로 잘못된 설계라고 판단하지 않습니다.

두 구조를 비교합니다

구조 목록 캐시 개별 캐시 추가 책임
일반 Query Todo[] 상세 조회 시 Todo 무효화 또는 양쪽 업데이트
수동 정규화 string[] ID별 Todo ID·엔티티 존재·삭제·만료·동기화 정책

예를 들어 목록 [a,b]와 상세 a·b를 보관한다면 목록에서 ID만 읽고 각각의 상세 캐시를 조회할 수 있습니다. 하지만 상세 b가 gcTime으로 제거되거나 fetch가 실패하면 목록 ID만 남을 수 있습니다. 데이터 중복이 줄어든다고 전체 메모리 사용량이 반드시 줄어드는 것도 아닙니다. 쿼리마다 메타데이터와 observer 비용이 생기기 때문입니다.

조회 후 분리 저장하는 부분 예제

다음은 구조를 보여 주는 부분 코드입니다. ZIP의 기본 Todo 구현에 그대로 덮어쓰지 않습니다. cache list의 타입이 Todo[]에서 string[]으로 바뀌면 컴포넌트와 모든 mutation도 함께 바뀌어야 합니다.

const todos = await fetchTodos(signal);
for (const todo of todos) {
  queryClient.setQueryData(todoKeys.detail(todo.id), todo);
}
return todos.map((todo) => todo.id);

queryFn 안에서 다른 캐시를 쓰면 해당 요청의 취소·재실행·동시 요청에 따른 부수 효과도 관리해야 합니다. 늦게 도착한 목록 응답이 더 최신인 상세 값을 덮을 수 있습니다. 이 부분 예제는 완성된 정규화 계층이 아니며, 호출 순서만으로 모든 경쟁을 해결하지 않습니다.

enabled: false가 해결하지 않는 문제

목록 항목의 상세 쿼리를 enabled: false로 두면 자동 조회를 막을 수 있지만, invalidateQueries로 해당 키를 무효화한다고 비활성화한 쿼리가 자동 재조회되는 것은 아닙니다. 캐시가 없을 때 어떤 화면을 보일지, 전체 목록을 재조회할지, 상세 요청을 켤지를 설계해야 합니다. 캐시 누락 시 무조건 throw하면 복구 가능한 상황이 전체 화면 오류가 될 수 있습니다.

추가·수정·삭제의 일관성

추가할 때는 상세 엔티티를 먼저 보관하고 목록 ID를 추가합니다. 수정할 때는 관련 목록 조회도 오래된 엔티티를 다시 기록할 수 있음을 고려합니다. 삭제는 목록 ID와 상세 캐시를 함께 제거합니다. 여러 필터 목록이 있다면 한 목록만 지워서는 충분하지 않습니다. 실패 롤백은 존재하던 값뿐 아니라 없던 캐시가 새로 만들어졌는지도 구분해야 합니다.

먼저 적용할 수 있는 단순한 대안

대부분의 작은 목록은 mutation 성공 뒤 관련 키를 무효화하는 방식으로 시작할 수 있습니다. 서버가 확정한 응답으로 상세 캐시를 갱신하고 목록만 재조회하는 절충도 가능합니다. 목록 전체가 완전한 데이터인지 확신할 수 없으면 캐시가 없을 때 [newTodo]만 넣어 완전한 목록처럼 표시하지 않습니다. 필터·정렬·페이지네이션이 있는 목록은 특히 서버 규칙을 다시 확인해야 합니다.

직접 해보기와 확인 질문

필터 목록 두 개에 같은 ID가 있고 상세 캐시 하나가 삭제된 상황을 종이에 그려 보세요. 항목 수정·삭제 시 어느 키를 바꿀지 적으세요.

풀이 기준 보기

기본 방식은 관련 목록과 상세를 무효화하거나 확정 응답으로 일치시킵니다. 수동 방식은 모든 ID 목록·엔티티·누락 복구를 관리해야 합니다. 현재 요구에 그 비용이 필요한지 설명할 수 있다면 이 심화의 목표를 달성한 것입니다.

공통 실습 실행

아래 공통 ZIP을 풀고 Node.js 24 환경에서 실행합니다. package-lock.json에 실습 버전을 고정했습니다. 본문의 경로와 ZIP의 경로는 같습니다.

npm ci
npm run dev

터미널에 표시된 주소를 열고 이 글에 해당하는 메뉴를 선택하세요. 서버 Todo만 별도 터미널의 npm run server가 필요합니다. 본문 코드에서 생략한 공통 설정과 UI 파일까지 ZIP에 포함되어 있습니다.

프론트엔드 라이브러리 공통 실습 ZIP

UI 파일 범위: ZIP의 src/components/ui는 shadcn CLI 생성물이 아닙니다. 레지스트리 접속 실패로 Radix·Sonner·Embla를 사용하는 축약 학습용 구현을 작성했습니다. 공식 설치 절차는 별도 프로젝트에서 비교하세요. 공식 컴포넌트 전체 기능과 동일하다고 보장하지 않습니다.

검증 범위

Node.js 24.19.0, React 19.3.0, React Router 7.18.3, Zustand 5.0.15, TanStack Query 5.102.8, Tailwind CSS 4.3.3에서 타입 검사와 Vite 빌드를 확인했습니다. 자동 테스트는 jsdom 기반입니다. 실제 브라우저의 포커스·레이아웃·화면낭독기와 모든 네트워크 경쟁 상황은 별도 확인 대상입니다.

npm test
npm run build

공식 문서

2026-09-14 확인. 공식 최신 문서의 버전과 실습 고정 버전이 다를 수 있으므로 설치 버전은 ZIP을 기준으로 비교합니다.

이 글이 도움이 되었나요?

조회 중

TanStack Query 학습 순서

필수 11개 · 전체 12개

읽음 기록 관리

전체 과정 목차 (12개)
  1. 필수 길잡이 · TanStack Query vs Zustand: 서버 상태와 클라이언트 상태 차이
  2. 필수 학습 · TanStack Query Provider와 첫 useQuery 상태 처리 실습 가이드
  3. 필수 학습 · TanStack Query queryKey 설계 기준: 배열 키로 캐시 구분하기
  4. 필수 학습 · TanStack Query queryFn 사용법: 데이터 요청 로직 분리하기
  5. 필수 학습 · TanStack Query staleTime gcTime 차이: 캐시 시간 기준
  6. 필수 학습 · TanStack Query useMutation 저장 실습: 실패·재시도와 목록 갱신까지
  7. 선택 참고 · TanStack Query 화면 갱신 문제 해결: 데이터 변경 후 UI가 바뀌지 않을 때
  8. 필수 학습 · TanStack Query 페이지네이션 실습: placeholderData로 이전 목록 유지하기
  9. 필수 학습 · TanStack Query 무한 스크롤 사용법: useInfiniteQuery로 목록 이어 불러오기
  10. 필수 선수 · TanStack Query Hydration 오류 해결: QueryClient 설정 기준
  11. 필수 학습 · TanStack Query Todo 실습: 서버 연결·낙관적 업데이트·실패 롤백
  12. 필수 학습 · TanStack Query 수동 캐시 정규화: 목록·상세 동기화와 선택 기준 현재 글

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기