넥스트JS + 탠스택쿼리 캐시전략(심화편): 캐시 무효화 전략

김현준·2025년 6월 6일
0

넥스트JS 이모저모

목록 보기
14/23

전체 흐름 요약

// 서버 컴포넌트
await queryClient.prefetchQuery({
  queryKey: logKeys.list(params),
  queryFn: () => getLogs(params),
});

return (
  <HydrationBoundary state={dehydrate(queryClient)}>
    <ClientComponent />
  </HydrationBoundary>
);
// 클라이언트 컴포넌트
useQuery({
  queryKey: logKeys.list(params),
  queryFn: () => fetchUseLogs(params),
});
  • 서버에서 만든 queryClientdehydrate로 직렬화
  • HydrationBoundary 하위에서 동일한 queryKey를 쓰면, 클라이언트는 SSR 캐시를 재사용
  • queryFn은 fallback용 (SSR 캐시가 없을 때만 실행됨)

핵심 전략 1: queryKey는 SSR/CSR 완전 일치

예시 - 로그 리스트

// 쿼리 키 정의 (공통)
//actions/keys.ts
export const logKeys = {
  list: ({ currentPage, pageSize, sort }: LogsParams) =>
    safeKey('log', 'list', `page-${currentPage}`, `size-${pageSize}`, `sort-${sort}`),
};
// 서버 캐시 등록
unstable_cache(() => fetchLogs(params), [...logKeys.list(params)], { ... });

// 클라이언트
useQuery({
  queryKey: logKeys.list(params),
  queryFn: () => fetchUseLogs(params),
});
  • queryKey가 완전히 동일해야 dehydrate()useQuery()로 캐시가 이어진다.

[...logKeys.list(params)]처럼 펼친 이유

이유 1: unstable_cachekeyParts를 배열로 받아야 하기 때문

// 잘못된 예
unstable_cache(() => ..., 'log:list:page-1')// 올바른 예
unstable_cache(() => ..., ['log', 'list', 'page-1'])

이유 2: 명시적 배열 보장 + 유지보수 유리

  • logKeys.list()string[]을 반환하지만, 조건문 등으로 key가 변할 경우 문제가 생길 수 있음
  • 따라서 항상 [...]로 명시해주는 것이 타입 안전하고 일관성 있음

실제 추천 패턴

const queryKey = logKeys.list(params);return unstable_cache(
  () => fetchLogs(params),
  [...queryKey], // 안정성과 명확성을 위한 배열 spread
  {
    tags: [cacheTags.logList(params), cacheTags.logAll],
    revalidate: 300,
  }
)();

핵심 전략 2: tagKey는 SSR 무효화용 의미 기반 키

예시 - 로그 캐시 무효화

// 쿼리 키 → 태그 키로 변환
export const cacheTags = {
  logList: (params: LogsParams) => logKeys.list(params).join(':'), // 'log:list:page-1:size-12:sort-latest'
  logAll: 'log:all',
};
// SSR 캐시 등록
unstable_cache(() => fetchLogs(params), [...logKeys.list(params)], {
  tags: [cacheTags.logList(params), cacheTags.logAll],
  revalidate: 300,
});
// 특정 페이지 무효화
revalidateTag(cacheTags.logList(params));

// 전체 로그 무효화
revalidateTag(cacheTags.logAll);

전체 구성 구조

queryKey 정의 (keys.ts)

export const logKeys = {
  list: ({ currentPage, pageSize, sort }: LogsParams) =>
    safeKey('log', 'list', `page-${currentPage}`, `size-${pageSize}`, `sort-${sort}`),
  detail: (logId: string) => safeKey('log', 'detail', logId),
};

tagKey 정의 (tags.ts)

export const cacheTags = {
  logList: (params: LogsParams) => logKeys.list(params).join(':'),
  logDetail: (logId: string) => logKeys.detail(logId).join(':'),
  logAll: 'log:all',
};

서버 캐시 예시 전체

export async function getLogs(params: LogsParams) {
  return unstable_cache(
    () => fetchLogs(params),
    [...logKeys.list(params)], // SSR 캐시 키
    {
      tags: [
        cacheTags.logList(params), // 페이지 단위 무효화 가능
        cacheTags.logAll,          // 전체 무효화 시 사용
      ],
      revalidate: 300,
    }
  )();
}

마무리

항목설명예시
queryKeySSR/CSR 캐시 키['log', 'list', 'page-1', ...]
tagKey (cacheTags)무효화용 키 (문자열)'log:list:page-1:...'
globalTags그룹 무효화 키'log:all', 'user:all'
[...]keyParts 배열 보장unstable_cache(..., [...queryKey])

  • queryKeyuseQuery, prefetchQuery, unstable_cache 모두 동일 구조로
  • tagKey는 문자열 기반 → join(':') 또는 의미 있는 키 구성
  • safeKey() 유틸로 undefined/null 제거 → 캐시 키 일관성 유지
  • dehydrate + HydrationBoundary를 썼다면 반드시 queryKey 일치를 보장해야 함
profile
기록하자

0개의 댓글