[트러블슈팅] DB는 바뀌는데 UI는 그대로였다, TanStack Query 캐시 동기화 해결

2026. 2. 1. 02:11·📋 프로젝트/☄️ 트러블 슈팅
728x90
반응형

| 서론

안녕하세요, 팡일입니다.

 

이번 글에서는 TanStack Query(React Query)를 사용하는 환경에서 “DB는 정상적으로 변경되는데 UI가 갱신되지 않는 문제”를 해결한 경험을 공유합니다.

 

실제 서비스(HomePage)를 운영하면서 다음과 같은 상황을 반복해서 마주했습니다.

“분명 저장은 됐는데… 왜 화면은 그대로지?”

 

처음엔 단순한 버그처럼 보였지만, 결국 문제의 핵심은 캐시 키 구조와 갱신 전략에 있었습니다.

 

이 글에서는 실제로 발생한 두 가지 문제 상황을 중심으로 원인 분석 → 해결 전략 선택 → 코드 수정 → 교훈까지 정리해 보겠습니다.

 

 

 

 

| 문제 상황 요약

문제 1) 주간 루틴 체크 → 월간 캘린더가 갱신되지 않음

주간 루틴에 있는 항목들을 체크하거나 해제할 경우, 월간 캘린더에서 완료/미완료 카운트가 즉시 변해야 했는데요?

 

DB에는 정상적으로 반영이 되지만, 월간 캘린더의 완료/미완료 카운트는 즉시 변하지 않으면서 “체크는 했는데 숫자가 그대로인” 상태가 계속 반복됐습니다.

 

 

문제 2) 리스트 메뉴 액션 → DB만 바뀌고 화면은 그대로

  • 미완료 할 일을 오늘 하기
  • 미완료 할 일을 다른 날 하기
  • 미완료 할 일 삭제
  • 모든 할 일 복사
  • 모든 할 일 삭제

리스트 메뉴에서 위의 액션을 수행하면, 앞의 문제와 동일하게 DB는 변경되지만 화면은 그대로 유지되는 문제가 있었습니다. 

 

 

 

| 공통 원인: “캐시는 갱신되지 않았다”

두 문제의 공통점은 단순했습니다.

 

서버 데이터는 바뀌었지만, React Query 캐시는 이전 상태를 그대로 사용하고 있었다

 

특히 다음 조건이 동시에 맞물려 있었습니다.

  • staleTime이 길다
  • refetchOnMount가 꺼져 있다
  • invalidateQueries 또는 setQueryData가 없다

즉, 캐시를 명시적으로 갱신하지 않으면 UI는 절대 바뀌지 않는다는 걸 체감하게 된 순간이었습니다.

 

 

 

| 해결 전략 선택 기준

문제를 해결하기 전에, 먼저 선택지를 정리했습니다.

전략 장점 단점
invalidate (재조회) 구현 간단, 안전 네트워크 비용
optimistic update 즉시 반영 로직 복잡
실시간 구독 항상 최신 구조 변경 큼

 

각 전략별로 가진 장단점을 보면서 내린 결론은 상황에 따라 다르게 쓰자였습니다.

  • 정확히 식별 가능한 경우 → 로컬 캐시 업데이트
  • 불확실하거나 새 ID가 생기는 경우 → invalidate

 

 

해결 1) 루틴 토글 → 월간 캘린더 즉시 반영

먼저, 첫 번째 문제에 대해서 정확한 원인을 분석해 보았는데요?

 

월간 캘린더는 월별 루틴 로그 캐시를 기반으로 계산됩니다. 하지만 루틴 체크 시 갱신되는 건 주간 캐시뿐이었습니다.

 

게다가 더 큰 문제는 월별 루틴 로그 타입에 routineId가 없었던 것이었습니다.

// Before
type RoutineLogMonthly = {
  date: string;
  done: boolean;
};

 

이 상태에서는 “어떤 루틴이, 어떤 날짜에 토글 됐는지”를 캐시에서 식별할 수 없었고 결국 로컬 업데이트 자체가 불가능했습니다.

 

 

그래서 이를 두고 해결 방향을 다음과 같이 결정하게 되었습니다.

  1. 월별 루틴 로그 타입에 routineId 추가
  2. 루틴 토글 시, 월 캐시를 setQueryData로 직접 업데이트
// After
export type RoutineLogMonthly = {
  routineId: string;
  date: string;
  done: boolean;
};
const monthKey = date.slice(0, 7);
const done = !current;

queryClient.setQueryData(
  routineLogKeys.byMonth(user.uid, monthKey),
  (prev: RoutineLogMonthly[] | undefined) => {
    if (!prev) return prev;

    const index = prev.findIndex(
      (log) => log.routineId === routineId && log.date === date
    );

    if (index === -1) {
      if (!done) return prev;
      return [...prev, { routineId, date, done }];
    }

    const next = [...prev];
    next[index] = { ...next[index], done };
    return next;
  }
);

 

그 결과, DB 반영 전에도 월간 캘린더가 즉시 갱신되었고, 문제가 해결되니 “내가 한 행동이 바로 보인다”는 사용자 체감을 개선할 수 있었습니다. 

 

 

해결 2) 리스트 메뉴 액션 → 즉시 화면 갱신

그다음으로는 두 번째 문제에 대해서 정확한 원인을 분석해 보았는데요?

 

리스트 메뉴 액션들은 모두 DB 변경만 수행하고, React Query 캐시는 전혀 건드리지 않는 상황이었고, 게다가 staleTime이 길어 자동 재조회도 거의 발생하지 않는 구조였습니다.

 

