TanStack Query 캐시 키 안정화로 댓글·증거 조회 성능 개선하기: 시나리오 토글 상태를 정렬 기반 집합으로 리팩토링

작성일: 2026년 2월 27일 오전 03:35
조회수: 37

이 글은 팀원의 댓글 도메인을 코드 리뷰하는 과정에서, TanStack Query 캐시 설계 상의 문제를 발견하고 스토어 레벨에서 쿼리 키를 안정화한 경험을 정리한 것입니다.

1) 문제 상황 (Problem)

쿼리 키 불안정으로 인한 캐시 분산

프로젝트별 “집계에 포함된 시나리오”는 클라이언트 상태(useScenarioStore)의 getSelected(projectId)에서 관리되고 있었다. 이 함수는 내부적으로 Object.entries(visibilityMap) 순서를 그대로 사용해 시나리오 ID 배열을 만들고 있었는데, 이 값에 의존하는 쿼리(예: 선택된 시나리오에 대한 댓글/증거 조회)에서 같은 시나리오 집합이라도 토글 순서에 따라 배열 순서가 달라져 쿼리 키가 유실될 수 있는 구조였다.

TanStack Query 캐시 미스 증가 가능성

댓글/증거 페이지에서는 scenarios 배열을 그대로 쿼리 키에 포함하고 있었다.

tsx
// commentQueries.list / evidenceQueries.list 내부 예시
queryOptions({
  queryKey: [...lists(), params] as const,
  queryFn: () => getCommentList(params), // 또는 getEvidenceList
  enabled: params.scenarios.length > 0,
});

TanStack Query는 배열 기반 키에서 요소 순서를 구분하기 때문에 ['s1', 's2']['s2', 's1']는 서로 다른 쿼리 키로 취급된다. 즉, 토글 경로에 따라 다음과 같은 문제가 발생할 수 있었다.

  1. 동일 데이터에 대한 중복 캐시 엔트리 생성
  2. 동일 화면 전환에서도 불필요한 API 재요청
  3. 캐시 무효화(invalidate) 시 더 많은 키를 관리해야 하는 복잡도 증가

댓글 도메인은 팀원의 담당 영역이었지만, 리뷰 과정에서 이 쿼리 키 설계가 시나리오 토글 방식과 맞물려 캐시를 분산시킬 여지가 있다는 점을 발견했고, 이를 도메인 단위가 아니라 공통 스토어 레벨에서 해결하는 방향으로 접근했다.


2) 해결 과정 (Approach)

Step 1: TanStack Query 키 동작 재정리

먼저 팀원과 함께 TanStack Query의 키 동작을 다시 짚었다.

  1. 배열 기반 키 예시: ['todos', status, page]
    같은 요소라도 순서가 다르면 다른 키로 간주된다.
  2. 객체 기반 키 예시: ['todos', { status, page }]
    프로퍼티 순서는 무시되고, 프로퍼티 집합과 값이 같으면 동일 키로 간주된다.

현재 구조는 queryKey: [...lists(), params] 형태로 params 자체를 키에 넣고 있었고, 이 params 안에 scenarios: string[]가 포함되어 있었다. 배열은 순서에 민감하기 때문에, scenarios가 정렬되지 않으면 같은 시나리오 집합이어도 서로 다른 쿼리 키가 만들어질 수 있음을 확인했다.

Step 2: 실제 코드 경로 추적

실제 문제가 어떻게 발생할 수 있는지, 댓글/증거 도메인과 시나리오 토글 스토어를 연결해서 살펴보았다.

  1. 시나리오 선택 상태 소스
ts
// src/features/scenario-toggle-visibility/model/scenario-store.ts

getSelected: (projectId: string) => {
  const state = get();
  const visibilityMap = state.visibilityByProject[projectId];

  if (visibilityMap) {
    return Object.entries(visibilityMap)
      .filter(([, v]) => v !== false)
      .map(([id]) => id);
  }

  return [];
},

Object.entries는 키의 삽입 순서를 보존하기 때문에, 시나리오를 끄고 다시 켜는 순서에 따라 반환되는 ID 배열의 순서가 매번 달라질 수 있다.

  1. 쿼리 키 소비자 (팀원 담당 코드)
tsx
// src/views/projects/ui/comments-page.tsx

const { getSelected } = useScenarioStore();
const selectedScenarios = getSelected(projectId);

const { data } = useQuery(
  commentQueries.list({
    scenarios: selectedScenarios,
    // ...
  })
);
tsx
// src/views/projects/ui/evidence-page.tsx

const { getSelected } = useScenarioStore();
const selectedScenarios = getSelected(projectId);

