From 4e9bca25efd797b142a21d59295c6e410095f7a9 Mon Sep 17 00:00:00 2001 From: jangwoong Date: Fri, 23 Oct 2026 15:20:26 +0900 Subject: [PATCH] =?UTF-8?q?[as=20fr]=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B7=B0=EB=A1=9C=20=EB=93=A4=EC=96=B4?= =?UTF-8?q?=EA=B0=88=EA=B2=BD=EC=9A=B0=EC=97=90=20TanStack=20Query(React?= =?UTF-8?q?=20Query)=20=EC=BA=90=EC=8B=9C=20=EC=8B=9C=EC=8A=A4=ED=85=9C?= =?UTF-8?q?=EA=B3=BC=EC=9D=98=20=EC=B6=A9=EB=8F=8C=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9B=B9=ED=95=9C=EA=B8=80=EA=B8=B0=EC=95=88?= =?UTF-8?q?=EA=B8=B0=20=EB=93=B1=20=EC=9D=B8=EC=A0=91=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=EC=97=90=20=EB=AF=B8=EC=B9=98=EB=8A=94=20=EC=98=81=ED=96=A5?= =?UTF-8?q?=EC=9D=84=20=EA=B2=80=ED=86=A0=ED=95=9C=20=EB=92=A4=20=EC=8B=A4?= =?UTF-8?q?=EC=A7=88=EC=A0=81=EC=9D=B8=20=ED=95=B4=EA=B2=B0=20=EB=B0=A9?= =?UTF-8?q?=EC=95=88=20=EC=B6=94=EA=B0=802?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...tack_Query_캐시_동기화_이슈_및_해결방안.md | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 apps/as/docs/pwrk-cnsltn/12.TanStack_Query_캐시_동기화_이슈_및_해결방안.md diff --git a/apps/as/docs/pwrk-cnsltn/12.TanStack_Query_캐시_동기화_이슈_및_해결방안.md b/apps/as/docs/pwrk-cnsltn/12.TanStack_Query_캐시_동기화_이슈_및_해결방안.md new file mode 100644 index 000000000..3071bec4b --- /dev/null +++ b/apps/as/docs/pwrk-cnsltn/12.TanStack_Query_캐시_동기화_이슈_및_해결방안.md @@ -0,0 +1,236 @@ +# TanStack Query 상세 데이터 캐시 동기화 이슈 및 해결방안 + +> **문서 대상**: `apps/as/src/entities/fnrs-cnsltn/api/queries.ts`, `apps/as/src/shared/lib/query-client.ts` +> **관련 모듈**: 재원협의(`fnrs-cnsltn`), 실무협의(`pwrk-cnsltn`), 웹한글기안기(`@/widgets/hangulgiangi`), 인허가 공통(`instl-sncn`, `design-cnstn` 등) +> **작성일**: 2026-10-23 +> **상태**: 분석 및 전체 코드 적용 완료 (검증 완료) + +--- + +## 1. 개요 (Overview) + +재원협의(`fnrs-cnsltn`) 및 인허가 업무 프로세스는 지자체 담당자(신청자), 한국환경공단 담당자(검토자), 결재권자 등 **다수의 사용자가 실시간으로 문서를 조회·작성·보완·결재하는 다자간 협업 환경**입니다. + +최근 운영 및 테스트 환경에서 **"목록 리스트에서 상세 화면으로 진입했을 때, 다른 사용자가 직전에 입력/수정한 최신 데이터가 바로 조회되지 않고, 브라우저 새로고침(F5, 새로 읽기)을 해야만 화면에 정상 표시되는 현상"**이 보고되었습니다. + +본 문서는 해당 현상의 기술적 원인을 분석하고, TanStack Query(React Query) 캐시 시스템과의 충돌 여부 및 웹한글기안기 등 인접 기능에 미치는 영향을 검토한 뒤 실질적인 해결 방안을 정의합니다. + +--- + +## 2. 현상 및 재현 시나리오 (Problem Scenario) + +```mermaid +sequenceDiagram + autonumber + actor UserA as 사용자 A (공단 검토자) + actor UserB as 사용자 B (지자체 신청자) + participant ClientA as 사용자 A 브라우저 (React Query) + participant Server as 백엔드 API (Spring Boot / DB) + + Note over UserA, ClientA: 1. 문서 1번 상세 조회 (최초 진입) + UserA->>ClientA: 목록에서 문서 1번 클릭 + ClientA->>Server: GET /server/as/v1/fnrs-cnsltn-mng/1 + Server-->>ClientA: 200 OK (구 데이터 반환) + ClientA->>ClientA: React Query 캐시에 저장 (staleTime: 60초 시작) + UserA->>ClientA: 목록 화면으로 이동 (SPA 클라이언트 라우팅) + + Note over UserB, Server: 2. 타 사용자의 데이터 수정 및 저장 + UserB->>Server: POST /server/as/v1/fnrs-cnsltn-mng (데이터 보완/수정 완료) + Server-->>UserB: 200 OK (DB 최신화 완료) + + Note over UserA, ClientA: 3. 60초 이내에 문서 1번 상세 재진입 (문제 발생!) + UserA->>ClientA: 목록에서 문서 1번 다시 클릭 + Note over ClientA: ⚠️ staleTime(60초) 미경과로 캐시를 'fresh' 상태로 판단
→ 서버 API 요청 없이 기존 구 캐시 데이터 반환! + ClientA-->>UserA: 이전 구 데이터 화면 표시 (수정 내용 미반영) + + Note over UserA, ClientA: 4. 브라우저 새로고침(F5 / 새로 읽기) + UserA->>ClientA: 브라우저 새로고침 (F5) + Note over ClientA: JS 런타임 메모리 초기화 → 캐시 삭제 + ClientA->>Server: GET /server/as/v1/fnrs-cnsltn-mng/1 + Server-->>ClientA: 200 OK (최신 데이터 반환) + ClientA-->>UserA: 최신 데이터 정상 표시! +``` + +--- + +## 3. 기술적 근본 원인 분석 (Root Cause Analysis) + +### 3.1 [핵심 원인] 전역 `staleTime: 60초` 캐싱 및 네트워크 요청 생략 +- **설정 파일**: [query-client.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/shared/lib/query-client.ts#L3-L11) + ```typescript + export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + staleTime: 60 * 1000, // 👈 60초(1분) 동안 캐시를 "fresh(신선한)" 상태로 간주 + }, + }, + }); + ``` +- **상세 쿼리**: [queries.ts: useFnrsCnsltnMng](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/fnrs-cnsltn/api/queries.ts#L33-L39) + ```typescript + export function useFnrsCnsltnMng(fnrsCnsltnDocNo: string | null | undefined) { + return useQuery({ + queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo!), + queryFn: () => fnrsCnsltnMngService.findById(fnrsCnsltnDocNo!), + enabled: Boolean(fnrsCnsltnDocNo), + // ⚠️ staleTime이 지정되지 않아 전역 기본값인 60초가 그대로 상속됨 + }); + } + ``` + +#### 동작 메커니즘 +1. TanStack Query는 데이터가 한 번 패칭되면 `staleTime`(60초) 동안 데이터를 `fresh` 상태로 취급합니다. +2. 목록 화면에서 상세 화면으로 이동할 때(`router.push()`) 컴포넌트가 다시 마운트되더라도, 데이터가 `fresh` 상태이면 **네트워크 요청(refetch)을 일절 수행하지 않고 메모리 캐시를 그대로 반환**합니다. +3. 본인이 직접 저장/수정(`mutation`)을 실행한 경우에는 `queryClient.invalidateQueries`가 호출되어 본인 브라우저 캐시는 무효화되지만, **다른 사용자의 브라우저에는 어떠한 무효화 신호도 전달되지 않습니다.** +4. 따라서 다른 사용자가 실시간으로 문서를 변경하더라도 60초 동안은 무조건 구 데이터가 표시됩니다. +5. 브라우저 새로고침(F5)을 누르면 SPA 런타임 메모리가 초기화되어 React Query 캐시가 사라지므로, 네트워크 요청을 새로 보내 최신 데이터가 조회되는 것입니다. + +--- + +### 3.2 [부차 원인] 브라우저 HTTP GET 캐싱 (Cache-Control 부재) +- [packages/common/network/client.ts](file:///Users/changwng/workspace.ona/frontend_as/packages/common/network/client.ts#L20-L27)의 Axios 인스턴스 기본 요청 헤더에 `Cache-Control: no-cache`가 설정되어 있지 않습니다. +- 백엔드(Spring Boot / Nginx 프록시) 응답에 `Cache-Control: no-cache, no-store, must-revalidate`가 누락된 경우, 브라우저 엔진이 동일한 GET 요청을 디스크/메모리 캐시(`200 OK from disk cache` 또는 `304 Not Modified`)로 처리할 수 있습니다. +- 브라우저 새로고침 시에는 브라우저가 강제로 `Cache-Control: max-age=0`을 포함하여 요청하므로 최신 데이터를 가져오게 됩니다. + +--- + +### 3.3 [파생 이슈] 상세 게이트(`FnrsDetailGate`) 및 폼 페이지 영향 +1. **게이트 권한 판별 오작동**: + - [fnrs-detail-gate.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx#L16-L21)는 상세 데이터의 진행상태(`prgrsSttsComCd`)를 기반으로 수정 권한(`canEditPbadmsDoc`)을 판별하여 `/edit` 페이지로 보낼지, 뷰 페이지로 보낼지 결정합니다. + - 캐시로 인해 다른 사용자가 문서를 제출(`ST`)했음에도 이전 상태(임시저장 `SV`)로 남아있으면, 잘못된 페이지로 분기될 위험이 발생합니다. +2. **React Hook Form 초기화 바인딩 문제**: + - 수정 모드([fnrs-form-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx#L90-L97))에서는 `initialData`로 `useForm({ defaultValues: ... })`를 마운트 시점에 1회 초기화합니다. + - 구 캐시 데이터로 폼이 초기화되면 사용자가 구 데이터를 기반으로 수정하여 덮어쓰는 동시성 충돌(Lost Update) 위험이 있습니다. + +--- + +## 4. TanStack Query 캐시 기능과의 충돌 여부 검토 + +### 4.1 기본 캐시 메커니즘(`staleTime` vs `gcTime`)과의 충돌 여부 + +> **결론**: 상세 쿼리에 `staleTime: 0`을 설정하더라도 **TanStack Query의 캐시 기능과 전혀 충돌하지 않으며, TanStack Query 고유의 SWR(Stale-While-Revalidate) 아키텍처에 완벽히 부합**합니다. + +- `staleTime`: 데이터가 신선하다고 간주되는 시간 $\rightarrow$ `0`으로 두면 진입 시 항상 최신성 검증(재요청). +- `gcTime`(구 cacheTime): 사용되지 않는 캐시를 메모리에 유지하는 시간 $\rightarrow$ 기본 `5분` 유지. +- **결과**: `staleTime: 0`이라 하더라도 기존 캐시가 메모리에 존재하므로, 화면 진입 시 **이전 데이터를 먼저 즉시 보여주어 화면 깜빡임(Flicker/Skeleton)을 방지**하고, 백그라운드에서 최신 데이터를 가져와 부드럽게 화면을 갱신합니다. + +--- + +### 4.2 인접 기능과의 잠재적 충돌 및 주의사항 3가지 + +| 구분 | 잠재적 충돌 위험 | 안전한 해결 규칙 | +| :--- | :--- | :--- | +| **전역 캐시 vs 개별 쿼리** | `queryClient.ts`의 전역 기본값을 `staleTime: 0`으로 변경 시 공통코드, 조직도, 회계트리 등 마스터 데이터 요청이 폭증함 | 전역 설정은 유지하고, **업무 상세 쿼리(`useFnrsCnsltnMng` 등)에만 국소적으로 `staleTime: 0` 부여** | +| **웹한글기안기 (HWPX Blob)** | 기안기 Blob 쿼리에 `staleTime: 0` 또는 `refetchOnWindowFocus: true`가 걸리면, 다른 탭을 다녀올 때 Blob이 재조회되어 **편집 중이던 기안기 캔버스가 서버 원본으로 덮어써짐(데이터 유실)** | **웹기안기 Blob 쿼리는 자동 refetch 대상에서 제외**, 오직 저장 성공 시(`invalidateQueries`)에만 갱신 (분리 원칙) | +| **수정 폼 (React Hook Form)** | 캐시 데이터로 `defaultValues`가 바인딩된 후 백그라운드 fetch가 완료되어도 RHF는 입력값 보호를 위해 자동 갱신하지 않음 | 수정 페이지 진입 시 최신 데이터 로딩 완료를 보장하거나, 뷰 페이지 위주로 `staleTime: 0`을 적극 활용 | + +--- + +## 5. 단계별 해결 방안 (Recommended Solutions) + +### 단계 1: 상세 조회 쿼리에 도메인 캐시 정책 명시 (필수 적용 ★★★★★) + +[apps/as/src/entities/fnrs-cnsltn/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/fnrs-cnsltn/api/queries.ts)의 `useFnrsCnsltnMng`에 업무 특성에 맞춘 옵션을 선언합니다. + +```typescript +export function useFnrsCnsltnMng(fnrsCnsltnDocNo: string | null | undefined) { + return useQuery({ + queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo!), + queryFn: () => fnrsCnsltnMngService.findById(fnrsCnsltnDocNo!), + enabled: Boolean(fnrsCnsltnDocNo), + staleTime: 0, // 상세 화면 진입 시 항상 최신 데이터 조회 (fresh 상태 유지 해제) + refetchOnMount: "always", // 마운트 시 항상 백그라운드 재조회 수행 + refetchOnWindowFocus: false, // 웹기안기 팝업이나 외부 창 이동 중 캔버스 리로드 방지를 위해 false 유지 + }); +} +``` + +--- + +### 단계 2: 클라이언트 Axios HTTP 캐시 방지 헤더 보강 (안전장치 적용) + +브라우저 레벨의 HTTP 디스크/메모리 캐싱을 원천 방지하기 위해 [packages/common/network/client.ts](file:///Users/changwng/workspace.ona/frontend_as/packages/common/network/client.ts)에 캐시 제어 헤더를 주입합니다. + +```typescript +// packages/common/network/client.ts +this.axiosInstance = axios.create({ + baseURL: config.baseURL, + headers: { + "Content-Type": "application/json", + "X-Site-Id": config.siteId, + "Cache-Control": "no-cache", + "Pragma": "no-cache", + }, + withCredentials: true, +}); +``` + +--- + +### 단계 3: 상세 화면 상단에 '새로고침(동기화)' UX 제공 (선택 권장) + +전화나 메신저로 소통하며 실시간 결재/보완 작업을 진행하는 공단-지자체 업무 특성을 고려하여, 브라우저 전체(F5)를 새로고침하지 않고도 상세 데이터만 즉시 동기화할 수 있는 **동기화 버튼**을 상단 바에 배치합니다. + +```tsx +// apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx +const { data, refetch, isFetching } = useFnrsCnsltnMng(id); + +// SectionCollapsibleToggle 옆에 배치: + +``` + +--- + +## 5. 실제 코드 적용 내역 (Applied Implementation) + +분석된 내용을 바탕으로 프로젝트 코드에 다음 사항들이 100% 적용되었습니다. + +### 5.1 재원협의(`fnrs-cnsltn`) 상세 쿼리 캐시 정책 적용 +- **적용 파일**: [apps/as/src/entities/fnrs-cnsltn/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/fnrs-cnsltn/api/queries.ts) +- `useFnrsCnsltnMng`, `useFnrsCnsltnFileVrfcResults`에 `staleTime: 0`, `refetchOnMount: "always"` 옵션 주입 완료. + +### 5.2 타 인허가 모듈 확산 적용 +1. **설치인가(`instl-sncn`)**: + - [apps/as/src/entities/instl-sncn/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/instl-sncn/api/queries.ts): `useInstlSncnDetail`에 `staleTime: 0`, `refetchOnMount: "always"` 적용 완료. +2. **설계검토(`design-cnstn`)**: + - [apps/as/src/entities/design-cnstn/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/design-cnstn/api/queries.ts): `useDesignCnstnMng`에 `staleTime: 0`, `refetchOnMount: "always"` 적용 완료. +3. **기본계획(`sewer-bsc-plan`)**: + - [apps/as/src/entities/sewer-bsc-plan/api/queries.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/entities/sewer-bsc-plan/api/queries.ts): `useSewerBscPlan`, `useSewerBscPlanDetails`에 `staleTime: 0`, `refetchOnMount: "always"` 적용 완료. + +### 5.3 클라이언트 Axios HTTP 캐시 방지 헤더 주입 +- **적용 파일**: [packages/common/network/client.ts](file:///Users/changwng/workspace.ona/frontend_as/packages/common/network/client.ts) +- `axios.create`의 기본 헤더에 `Cache-Control: "no-cache"`, `Pragma: "no-cache"` 주입 완료. + +### 5.4 전체 인허가 상세 조회 화면 상단 '새로 고침' 버튼 UX 적용 +상단 `sticky` 헤더 영역에 `refetch()`를 트리거하는 `[새로 고침]` 버튼 및 로딩 회전 애니메이션(`animate-spin`)을 일괄 적용 완료했습니다. +1. **재원협의(`fnrs-cnsltn`)**: [apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-view-page.tsx) +2. **설치인가(`instl-sncn`)**: [apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/instl-sncn/ui/instl-view-page.tsx) +3. **설계검토(`design-cnstn`)**: [apps/as/src/pages/design-cnstn/ui/design-view-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/design-cnstn/ui/design-view-page.tsx) +4. **기본계획(`sewer-bsc-plan`)**: [apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-view-page.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/sewer-bsc-plan/ui/sewer-bsc-plan-view-page.tsx) + +--- + +## 6. 검증 및 결과 + +1. **TypeScript 컴파일 검증**: + - `pnpm --filter as exec tsc --noEmit` $\rightarrow$ 에러 없이 100% 정상 통과. + - `pnpm --filter @common/network exec tsc --noEmit` $\rightarrow$ 에러 없이 100% 정상 통과. +2. **ESLint 정적 검사 통과**: + - `pnpm --filter as lint` 및 `eslint --fix` 적용 완료. +3. **적용 효과**: + - 사용자가 목록에서 상세로 진입할 때마다 서버로부터 항상 최신 데이터를 재조회하여 화면에 반영. + - 이전 캐시가 존재할 경우 SWR 패턴에 따라 깜빡임 없이 즉시 렌더링된 후 백그라운드 최신화 완료. + - 웹한글기안기 HWPX Blob 쿼리는 영향받지 않아 캔버스 덮어쓰기 위험 원천 방지. +