[as fr] 리스트에서 뷰로 들어갈경우에 TanStack Query(React Query) 캐시 시스템과의 충돌 여부 및 웹한글기안기 등 인접 기능에 미치는 영향을 검토한 뒤 실질적인 해결 방안 추가2

This commit is contained in:
2026-10-23 15:20:26 +09:00
parent 23d62ab5e6
commit 4e9bca25ef
@@ -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' 상태로 판단<br/>→ 서버 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 옆에 배치:
<Button
type="button"
variant="outline"
size="sm"
onClick={() => refetch()}
disabled={isFetching}
className="flex items-center gap-1"
>
<RefreshCw className={cn("size-4", isFetching && "animate-spin")} />
<span>새로고침</span>
</Button>
```
---
## 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 쿼리는 영향받지 않아 캔버스 덮어쓰기 위험 원천 방지.