const { data } = useQuery(
  evidenceQueries.list({
    scenarios: selectedScenarios,
    // ...
  })
);

댓글/증거 페이지 모두 getSelected(projectId)에서 반환된 배열을 그대로 params.scenarios로 넘기고 있었기 때문에, “집계에 포함된 시나리오 집합”이 같더라도 토글 경로에 따라 매번 새로운 캐시 키가 생성될 수 있는 구조임을 코드 리뷰 과정에서 명확히 확인했다.

Step 3: 쿼리 키 안정화 전략 설계

문제를 인지한 뒤, 팀원과 두 가지 방향을 비교했다.

  1. 스토어 레벨에서 정렬
    getSelected(projectId)가 같은 집합에 대해 항상 같은 순서를 보장하도록 수정한다.
    이 경우:

    • 한 번의 수정으로 이 함수를 사용하는 모든 쿼리/로직이 자동으로 안정화된다.
    • 댓글, 증거, 이후 추가될 분석 쿼리 등에서 params.scenarios를 그대로 받아도 된다.
  2. 쿼리 팩토리 레벨에서 정규화
    commentQueries.list, evidenceQueries.list 등에서 queryKey에 사용할 때만
    scenarios: [...params.scenarios].sort()처럼 키용 파라미터를 정규화한다.
    이 경우:

    • 쿼리 키와 네트워크 파라미터를 분리해 설계할 수 있지만,
    • analysisQueries처럼 새로운 소비자가 생길 때마다 동일한 패턴을 반복해야 한다.

리팩토링의 목표는 “기존 도메인 로직은 최대한 건드리지 않고, 캐시 키만 안정적으로 만들자”였기 때문에, 팀과 논의 끝에 **스토어 레벨에서 정렬(옵션 1)**을 선택했다. 이렇게 하면 팀원 코드의 인터페이스는 유지하면서, 내부 데이터 형태만 더 안전하게 만드는 방향으로 개선할 수 있다.


3) 최종 코드 (Final Solution)

getSelected: 시나리오 ID 집합 → 고정 순서 배열로 매핑

tsx
// src/features/scenario-toggle-visibility/model/scenario-store.ts

getSelected: (projectId: string) => {
  const state = get();
  const visibilityMap = state.visibilityByProject[projectId];

  if (visibilityMap) {
    return Object.entries(visibilityMap)
      .filter(([, v]) => v !== false)
      .map(([id]) => id)
      .sort(); 
      // 시나리오 배열을 쿼리 키로 사용하기 위해 정렬
      // → 같은 ID 집합이면 항상 같은 순서의 배열로 매핑
  }

  return [];
},

이 변경으로 다음이 보장된다.

  1. 같은 프로젝트에서 “집계에 포함된 시나리오 ID 집합”이 같다면, 언제 어떤 순서로 토글했더라도 getSelected(projectId)가 항상 동일한 배열을 반환한다.
  2. 댓글/증거 페이지처럼 getSelected 결과를 그대로 params.scenarios로 사용하는 코드들은 수정 없이도 안정적인 쿼리 키를 가지게 된다.
  3. 새로운 도메인(예: 분석용 대시보드)이 getSelected를 사용하더라도, 별도의 정렬 로직 없이 동일한 안정성을 그대로 가져간다.

4) 결과 (Result)

캐시 재사용률 향상 및 네트워크 트래픽 감소

같은 시나리오 집합에 대해 댓글/증거 목록을 다시 열거나, 필터 조건만 변경하는 경우에도 기존 캐시를 재활용하게 되어 불필요한 API 요청 가능성을 줄였다. 특히 “시나리오를 여러 번 토글해도 결국 같은 집합에 수렴하는” 사용자 행동에서 효과가 크다.

쿼리 키 설계 원칙을 스토어에 내재화

“집합 의미를 가지는 파라미터는 정렬/정규화해서 키에 사용한다”는 규칙을 Store 레벨에 녹여냈다. 이로써 댓글 도메인뿐 아니라, 같은 선택 상태를 공유하는 다른 도메인(증거, 향후 분석 기능 등)도 일관된 캐시 전략을 재사용할 수 있는 기반이 마련되었다.

리뷰 관점에서의 기여

댓글 도메인은 팀원 담당이었지만, 코드 리뷰 과정에서 TanStack Query 캐시 동작과 스토어 구현을 함께 바라보며 문제를 발견했고, 도메인 로직은 유지한 채 공통 스토어를 개선하는 방식으로 제안·적용했다. 이를 통해 개별 도메인 구현을 존중하면서도, 전역 캐시 전략 관점에서 시스템적인 품질을 끌어올리는 협업 경험을 만들 수 있었다.

0개의 댓글
💬

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!