월간 캘린더에 있는 내용을 전체 invalidate 하는 방법보다는 영향받는 날짜 / 월 캐시만 선택적으로 invalidate 하는 것이 더 효율적이라고 판단하게 되었습니다.

const invalidateTaskCaches = async (dates: string[]) => {
  const uniqueDates = Array.from(new Set(dates));
  const months = Array.from(new Set(uniqueDates.map((d) => d.slice(0, 7))));

  await Promise.all([
    ...uniqueDates.map((date) =>
      queryClient.invalidateQueries({
        queryKey: taskKeys.byDate(user?.uid ?? "", date),
      })
    ),
    ...months.map((month) =>
      queryClient.invalidateQueries({
        queryKey: taskKeys.byMonth(user?.uid ?? "", month),
      })
    ),
  ]);
};

 

그래서 위와 같이 invalidateTaskCahes 함수를 생성한 뒤에 아래와 같이 리스트 메뉴 액션이 진행된 뒤에 호출되어 즉시 화면을 갱신할 수 있도록 했습니다.

await moveUncompletedTasksToDate({ ... });
await invalidateTaskCaches([fromDate, toDate]);

 

그 결과, 복사 / 삭제 / 이동 직후 화면 즉시 갱신되었고, 비로소 사용자 피드백 문제를 해결할 수 있었습니다.

 

 

 

| 이번 트러블슈팅에서 얻은 교훈

1) DB 반영과 UI 반영은 완전히 별개다

: TanStack Query를 쓰는 순간, 캐시를 갱신하지 않으면 UI는 절대 바뀌지 않는다.

 

2) 캐시 키 설계 = 동기화 전략

: date / week / month로 쪼갰다면 어떤 변경이 어떤 캐시에 영향을 주는지 명확해야 한다.

 

 

3) optimistic update는 “식별자”가 있어야 가능하다

: ID(routineId)가 없으면 로컬 캐시 업데이트는 애초에 불가능하다.

 

 

4) invalidate는 가장 안전한 기본값

: 특히 “복사”처럼 새 ID가 생성되는 작업은 로컬 업데이트보다 재조회가 정확하다.

 

 

 

| 결론

이번 문제는 겉으로 보면 단순히 “왜 화면이 안 바뀌지?”였지만, 실제로는 캐시 키 구조와 갱신 전략의 문제였습니다.

 

결국 해결은 이 두 문장으로 정리됩니다.

  • 로컬 업데이트가 가능한 경우 → 캐시를 직접 수정
  • 로컬 업데이트가 불확실한 경우 → 필요한 범위만 invalidate

이 글이 비슷한 상황에서 “DB는 바뀌는데 UI는 그대로일 때” 원인을 빠르게 좁히는 데 도움이 되었으면 합니다.

 

읽어주셔서 감사합니다 🙇‍♂️

728x90
반응형

'📋 프로젝트 > ☄️ 트러블 슈팅' 카테고리의 다른 글

[checky] 한글 입력 중 Enter로 태스크가 두 번 생성되던 문제 해결기  (0) 2026.01.12
'📋 프로젝트/☄️ 트러블 슈팅' 카테고리의 다른 글
  • [checky] 한글 입력 중 Enter로 태스크가 두 번 생성되던 문제 해결기
pangil_kim
pangil_kim
기록을 통해 지속적인 성장을 추구합니다.
  • pangil_kim
    멈추지 않는 기록
    pangil_kim
  • 전체
    오늘
    어제
  • 📝 글쓰기
      ⚙️ 관리

    • 분류 전체보기 (405) N
      • 💻 개발 (176) N
        • ※ 참고 지식 (9)
        • 🦕 React (13)
        • 🎩 Next.js (25)
        • 📘 TypeScript (4)
        • 📒 JavaScript (8)
        • 🟩 Node.js (7)
        • 📀 MySQL (24)
        • 🌸 Spring Boot (5)
        • 👷 SveleteKit (24)
        • 🩵 Flutter (11)
        • 🌀 Dart (2)
        • 🌈 CSS (5)
        • 🔸Git (1)
        • 🔥 Firebase (4)
        • 🧑🏻‍💻 코테 (29) N
        • 🕸️ 알고리즘 (4)
        • 🌤️ AWS (1) N
      • 📋 프로젝트 (4) N
        • ☄️ 트러블 슈팅 (2) N
        • 🧑🏻‍💻 서비스 소개 (2)
      • ✍🏻 회고 (52) N
        • ☀️ 취준일지 (6) N
        • 🍀 우테코 (32)
        • 👋 주간회고 (1) N
      • 📰 정보 공유 (12)
      • 🧑🏻‍💻 개발자라면? (1)
      • 🏫 한동대학교 (153)
        • Database (15)
        • Software Engineering (18)
        • EAP (22)
        • 일반화학 (26)
        • 25-1 수업 정리 (19)
        • Computer Networking (36)
        • OPIc (2)
        • 미술의 이해 (15)
  • 최근 글

  • 인기 글

  • 태그

    묵상
    주일
    부트캠프
    프론트엔드
    네트워킹
    웹개발
    computer networks and the internet
    csee
    고윤민교수님
    컴네
    데이터베이스
    한동대학교
    CCM
    웹 프론트엔드 8기
    어노인팅
    예배
    typeScript
    GLS
    전산전자공학부
    글로벌리더십학부
    우아한테크코스
    날솟샘
    우테코 8기
    FE
    우테코
    날마다 솟는 샘물
    프리코스
    찬양
    설교
    QT
  • 최근 댓글

  • 250x250
  • hELLO· Designed By정상우.v4.10.4
pangil_kim
[트러블슈팅] DB는 바뀌는데 UI는 그대로였다, TanStack Query 캐시 동기화 해결
상단으로

티스토리툴바