33 KiB
title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, imports, delegates, accepts_delegations, contract_packet, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label
| title | source_type | status | id | kind | project | work_item | inherits | refines | overrides | depends_on | imports | delegates | accepts_delegations | contract_packet | branch | parent_branch | related_projects | governing_docs | tags | created | target_merge | status_label | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| branch / feature-frontend-cache-tier-cross-tab-invalidation-contract | branch-note | raw | BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-029 | project-work-item | ca-skeleton-frontend-operational-contract | WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-029 |
|
|
|
|
1 | feature-frontend-cache-tier-cross-tab-invalidation-contract |
|
|
|
2026-07-28 | in-progress |
branch: feature-frontend-cache-tier-cross-tab-invalidation-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
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
2 - 패킷 스키마:
contract_packet: 1 - 완료 조건: version 파티션·탭 간 무효화·채널 부재 시 탭 내 무효화 fixture가 통과한다
상속한 프로젝트 결정
| 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@2 |
탭 간 무효화는 BroadcastChannel만 쓰고 별도 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 |
브랜치 지역 결정
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
| D1 | 영속 캐시 파티션 키는 releaseId·configSchemaVersion·apiContractVersion 세 값을 모두 포함하고 하나라도 불일치하면 복원하지 않고 폐기한다 |
refines DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SERVER-STATE-001@1 |
raw/official-docs/tanstack-query-persistence-hydration-official.md#C1, #C2 (폐기 동작). tuple 구성은 project-local |
proposed |
| D2 | 탭 간 메시지는 무효화 key 만 싣고 값을 싣지 않는다 | refines DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CROSS-TAB-001@2 |
raw/official-docs/mdn-broadcastchannel-storage-event.md#C5 (structured clone 이라 값 전송은 가능 — 금지는 우리 선택) |
proposed |
| D3 | CachePersistencePort 는 BlobStorePort 를 재사용하지 않고 자체 백엔드를 가진다 |
UNSUPPORTED_DECISION — 조사 후에도 근거 없음. 오히려 raw/official-docs/mdn-storage-quotas-eviction-persistence.md#C1·#C2 는 백엔드를 나눠도 quota·eviction 은 origin 단위로 함께 움직인다고 말하므로 "quota 격리" 를 이 결정의 근거로 쓸 수 없다. trade-off: 그럼에도 분리를 택한 이유는 FE-OC-002 의 port 경계다 — 캐시 스냅샷과 전송 버퍼는 수명·폐기 규칙·소유 branch 가 전부 다르고, 하나의 port 로 묶으면 한쪽 폐기 규칙이 다른 쪽에 샌다. 비용은 같은 origin 에 IndexedDB 사용처가 둘로 늘어나는 것이며 이는 quota 경쟁으로 나타난다 |
proposed |
|
| D4 | 로컬 무효화는 채널을 거치지 않고 직접 수행한다. adapter 는 컨텍스트당 BroadcastChannel 객체를 정확히 1개 유지한다 |
local |
raw/official-docs/mdn-broadcastchannel-storage-event.md#C2 (보낸 객체만 제외) |
proposed |
| D5 | 채널을 만들 수 없으면 대체 경로를 만들지 않고 탭 내 무효화만 수행한다 | refines DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CROSS-TAB-001@2 |
raw/official-docs/mdn-broadcastchannel-storage-event.md#C4 (2022-03 부터 모든 주요 브라우저에서 동작), #C10 (대체 후보였던 sessionStorage 경로는 다른 탭에 도달하지 않음) |
proposed |
| D6 | 복원은 bootstrap 에서 await 하여 첫 렌더 이전에 끝낸다. 복원 실패·불일치는 부팅을 막지 않고 메모리 캐시로 진행한다 | local |
raw/official-docs/tanstack-query-persistence-hydration-official.md#C5 (복원 중 렌더는 mount·fetch 와 경합) |
proposed |
| D7 | 영속 대상은 성공한 query 중 FE-REG-QUERY.persistenceTier 가 memory 가 아닌 행으로 한정하고, 직렬화는 adapter 책임이다 |
local |
raw/official-docs/tanstack-query-persistence-hydration-official.md#C6 (기본이 성공 query 만), #C7 (직렬화는 소비자 책임) |
proposed |
deferred (이번 회차 조사 범위 밖): D8 — 영속 write 의 최소 간격과 병합 정책. tanstack-query-persistence-hydration-official.md#C4 는 번들 persister 가 1초 throttle 을 쓴다고만 말하므로 custom adapter 값의 근거가 아니다. 적정 간격은 스냅샷 크기 실측 후 정한다.
선언한 예외
해당 없음.
가져온 artifact 계약
| Artifact Ref | Owner | Producer | Schema Ref |
|---|
가져온 프로젝트 계약
| 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 대체 |
수신한 위임
없음. 이 branch 는 DELEG-FE-009 의 delegator 다.
가져온 흐름 단계
| Stage Ref | Order | Owner | Input | Action | Output |
|---|
목표
CachePersistencePort 와 CrossTabSyncPort 를 정의하고, memory↔session↔local↔IndexedDB 캐시 계층과 탭 간 무효화를 고정한다. 이 계약이 없으면 탭 A 의 mutation 이 탭 B 의 캐시를 무효화하지 않아 두 탭이 서로 다른 사실을 보여주고, release 를 넘어 살아남은 영속 캐시가 새 스키마로 파싱되어 렌더 트리 깊은 곳에서 터진다.
- 이슈:
- PR:
범위
포함 범위
CachePersistencePort— 캐시 스냅샷 직렬화·영속·복원과 복원 거부CrossTabSyncPort— BroadcastChannel 전송, 채널 부재 시 탭 내 무효화만persistenceTier·crossTabScope규칙(FE-REG-QUERY확장) 소비- release·config·API version 파티션과 불일치 시 폐기
CAP_FE_CACHE_PERSISTENCEcapability 행 소유
제외 범위
- 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/official-docs/tanstack-query-persistence-hydration-official | D1 불일치 캐시 전량 폐기 · D6 복원/렌더 경합 gating · D7 영속 대상 선별과 직렬화 책임 |
| raw/official-docs/mdn-broadcastchannel-storage-event | D2 메시지 봉투 제약 · D4 발신자 자기 수신 불가 · D5 fallback 이 localStorage 여야 하는 이유 |
| raw/official-docs/mdn-storage-quotas-eviction-persistence | D2 fallback 크기 예산(#C6) · D3 반증(#C1·#C2 — 백엔드를 나눠도 quota 는 분리되지 않음) |
| raw/project-notes/ca-skeleton-frontend-operational-contract §5.7·§9.2 | query key registry 와 cache defaults |
근거 등급 경계: 2026-07-28 /branch-spec 조사로 채널의 의미론(전달 범위·자기 제외·직렬화)과 캐시 영속의 폐기·복원 동작은 공식 문서 근거를 확보했다. 아직 근거가 없는 것은 파티션 키를 세 값으로 구성한다는 D1 의 tuple 구성이다 — 라이브러리는 buster 가 문자열이라는 것만 말한다(tanstack-query-persistence-hydration-official#C1). §검증해야 할 주장에 기록했다. 조사 중 나왔던 "대체 경로의 존재 이유 부족" 은 경로 자체를 삭제해 해소했다(§Audit & Findings).
TODO
CachePersistencePort인터페이스와 파티션 키 규칙 확정 — 등급:plannedCrossTabSyncPort인터페이스와 메시지 봉투 확정 — 등급:planned- version 불일치 캐시 폐기 fixture — 등급:
planned - 탭 A mutation → 탭 B 무효화 integration fixture — 등급:
planned - BroadcastChannel 부재 시
CROSS_TAB_CHANNEL_UNAVAILABLE+ 탭 내 무효화만 (D5) — 등급:planned - 발신 탭 자기 수신 없음 negative fixture (D4) — 등급:
planned - 복원 await 가 첫 렌더보다 앞서는지 순서 fixture (D6) — 등급:
planned persistenceTier미지정 query 가 영속되지 않음을 확인하는 fixture (D7) — 등급:planned- 파티션 폐기·fallback 진입·채널 부재 telemetry event 등록 (관심사 커버리지 should-fix) — 등급:
planned FE-GATE-028cache tier report 산출 — 등급:planned
진행 중 메모
crossTabScope: same-origin 이 값이 아니라 key 만 전파하는 이유는 하나다. 값을 전파하면 수신 탭이 자기 권한으로 얻지 않은 데이터를 갖게 된다. 수신 탭은 key 를 받아 자기 QueryCachePort 로 refetch 한다.
이건 플랫폼 제약이 아니다. mdn-broadcastchannel-storage-event#C5 대로 structured clone 이라 값을 그대로 보낼 수 있다 — 안 보내는 건 우리 정책이다.
결정 사항
- 2026-07-28: 파티션 키에 세 version 을 모두 포함 / 이유: 하나만 쓰면 config 만 바뀐 배포에서 stale 캐시가 살아남음 / 검토한 대안:
releaseId단독 / 근거: 근거 raw 미수집, project-local 판단 (FE-Q-011) - 2026-07-28: leader election 미도입 / 이유: 탭 간 무효화에 리더가 필요 없고 리더 선출 자체가 새 실패 모드 / 검토한 대안: Web Locks 기반 리더 / 근거:
FE-D028 - 2026-07-28:
storageevent 대체 경로 삭제 / 이유: BroadcastChannel 이 2022-03 부터 모든 주요 브라우저에서 동작하고, 대체가 필요한 환경이 지원 대상에 있다는 근거가 없다. 평소 실행되지 않는 경로는 테스트로도 검증되지 않으면서 저장 예산·자기 수신 제외·연속 동일값 회피 장치를 계속 요구한다 / 검토한 대안:localStoragesignal key 유지 / 근거:mdn-broadcastchannel-storage-event#C4·#C10, hubCROSS-TAB-001revision 2
결정-근거 매핑
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | 세 version 파티션 + 불일치 시 전량 폐기 | 항상. migration 이 폐기보다 싼 대용량 캐시가 생기면 재검토 | tanstack-query-persistence-hydration-official.md#C1(buster 불일치 → discarded), #C2(removeClient() 후 즉시 폐기, 부분 복원 경로 없음) |
official-reference(폐기 동작) + project-local default(tuple 구성) |
매 배포마다 캐시가 비워져 첫 로드가 느려진다. 세 값 중 어느 것이 실제로 캐시를 무효화해야 하는지는 미측정 |
| D2 | key 만 전파 | 항상. 값 전파가 필요한 실시간 협업 요구가 생기면 재검토 | mdn-broadcastchannel-storage-event.md#C5(structured clone — 값 전송은 기술적으로 가능), mdn-storage-quotas-eviction-persistence.md#C6(localStorage 5 MiB) |
project-local default(금지 자체) + official-reference(제약) |
수신 탭의 refetch 가 몰려 backend 부하가 튄다. dedup 경계가 QueryCachePort 와 얇다 |
| D3 | CachePersistencePort 가 자체 백엔드 보유 |
항상. 단, quota 격리를 근거로 쓰면 안 된다 | 없음 — 조사 결과 반증만 나왔다(mdn-storage-quotas-eviction-persistence.md#C1·#C2: origin 단위 전량 eviction) |
UNSUPPORTED_DECISION |
같은 origin 에 IndexedDB 사용처가 둘. 한쪽이 quota 를 소진하면 다른 쪽도 함께 브라우저 eviction 대상이 된다 |
| D4 | 로컬 무효화는 채널 왕복 없이 직접 수행 | 항상. 발신자 echo 가 없으므로 예외 없음 | mdn-broadcastchannel-storage-event.md#C2 |
official-reference |
채널 객체를 실수로 2개 만들면 자기 메시지를 자기가 받는 경로가 생겨 무효화가 2회 실행된다 |
| D5 | 대체 경로 없음 — 채널 부재 시 탭 내 무효화만 | 항상. 지원 대상 브라우저에 BroadcastChannel 미동작 환경이 실제로 들어오면 재검토 | mdn-broadcastchannel-storage-event.md#C4, #C10 |
official-reference |
미동작 환경이 나중에 발견되면 그 환경의 사용자는 다중 탭에서 stale 을 본다. 감지는 CROSS_TAB_CHANNEL_UNAVAILABLE 계측에 의존한다 |
| D6 | 복원을 첫 렌더 이전에 await | 항상. 복원 시간이 체감 가능해지면 skeleton UI 로 보완하되 순서는 유지 | tanstack-query-persistence-hydration-official.md#C5 |
official-reference |
복원이 느린 저사양 기기에서 첫 페인트가 지연된다 |
| D7 | 성공 query + persistenceTier 로 대상 한정, 직렬화는 adapter |
항상. error/pending 캐시를 살려야 할 요구가 생기면 재검토 | tanstack-query-persistence-hydration-official.md#C6, #C7 |
official-reference |
registry 에 persistenceTier 를 빠뜨린 신규 query 는 조용히 영속되지 않는다 — gate 가 잡아야 함 |
구현 가이드
2026-07-28
/branch-spec조사(MDN 2건 + TanStack 공식 2페이지)로 in-scope detail 을 채웠다. 근거가 원칙만 지지하고 detail 은 지지하지 않는 cell 은UNSUPPORTED_IMPL_DECISION+ trade-off 한 줄로 표시했다(CLAUDE.md §15.5 R2). 다른 branch 결정 영역(QueryCachePort정책, storage key·classification)은 남기지 않았다(R3).
1. CrossTabSyncPort — 채널 하나와 발신자 처리
Trace: D4(로컬 무효화 직접 수행) ←
mdn-broadcastchannel-storage-event.md#C2/ D5(대체 경로 없음) ← 같은 문서#C4·#C10/ 상속DEC-…-CROSS-TAB-001@2
adapter 는 부팅 시 채널을 한 번 만들고 그 뒤로 바꾸지 않는다. 경로는 두 개뿐이다.
| 조건 | 동작 |
|---|---|
BroadcastChannel 생성 성공 |
채널 객체를 컨텍스트당 1개 만들어 발행·수신 모두에 쓴다 |
| 생성 실패 | CROSS_TAB_CHANNEL_UNAVAILABLE 로 표면화하고 탭 내 무효화만 수행한다(§8.2). 대체 전송을 만들지 않는다 |
발신 탭의 무효화는 채널을 거치지 않고 직접 호출한다. #C2 가 보낸 객체를 수신 대상에서 제외하므로, echo 를 기다리는 구현은 발신 탭만 stale 로 남긴다.
대체 경로를 두지 않는 이유는 #C4 다 — BroadcastChannel 은 2022년 3월부터 모든 주요 브라우저에서 동작하며, 대체가 필요한 환경이 지원 대상에 있다는 근거는 조사에서 나오지 않았다. 유력 후보였던 storage event 경로는 #C10 대로 sessionStorage 에서 다른 탭에 도달하지 않아, 잘못 구현하면 조용히 아무 일도 하지 않는 경로가 된다. 평소 실행되지 않아 테스트로도 걸리지 않는 경로를 미리 만들어 두지 않고, 미동작 환경이 실제로 관측되면(CROSS_TAB_CHANNEL_UNAVAILABLE 계측) 그때 추가한다.
UNSUPPORTED_IMPL_DECISION — 부재 판정을 typeof 검사 + 생성 시도로 하는 것. #C4 는 Baseline 이라고만 말하고 부재를 어떻게 감지하는지는 말하지 않는다. trade-off: 생성까지 시도해야 차단 환경의 예외를 잡을 수 있어 객체 1개 비용을 감수했다.
2. 메시지 봉투
Trace: D2(key 만 전파) ←
mdn-broadcastchannel-storage-event.md#C5/ D4 ←#C2
봉투는 두 필드만 갖는다.
keys— 무효화할 query key 배열.FE-OC-012의 registry factory 가 만든 key 만 허용한다. 값·응답 본문·사용자 식별자를 넣지 않는다origin— 발신 컨텍스트 식별자. 수신부가 자기 발신을 걸러내는 2차 방어(1차는 D4 의 채널 자체 제외)
값을 싣지 않는 것은 기술 제약이 아니다. #C5 대로 structured clone 이라 객체를 그대로 보낼 수 있다. 금지하는 이유는 수신 탭이 자기 권한으로 얻지 않은 데이터를 갖게 되기 때문이며, 수신 탭은 key 를 받아 자기 QueryCachePort 로 다시 가져온다.
수신부는 방어적으로 판독한다. 봉투 파싱 실패는 던지지 않고 계측한 뒤 무시한다 — 무효화 신호 하나를 놓치는 것이 UI 를 죽이는 것보다 낫다.
UNSUPPORTED_IMPL_DECISION — origin 필드로 2차 방어를 두는 것. #C2 만으로 발신자 제외가 보장되므로 원칙적으로는 불필요하다. trade-off: 채널 객체가 실수로 2개 만들어졌을 때 중복 무효화를 막는 안전망이며, 문자열 하나의 비용으로 D4 위반을 런타임에 흡수한다.
3. CachePersistencePort — 파티션 키와 복원 순서
Trace: D1(3-tuple + 폐기) ←
tanstack-query-persistence-hydration-official.md#C1·#C2/ D6(렌더 이전 await) ←#C5/ D7(대상 선별·직렬화) ←#C6·#C7/ 상속DEC-…-SERVER-STATE-001@1
파티션 키는 <releaseId>:<configSchemaVersion>:<apiContractVersion> 단일 문자열로 만들어 스냅샷과 함께 저장한다. 복원 시 이 문자열이 정확히 일치하지 않으면 스냅샷을 폐기하고 빈 캐시로 시작한다. 부분 복원·필드 단위 migration 경로를 두지 않는 것은 #C2 가 채택 라이브러리의 기존 동작(removeClient() 후 즉시 폐기)임을 확인해 준다.
복원 순서는 bootstrap §4.5 의 캐시 복원 단계에서 await 한다. #C5 가 경고하는 경합(복원 중 query mount → fetch)이 정확히 이 순서를 어겼을 때 나타난다. 복원 실패는 부팅을 막지 않는다 — CACHE_PERSISTENCE_FAILURE 로 계측하고 메모리 캐시로 계속한다(§8.2 와 정합).
영속 대상은 FE-REG-QUERY.persistenceTier !== 'memory' 인 행 중 성공한 query 로 한정한다. #C6 의 기본 동작과 같은 방향이며, registry 를 상위 필터로 두어 "라이브러리 기본값이 바뀌면 우리 계약도 바뀌는" 결합을 끊는다. 직렬화는 adapter 책임이다(#C7).
UNSUPPORTED_IMPL_DECISION — 파티션 키를 : 구분 단일 문자열로 만드는 것. #C1 은 buster 가 문자열이라는 것만 말하고 구성·구분자를 말하지 않는다. trade-off: 구조화 객체 대신 문자열을 택해 비교를 동등성 1회로 끝냈다. 비용은 세 값 중 무엇이 불일치했는지 폐기 시점에 알 수 없는 것이며, 이는 telemetry 에 세 값을 따로 실어 보완한다.
4. 이 branch 가 남기지 않는 것 (R3)
- query 의 stale time·gc·refetch·invalidation 매핑 → raw/branch-notes/feature-server-state-caching-contract 소유(
FE-OC-012) QUERY_CACHE_SNAPSHOT의 physical key·namespace·classification·quota fallback →DELEG-FE-009로 raw/branch-notes/feature-frontend-storage-registry-contract 에 위임- 브라우저 eviction 자체에 대한 방어(
persist()) → raw/branch-notes/feature-frontend-binary-file-io-store-contractD7소유. 이 branch 의 캐시는 폐기돼도 correctness 를 잃지 않으므로 persist 를 요청하지 않는다
엣지·실패·의존
- 실패·엣지 경로
- 직렬화·영속·복원 실패 →
CACHE_PERSISTENCE_FAILURE, 메모리 캐시만 사용하고 제품 흐름을 막지 않음 (D6) - BroadcastChannel 생성 불가 →
CROSS_TAB_CHANNEL_UNAVAILABLE, 탭 내 무효화만 수행. 대체 전송을 시도하지 않는다 (D5) - 파티션 키 불일치 캐시 발견 → 복원하지 않고 폐기. 부분 복원 금지 (D1)
- 수신 탭이 무효화 key 를 받았으나 해당 query 를 구독하지 않음 → 무시 (에러 아님)
- 다중 탭이 동시에 같은 key 를 무효화 → 중복 refetch 를
QueryCachePort의 dedup 이 흡수해야 함 - 발신 탭이 자기 무효화를 놓침 → 채널이 발신자에게 echo 하지 않으므로(
mdn-broadcastchannel-storage-event#C2) 로컬 무효화는 직접 호출한다 (D4). echo 대기 구현은 이 경로에서 조용히 실패한다 - 채널 객체를 2개 이상 만든 경우 →
#C2의 제외 단위가 객체라서 같은 문서의 두 번째 객체가 자기 메시지를 수신해 무효화가 중복 실행된다. adapter 는 컨텍스트당 1개를 강제하고origin필드로 흡수한다 (D4) - 닫힌 채널에 발행 →
InvalidStateError(#C6). unmount 후 발행 경로가 남아 있다는 신호이므로 삼키지 않고 계측한다 - 봉투 JSON 파싱 실패 → 던지지 않고 계측 후 무시. 신호 1건 손실이 UI 중단보다 낫다
- 직렬화·영속·복원 실패 →
- 다른 계약 의존
- raw/branch-notes/feature-server-state-caching-contract
D?—QueryCachePort의 invalidation 매핑과 dedup 책임. 매핑이 바뀌면 전파 대상이 바뀌고, dedup 이 없으면 D2 의 refetch 폭주 위험이 이 branch 로 되돌아온다. 해당 branch 의 Decision ID 는/branch-spec미실행이라 아직 부여되지 않았다 — 확정 시 이 줄을 D-ID 로 갱신한다 - raw/branch-notes/feature-frontend-storage-registry-contract —
QUERY_CACHE_SNAPSHOT행의 physical key·classification·TTL·quotaFallback.DELEG-FE-009로 위임했고 D1·D7 이 그 행을 소비한다 - raw/branch-notes/feature-frontend-contract-compatibility-governance —
releaseId·configSchemaVersion·apiContractVersionversion tuple 의 정의. D1 의 파티션 키가 이 tuple 에서 나오므로 tuple 구성이 바뀌면 D1 도 바뀐다 - raw/branch-notes/feature-frontend-env-runtime-config-contract
D4—CAP_FE_CACHE_PERSISTENCE를 포함한 capability flag 의 해석 시점. flag 가 OFF 면 이 branch 의 adapter 는 번들에 없어야 한다(FE-GATE-033) - raw/branch-notes/feature-frontend-binary-file-io-store-contract
D7—navigator.storage.persist()요청 소유권. 이 branch 는 요청하지 않으며, 캐시가 브라우저 eviction 으로 사라져도 폐기와 같은 경로로 처리한다
- raw/branch-notes/feature-server-state-caching-contract
검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| version 불일치 캐시가 복원되지 않는다 | 부분 복원이 조용히 성공하기 쉬움 | negative fixture — 이전 version 스냅샷 주입 후 복원 시도가 폐기로 끝나는지 | planned |
| 탭 A mutation 이 탭 B 캐시를 무효화한다 | BroadcastChannel 은 같은 origin 의 다른 탭에서만 동작 | integration test — 두 컨텍스트에서 발행/수신 확인 | planned |
| BroadcastChannel 부재 시 탭 내 무효화만 하고 조용히 넘어가지 않는다 | 부재를 감지 못하면 다중 탭 stale 이 무음으로 남는다 | fixture — BroadcastChannel 을 undefined 로 만든 뒤 CROSS_TAB_CHANNEL_UNAVAILABLE 계측과 탭 내 무효화 동작 확인 |
planned |
| key 만 전파해도 UI 가 일관된다 | 수신 탭의 refetch 타이밍에 따라 잠깐 어긋날 수 있음 | integration test — 전파 후 두 탭의 최종 상태 일치 | needs-confirmation |
| 다중 탭 동시 무효화가 refetch 폭주를 만들지 않는다 | dedup 이 QueryCachePort 책임인지 이 branch 책임인지 경계가 얇음 |
부하 fixture — N개 탭 시뮬레이션 후 실제 요청 수 측정 | needs-confirmation |
| 지원 대상 브라우저에 BroadcastChannel 미동작 환경이 실제로 있는지 | #C4 는 2022-03 이후 모든 주요 브라우저에서 동작한다고 말하지만 지원 대상 목록(FE-Q-007)이 미확정이다. 있으면 CROSS-TAB-001@2 의 "대체 경로 없음" 을 되돌려야 한다 |
FE-Q-007 확정 후 대조 + CROSS_TAB_CHANNEL_UNAVAILABLE 발생률 관측 |
needs-confirmation |
| 발신 탭이 자기 무효화를 받지 못한다 | #C2 는 명시적이나 구현이 echo 를 기대하기 쉬움 |
negative fixture — 탭 A 발행 후 탭 A 의 수신 handler 가 호출되지 않는지 | planned |
| 세 version 중 무엇이 실제로 캐시를 무효화해야 하는지 | 세 값을 모두 넣은 것은 project-local 판단이며 과잉일 수 있음 | 배포 로그 대조 — 각 값이 단독으로 바뀐 배포에서 stale 캐시가 실제 문제를 냈는지 | needs-confirmation |
| 복원 await 가 첫 페인트를 유의미하게 늦추지 않는다 | #C5 는 경합만 말하고 비용은 말하지 않음 |
저사양 기기에서 복원 유/무 FCP 비교 | needs-confirmation |
Audit & Findings
/branch-spec2026-07-28 조사에서 발견한 상위 계약과 조사 결과의 불일치.FE-D028은 hub 소유이므로 이 branch 는 정합 권고만 남기고 자동 수정하지 않는다(hub §3.3).
| Finding ID | 대상 | 현재 서술 | 조사 결과 | 권고 | 처리 |
|---|---|---|---|---|---|
FALLBACK_JUSTIFICATION_GAP |
hub FE-D028 / DEC-…-CROSS-TAB-001@1 |
"BroadcastChannel 우선에 storage event fallback" — fallback 이 필요한 환경을 특정하지 않는다 |
mdn-broadcastchannel-storage-event#C4: "It's been available across browsers since March 2022" (Baseline Widely available). fallback 은 localStorage write·5 MiB 예산·연속 동일값 회피 장치를 추가로 요구한다 |
근거 없는 대체 경로를 유지하지 말고 삭제할 것. 필요해지면 그때 추가 | resolved 2026-07-28 — 사용자 결정으로 fallback 삭제. CROSS-TAB-001 revision 1→2(behavior-change), 이 노트의 D5·§구현 가이드·§엣지·TODO 동기화 |
STORAGE_EVENT_SCOPE_UNSPECIFIED |
hub FE-D028 및 §4.2 adapters/cross-tab |
"storage event fallback" 이라고만 적어 backend 를 명시하지 않는다 |
mdn-broadcastchannel-storage-event#C10: sessionStorage 의 storage event 는 "not other tabs" — 탭 간 신호로 동작하지 않는다 |
서술을 좁히거나, 위 finding 대로 경로 자체를 삭제할 것 | resolved 2026-07-28 — fallback 삭제로 해소. 고칠 대상 자체가 사라졌다 |
NO_GROUND_TRUTH |
/branch-spec §2 ca-tmpl 대조 |
명령은 /home/donghyeon/workspace/ca-tmpl 의 registry·코드와 대조하라고 요구 |
ca-tmpl 은 Gradle/Java 백엔드 전용. frontend 구현 repo 는 hub §0.3 대로 미식별 |
이 branch 의 모든 명세는 planned. actually-implemented 승격은 frontend repo 식별(FE-Q-001) 이후 |
open — FE-Q-001 선행 |
관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
2026-07-28
/branch-spec§8b. agent dispatch 없이 수기 판정했다(세션 제약). governing doc = raw/project-notes/ca-skeleton-frontend-operational-contract. 기준은 hub §2.2 의 10개 universal acceptance question.
| # | 관심사 | 판정 | 근거 |
|---|---|---|---|
| 1 | 문제와 실패 모드가 구체적인가 | covered-here | §목표 — 탭 간 stale 과 release 를 넘은 캐시 파싱 실패. §엣지에 10개 실패 경로 |
| 2 | 상속한 결정을 실제로 적용했는가 | covered-here | CROSS-TAB-001@2 → D2·D5, SERVER-STATE-001 → D1·D7, CAPABILITY-001 → CAP_FE_CACHE_PERSISTENCE 소유 |
| 3 | project-wide default 와 limit | covered-here | D1(3-tuple 폐기), D4(echo 없음), D5(대체 경로 없음), D6(렌더 이전 await), D7(성공 query 한정) |
| 4 | 대안을 검토했는가 | covered-here | §결정 사항 — releaseId 단독 / Web Locks 리더 / storage event 대체 경로(삭제 결정, §Audit). D3 는 반증까지 기록 |
| 5 | 금지 구현 | covered-here | §구현 가이드 2 — 값·응답 본문·사용자 식별자 전송 금지, factory 밖 key 금지. §구현 가이드 1 — 대체 전송 신설 금지 |
| 6 | 실패 경로가 error kind 로 매핑되는가 | covered-here | CACHE_PERSISTENCE_FAILURE·CROSS_TAB_CHANNEL_UNAVAILABLE (hub §8.2 와 정합) |
| 7 | 관측 가능한가 | should-fix | 파티션 폐기·fallback 진입·채널 부재를 구분할 telemetry event 를 FE-REG-TELEMETRY 에 등록하지 않았다. D1 의 "세 값 중 무엇이 불일치했는지" 보완도 여기에 걸린다 |
| 8 | 위임 경계가 명확한가 | covered-here | DELEG-FE-009 (storage registry), §구현 가이드 4 의 R3 목록 |
| 9 | 검증 수단이 있는가 | covered-here | §검증해야 할 주장 11행, FE-GATE-028 fixture + cache tier report |
| 10 | 말할 수 있는 범위 | covered-here | 전 항목 planned — NO_GROUND_TRUTH 로 구현 repo 미식별 |
판정: Covered (missing 0) · Should-fix 1건(관심사 7 — telemetry event 미등록). Blocking 아님.
마주친 문제
없음.
묶음 (이 branch에서 파생된 자료)
Sub-branches (세부 작업)
아직 없음.
오류 기록 (이 branch 작업 중 발생)
아직 없음.
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
아직 없음.
강의 (이 작업을 위해 학습한 강의)
아직 없음.
job-posting tie-ins (이 작업에서 파생된 글감)
아직 없음.
관련 일일 노트
- 아직 없음
완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- wiki 추출 대상 (verified만,
wiki/projects/로만 추출):actually-implemented항목: 없음locally-verified항목: 없음prod-verified항목: 없음
- 추출하지 않을 항목: 현재 전 항목
planned