WI-...-028~033 에 대응하는 FE-OC-027~032 owner 노트. - 각 노트의 제외 범위에 'use case·domain model·business rule' 을 명시해 이 skeleton 이 port 와 adapter 계약까지임을 못박았다 - 근거 절에 '근거 등급 경계' 문단을 넣어 project-local default 와 외부 근거를 구분했다 (수집은 FE-Q-011~014 가 소유) - 구현 가이드 절은 비워 두고 사유를 남겼다 — 근거 raw 없이 채우면 모든 cell 이 UNSUPPORTED_IMPL_DECISION 이 된다 - multi-protocol 노트는 '신규 port 0개' 를 제외 범위에 명시했다. port 가 늘어나면 그 자체가 회귀 신호다
242 lines
14 KiB
Markdown
242 lines
14 KiB
Markdown
---
|
|
title: branch / feature-frontend-cache-tier-cross-tab-invalidation-contract
|
|
source_type: branch-note
|
|
status: raw
|
|
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-029
|
|
kind: project-work-item
|
|
project: ca-skeleton-frontend-operational-contract
|
|
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-029
|
|
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CAPABILITY-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CROSS-TAB-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SERVER-STATE-001@1]
|
|
refines: []
|
|
overrides: []
|
|
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-010, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-011]
|
|
imports: [FE-GATE-028@1, FE-OC-012@1, FE-OC-013@1, FE-OC-023@1]
|
|
delegates: [DELEG-FE-009]
|
|
accepts_delegations: []
|
|
contract_packet: 1
|
|
branch: feature-frontend-cache-tier-cross-tab-invalidation-contract
|
|
parent_branch:
|
|
related_projects: [ca-skeleton-frontend, ca-skeleton]
|
|
tags: [branch, ca-skeleton-frontend, cache, server-state, cross-tab]
|
|
created: 2026-07-28
|
|
target_merge:
|
|
status_label: in-progress
|
|
---
|
|
|
|
# branch: feature-frontend-cache-tier-cross-tab-invalidation-contract
|
|
|
|
<!-- section-id: branch-parent -->
|
|
## 부모 (필수)
|
|
|
|
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
|
|
|
|
형제 branch (같은 부모, 이번 확장에서 함께 생성):
|
|
|
|
- [[raw/branch-notes/feature-frontend-binary-file-io-store-contract]]
|
|
- [[raw/branch-notes/feature-frontend-large-object-transfer-contract]]
|
|
- [[raw/branch-notes/feature-frontend-multi-protocol-api-transport-contract]]
|
|
- [[raw/branch-notes/feature-frontend-realtime-subscription-lifecycle-contract]]
|
|
- [[raw/branch-notes/feature-frontend-background-execution-worker-contract]]
|
|
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
- **생성 시 프로젝트 개정**: `2`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: version 파티션·탭 간 무효화·채널 부재 fallback fixture가 통과한다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| Decision Ref | Project Summary | Branch Application | Source |
|
|
|---|---|---|---|
|
|
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CAPABILITY-001@1` | 신규 runtime capability 6종은 FE-REG-CAPABILITY flag로 default OFF이며 활성화는 owner·gate·runbook을 동반한다 | `CAP_FE_CACHE_PERSISTENCE` 를 이 branch 가 소유하고 default OFF 로 유지한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
|
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CROSS-TAB-001@1` | 탭 간 무효화는 BroadcastChannel 우선에 storage event fallback을 쓰고 leader election 없이 무효화 key만 전파한다 | `CrossTabSyncPort` adapter 의 transport 선택과 메시지 봉투에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
|
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SERVER-STATE-001@1` | application-owned QueryCachePort와 TanStack Query adapter를 사용하고 client store에 server state를 복제하지 않는다 | 영속 tier 를 추가해도 `QueryCachePort` 를 우회하지 않는다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
|
|
|
<!-- section-id: branch-local-decisions -->
|
|
### 브랜치 지역 결정
|
|
|
|
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
|
|---|---|---|---|---|
|
|
| D1 | 영속 캐시 파티션 키는 `releaseId`·`configSchemaVersion`·`apiContractVersion` 세 값을 모두 포함하고 하나라도 불일치하면 복원하지 않고 폐기한다 | `refines DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SERVER-STATE-001@1` | 근거 raw 미수집 (`FE-Q-011`) | `proposed` |
|
|
| D2 | 탭 간 메시지는 무효화 key 만 싣고 값을 싣지 않는다 | `refines DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CROSS-TAB-001@1` | 근거 raw 미수집 (`FE-Q-011`) | `proposed` |
|
|
| D3 | `CachePersistencePort` 는 `BlobStorePort` 를 재사용하지 않고 자체 백엔드를 가진다 | `local` | 근거 raw 미수집 (`FE-Q-011`) | `proposed` |
|
|
|
|
<!-- section-id: declared-overrides -->
|
|
### 선언한 예외
|
|
|
|
해당 없음.
|
|
|
|
<!-- GENERATED: artifact-imports:start -->
|
|
### 가져온 artifact 계약
|
|
|
|
| Artifact Ref | Owner | Producer | Schema Ref |
|
|
|---|---|---|---|
|
|
<!-- GENERATED: artifact-imports:end -->
|
|
|
|
<!-- GENERATED: project-contract-imports:start -->
|
|
## 가져온 프로젝트 계약
|
|
|
|
| Ref | Owner | 요약 | Branch 적용 |
|
|
|---|---|---|---|
|
|
| `FE-GATE-028@1` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | version 파티션·탭 간 무효화 fixture 가 실패하면 merge 를 MUST 차단 | 이 branch 가 owner 로서 fixture 와 report 를 산출 |
|
|
| `FE-OC-012@1` | [[raw/branch-notes/feature-server-state-caching-contract]] | query key와 invalidation은 registry factory만 MUST 사용 | 탭 간 전파도 factory key 만 사용 |
|
|
| `FE-OC-013@1` | [[raw/branch-notes/feature-frontend-storage-registry-contract]] | storage key는 namespace·version·classification을 MUST 가지며 token/secret 저장을 금지 | `QUERY_CACHE_SNAPSHOT` 행을 소비 |
|
|
| `FE-OC-023@1` | [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] | API/config/storage/release schema의 breaking change는 migration 또는 version bump 없이 배포하면 안 됨 | 파티션 키 불일치 시 폐기 규칙이 migration 대체 |
|
|
<!-- GENERATED: project-contract-imports:end -->
|
|
|
|
<!-- GENERATED: received-delegations:start -->
|
|
### 수신한 위임
|
|
|
|
없음. 이 branch 는 `DELEG-FE-009` 의 delegator 다.
|
|
|
|
<!-- GENERATED: received-delegations:end -->
|
|
|
|
<!-- GENERATED: flow:start -->
|
|
### 가져온 흐름 단계
|
|
|
|
| Stage Ref | Order | Owner | Input | Action | Output |
|
|
|---|---:|---|---|---|---|
|
|
<!-- GENERATED: flow:end -->
|
|
|
|
<!-- section-id: branch-goal -->
|
|
## 목표
|
|
|
|
`CachePersistencePort` 와 `CrossTabSyncPort` 를 정의하고, memory↔session↔local↔IndexedDB 캐시 계층과 탭 간 무효화를 고정한다. 이 계약이 없으면 탭 A 의 mutation 이 탭 B 의 캐시를 무효화하지 않아 두 탭이 서로 다른 사실을 보여주고, release 를 넘어 살아남은 영속 캐시가 새 스키마로 파싱되어 렌더 트리 깊은 곳에서 터진다.
|
|
|
|
- 이슈:
|
|
- PR:
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- `CachePersistencePort` — 캐시 스냅샷 직렬화·영속·복원과 복원 거부
|
|
- `CrossTabSyncPort` — BroadcastChannel 우선, `storage` event fallback, 채널 부재 시 탭 내 무효화만
|
|
- `persistenceTier`·`crossTabScope` 규칙(`FE-REG-QUERY` 확장) 소비
|
|
- release·config·API version 파티션과 불일치 시 폐기
|
|
- `CAP_FE_CACHE_PERSISTENCE` capability 행 소유
|
|
|
|
### 제외 범위
|
|
|
|
- **use case, domain model, business rule** — port 와 adapter 계약까지만 정의한다
|
|
- `QueryCachePort` 의 **정책**(stale time·gc·refetch·invalidation 매핑) — [[raw/branch-notes/feature-server-state-caching-contract]] 소유
|
|
- physical storage key·namespace·classification·quota fallback — `DELEG-FE-009` 로 [[raw/branch-notes/feature-frontend-storage-registry-contract]] 에 위임
|
|
- offline-first 동기화 충돌 해결(CRDT·last-write-wins 등)
|
|
- leader election 기반 단일 리더 동기화 — `FE-D028` 이 명시적으로 배제
|
|
|
|
## 근거 (필수, 최소 1개+)
|
|
|
|
| Source | 정당화하는 결정 |
|
|
|---|---|
|
|
| `[[docs/superpowers/specs/2026-07-28-ca-skeleton-frontend-runtime-adapter-features-design]]` §5.1·§6.2 | port 분해와 `FE-REG-QUERY` 확장 |
|
|
| [[raw/official-docs/tanstack-query-server-state-official]] | `QueryCachePort` 정책의 상위 근거 (persistence 는 이 문서가 다루지 않음) |
|
|
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.7·§9.2 | query key registry 와 cache defaults |
|
|
|
|
**근거 등급 경계**: `FE-D028`(탭 간 무효화 transport)의 rationale 은 `project-local default, 외부 source claim 아님` 이다. TanStack Query 공식 문서는 persister 를 다루지만 이 repo 의 raw 발췌에는 그 내용이 없으며, BroadcastChannel·`storage` event 근거도 미수집이다(`FE-Q-011`).
|
|
|
|
## TODO
|
|
|
|
- [ ] `CachePersistencePort` 인터페이스와 파티션 키 규칙 확정 — 등급: `planned`
|
|
- [ ] `CrossTabSyncPort` 인터페이스와 메시지 봉투 확정 — 등급: `planned`
|
|
- [ ] version 불일치 캐시 폐기 fixture — 등급: `planned`
|
|
- [ ] 탭 A mutation → 탭 B 무효화 integration fixture — 등급: `planned`
|
|
- [ ] BroadcastChannel 부재 시 `storage` event fallback fixture — 등급: `planned`
|
|
- [ ] 두 transport 모두 불가 시 `CROSS_TAB_CHANNEL_UNAVAILABLE` 처리 — 등급: `planned`
|
|
- [ ] `FE-GATE-028` cache tier report 산출 — 등급: `planned`
|
|
|
|
## 진행 중 메모
|
|
|
|
`crossTabScope: same-origin` 이 값이 아니라 key 만 전파하는 이유는 두 가지다. 값을 전파하면 (1) 수신 탭이 자기 권한으로 얻지 않은 데이터를 갖게 되고, (2) 메시지가 커져 `storage` event fallback 의 크기 제한에 부딪힌다. 수신 탭은 key 를 받아 자기 `QueryCachePort` 로 refetch 한다.
|
|
|
|
## 결정 사항
|
|
|
|
- 2026-07-28: 파티션 키에 세 version 을 모두 포함 / 이유: 하나만 쓰면 config 만 바뀐 배포에서 stale 캐시가 살아남음 / 검토한 대안: `releaseId` 단독 / 근거: 근거 raw 미수집, project-local 판단 (`FE-Q-011`)
|
|
- 2026-07-28: leader election 미도입 / 이유: 탭 간 무효화에 리더가 필요 없고 리더 선출 자체가 새 실패 모드 / 검토한 대안: Web Locks 기반 리더 / 근거: `FE-D028`
|
|
|
|
<!-- section-id: decision-evidence -->
|
|
## 결정-근거 매핑
|
|
|
|
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
|
|---|---|---|---|---|---|
|
|
| D1 | 세 version 파티션 + 불일치 시 폐기 | 항상. migration 이 폐기보다 싼 대용량 캐시가 생기면 재검토 | 없음 — `FE-Q-011` | `project-local default` | 매 배포마다 캐시가 비워져 첫 로드가 느려질 수 있음 |
|
|
| D2 | key 만 전파 | 항상. 값 전파가 필요한 실시간 협업 요구가 생기면 재검토 | 없음 — `FE-Q-011` | `project-local default` | 수신 탭의 refetch 가 몰려 backend 부하가 튈 수 있음 |
|
|
| D3 | `CachePersistencePort` 가 자체 백엔드 보유 | 항상. 두 port 가 같은 IndexedDB 를 두고 quota 경쟁하면 재검토 | 없음 — `FE-Q-011` | `project-local default` | 같은 origin 에서 두 개의 IndexedDB 사용처가 생김 |
|
|
|
|
<!-- section-id: implementation -->
|
|
## 구현 가이드
|
|
|
|
> 근거 raw 자료(`FE-Q-011`) 수집 전까지 비워 둔다. 지금 채우면 모든 cell 이 `UNSUPPORTED_IMPL_DECISION` 이 된다.
|
|
|
|
<!-- section-id: edge-failure-dependency -->
|
|
## 엣지·실패·의존
|
|
|
|
- **실패·엣지 경로**
|
|
- 직렬화·영속·복원 실패 → `CACHE_PERSISTENCE_FAILURE`, 메모리 캐시만 사용하고 제품 흐름을 막지 않음
|
|
- BroadcastChannel 과 `storage` event 모두 불가 → `CROSS_TAB_CHANNEL_UNAVAILABLE`, 탭 내 무효화만 수행
|
|
- 파티션 키 불일치 캐시 발견 → 복원하지 않고 폐기. 부분 복원 금지
|
|
- 수신 탭이 무효화 key 를 받았으나 해당 query 를 구독하지 않음 → 무시 (에러 아님)
|
|
- 다중 탭이 동시에 같은 key 를 무효화 → 중복 refetch 를 `QueryCachePort` 의 dedup 이 흡수해야 함
|
|
- **다른 계약 의존**
|
|
- [[raw/branch-notes/feature-server-state-caching-contract]] 의 `QueryCachePort` 정책에 의존 — invalidation 매핑이 바뀌면 전파 대상이 바뀜
|
|
- [[raw/branch-notes/feature-frontend-storage-registry-contract]] 의 `QUERY_CACHE_SNAPSHOT` 행에 의존 (`DELEG-FE-009`)
|
|
- [[raw/branch-notes/feature-frontend-contract-compatibility-governance]] 의 version tuple 에 의존 — 파티션 키가 그 tuple 에서 나옴
|
|
|
|
<!-- section-id: claims-to-verify -->
|
|
## 검증해야 할 주장
|
|
|
|
| Claim | Why uncertain | How to verify | Status |
|
|
|---|---|---|---|
|
|
| version 불일치 캐시가 복원되지 않는다 | 부분 복원이 조용히 성공하기 쉬움 | negative fixture — 이전 version 스냅샷 주입 후 복원 시도가 폐기로 끝나는지 | `planned` |
|
|
| 탭 A mutation 이 탭 B 캐시를 무효화한다 | BroadcastChannel 은 같은 origin 의 다른 탭에서만 동작 | integration test — 두 컨텍스트에서 발행/수신 확인 | `planned` |
|
|
| BroadcastChannel 부재 시 `storage` event 로 대체된다 | fallback 경로가 실제로 도달하는지 불확실 | fixture — BroadcastChannel 을 undefined 로 만들고 전파 확인 | `planned` |
|
|
| key 만 전파해도 UI 가 일관된다 | 수신 탭의 refetch 타이밍에 따라 잠깐 어긋날 수 있음 | integration test — 전파 후 두 탭의 최종 상태 일치 | `needs-confirmation` |
|
|
| 다중 탭 동시 무효화가 refetch 폭주를 만들지 않는다 | dedup 이 `QueryCachePort` 책임인지 이 branch 책임인지 경계가 얇음 | 부하 fixture — N개 탭 시뮬레이션 후 실제 요청 수 측정 | `needs-confirmation` |
|
|
|
|
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
|
|
|
미생성.
|
|
|
|
## 마주친 문제
|
|
|
|
없음.
|
|
|
|
## 묶음 (이 branch에서 파생된 자료)
|
|
|
|
### Sub-branches (세부 작업)
|
|
|
|
아직 없음.
|
|
|
|
### 오류 기록 (이 branch 작업 중 발생)
|
|
|
|
아직 없음.
|
|
|
|
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
|
|
|
아직 없음.
|
|
|
|
### 강의 (이 작업을 위해 학습한 강의)
|
|
|
|
아직 없음.
|
|
|
|
### job-posting tie-ins (이 작업에서 파생된 글감)
|
|
|
|
아직 없음.
|
|
|
|
## 관련 일일 노트
|
|
|
|
- 아직 없음
|
|
|
|
## 완료 후 정리
|
|
|
|
- PR 링크:
|
|
- 리뷰 메모:
|
|
- 머지 결과 / 배포 환경:
|
|
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
|
- `actually-implemented` 항목: 없음
|
|
- `locally-verified` 항목: 없음
|
|
- `prod-verified` 항목: 없음
|
|
- **추출하지 않을 항목**: 현재 전 항목 `planned`
|