FileDialogPort 와 BlobStorePort 를 정의하고, IndexedDB·OPFS·Cache Storage adapter 와 object URL 수명 계약을 고정한다. 이 계약이 없으면 컴포넌트가 URL.createObjectURL 을 직접 부르고 revokeObjectURL 을 빠뜨려 탭 수명 동안 메모리가 증가하며, quota 초과 시 correctness 값이 조용히 memory 로 fallback 된다.
이슈:
PR:
범위
포함 범위
FileDialogPort — 파일 선택(<input type=file> / File System Access) 과 저장 dialog
port ownership, storage registry, capability registry
근거 등급 경계 (2026-07-28 조사 후 갱신): 2026-07-28 /branch-spec 조사로 MDN 근거 3건을 수집해 D1·D3~D7 은 official-reference 근거를 갖게 됐다. 남은 경계는 두 가지다.
D2 는 여전히 project-local — port 가 backend 를 노출하지 않는다는 추상화 선택은 외부 문서가 다루는 주제가 아니다.
FE-D027 의 backend 우선순위(IndexedDB default / OPFS opt-in)는 성능 근거가 없다 — mdn-file-system-access-opfs#C6 이 말하는 OPFS 의 속도 우위는 File System Access API 대비이지 IndexedDB 대비가 아니다. IndexedDB↔OPFS 비교는 자체 벤치마크가 필요하며 ## 검증해야 할 주장 에 남긴다.
ca-tmpl ground truth 는 이 branch 에 적용되지 않는다(NO_GROUND_TRUTH) — §Audit & Findings 참조.
TODO
FileDialogPort 인터페이스 확정 — 등급: planned
BlobStorePort 인터페이스와 backend 선택 규칙 확정 — 등급: planned
object URL 수명 계약과 누수 fixture — 등급: planned
quota 초과 시 evictionOrder 동작과 null 행 보호 fixture — 등급: planned
OPFS 순차 write fixture — 등급: planned
Cache Storage 버전 파티션 fixture — 등급: planned
FE-GATE-027 binary I-O report 산출 — 등급: planned
진행 중 메모
FE-REG-STORAGE 의 UPLOAD_PART_STATE 는 quotaFallback: 없음, evictionOrder: null 이다. 이 두 값은 §9.4 의 "correctness 에 영향을 주는 값은 storage fallback 을 임의 적용하지 않는다" 를 행 단위로 구현한 것이므로, adapter 가 이 행에 대해 memory fallback 을 하면 gate negative fixture 가 잡아야 한다.
결정 사항
2026-07-28: object URL 생성·해제를 adapter 가 쌍으로 소유 / 이유: 해제 누락이 컴포넌트 단위로 흩어지면 검출이 불가능 / 검토한 대안: 컴포넌트 훅에서 useEffect cleanup 으로 관리 / 근거: 근거 raw 미수집, project-local 판단 (FE-Q-011)
결정-근거 매핑
Decision ID
Decision
선택 조건 (언제 이 결정 / 언제 대안)
Supporting Claims
Evidence Strength
Open Risk
D1
object URL 생성·해제를 adapters/file 이 쌍으로 소유
항상. 컴포넌트가 URL 문자열을 장기 보유해야 하는 요구(장기 미리보기)가 생기면 handle 기반 대여 API 로 재검토
hub §5.5 의 필드 설명이 이 경계를 담고 있지 않다 — §Audit & Findings EVICTION_SCOPE_DRIFT
D7
correctness 값 사용 시 persist() 1회 요청 + 미허가를 표면화
correctness 값(UPLOAD_PART_STATE 등)을 쓰는 capability 가 활성일 때. 순수 캐시만 쓰면 요청하지 않음
mdn-storage-quotas-eviction-persistence.md#C4(persist 는 LRU 제외), #C5(허가는 브라우저 재량), #C7(Safari 7일 규칙)
official-reference
persist 미허가 + Safari ITP 조합이면 재개 가능 전송이 7일 만에 무효가 된다. 제품이 이를 수용 가능한지 미확인
구현 가이드
2026-07-28 /branch-spec 조사(MDN 3건)로 in-scope detail 을 채웠다. 근거가 원칙만 지지하고 detail 은 지지하지 않는 cell 은 UNSUPPORTED_IMPL_DECISION + trade-off 한 줄로 표시했다(CLAUDE.md §15.5 R2). 다른 branch 결정 영역은 남기지 않았다(R3).
구현 repository 가 아직 없으므로 아래 경로·명명은 전부 planned 다. actually-implemented 로 승격하려면 repo·commit·path 가 필요하다(hub §17.2).
1. FileDialogPort — 2경로 구조와 feature detection
Trace: D3(기준선 선택) ← mdn-file-system-access-opfs.md#C1·#C2·#C4 / D4(취소 정규화) ← 같은 문서 #C3
UNSUPPORTED_IMPL_DECISION: port 메서드 이름(pickFiles/saveFile)과 descriptor 필드명. MDN 은 API 표면만 말하고 우리 port 의 명명을 지지하지 않는다. trade-off: 브라우저 API 명(showOpenFilePicker)을 그대로 쓰면 fallback 경로에서 이름이 거짓이 되므로 중립 동사를 택했다.
UNSUPPORTED_IMPL_DECISION: "대여 handle" 이라는 형태 자체. MDN 은 해제 필요성만 말하고 소유·반납 모델을 말하지 않는다. trade-off: 컴포넌트가 URL 문자열을 들고 있으면 해제 시점을 adapter 가 알 수 없어, 반납 가능한 handle 로 감쌌다.
규칙
planned 동작
발급
adapter 가 createObjectURL 호출 후 { url, release() } 형태의 대여 handle 을 반환. raw 문자열 단독 반환 금지
반납
release() 가 revokeObjectURL 을 1회만 호출(멱등). 이중 호출은 no-op
adapter 파괴
미반납 대여를 전부 revoke 하고, 그 수를 WORKER/FILE 계열이 아닌 gate fixture 용 카운터로 노출
UNSUPPORTED_IMPL_DECISION: IndexedDB 를 default 로, OPFS 를 opt-in 으로 두는 우선순위. #C6 의 OPFS 속도 우위는 File System Access API 대비이지 IndexedDB 대비가 아니다. trade-off: IndexedDB 가 지원 범위가 넓고 구조화 값과 바이너리를 한 backend 로 다룰 수 있어 기준선으로 두었다. 벤치마크 전까지 이 순서는 측정 근거가 없다.
항목
planned 명세
근거
backend 선택
호출자는 registry backend 값만 지정. adapter 가 구현체를 고른다
D2
OPFS 접근
비동기 File System API 기본. createSyncAccessHandle() 은 worker 안에서만 가능하므로 WorkerTaskPort 경유
#C5
quota 초과
evictionOrder 오름차순으로 애플리케이션이 정리 후 1회 재시도. evictionOrder: null 행은 정리 대상에서 제외
D6
브라우저 eviction
막을 수 없다. origin 전량 삭제이므로 부분 복구 로직을 두지 않는다
#C1, #C3
persist 요청
CAP_FE_BINARY_IO 활성 boot 시 navigator.storage.persist() 1회. 결과를 capability 상태에 기록
#C4, #C5
persist 미허가
실패가 아니다. 기능은 그대로 동작하되 correctness 값 사용 화면에 "재개가 보장되지 않음" 을 표시
#C5, D7
Safari 경고
ITP 활성 + 7일 무상호작용이면 script 생성 데이터가 삭제된다. 재개 가능 전송의 상한을 이 값 이하로 잡는다
#C7
4. Cache Storage backend
Trace: mdn-object-url-cache-storage.md#C3(SW 전용 아님)·#C4(버전 캐시명)·#C5(keys()/delete())
UNSUPPORTED_IMPL_DECISION: 캐시명 형식 <app>-<releaseId>. MDN 예제는 `myapp-${cacheVersion}` 패턴만 보이고 우리 토큰 구성을 지지하지 않는다. trade-off: FE-REG-RELEASE.releaseId 를 재사용하면 rollback 시 파티션이 자동으로 갈라진다.
항목
planned 명세
근거
접근
Window.caches 로도 접근 가능하다 — service worker 는 기술적 필수 조건이 아니다
#C3
정책
그럼에도 이 skeleton 은 Cache Storage 를 SW 호스팅 response cache 로 한정한다. 이는 플랫폼 제약이 아니라 FE-D027 의 정책 선택이다
§Audit & Findings
파티션
캐시명에 release 토큰 포함. 활성 release 외의 캐시는 keys() 열거 후 delete()
#C4, #C5
정리 시점
SW activate 단계. 이 branch 는 파티션 규칙만 소유하고 실행 시점은 feature-frontend-background-execution-worker-contract 소유
R3 경계
엣지·실패·의존
실패·엣지 경로
사용자가 dialog 를 닫음 → FILE_PICKER_DISMISSED, error surface 없음, telemetry event 없음
accept/size 제약 위반 → FILE_REJECTED, 위반 제약만 안내하고 파일명은 telemetry 에 남기지 않음
private mode 등으로 storage 접근 불가 → BLOB_STORE_UNAVAILABLE, registry quotaFallback 적용, 없음 행은 terminal
quota 초과 → BLOB_STORE_QUOTA_EXCEEDED, evictionOrder 순 제거 후 재시도. evictionOrder: null 행은 제거 대상이 아니다
raw/branch-notes/feature-frontend-storage-registry-contractD1(versioned namespace)·D2(classification 필수)·D5(quotaFallback 을 registry field 로 관리)·D6(token·secret·PII 저장 거부) — 본 branch 는 FE-REG-STORAGE 신규 4행을 소비만 하고 key 형식·classification 을 정의하지 않는다. D5 가 바뀌면 본 branch 의 quota 처리(§구현 가이드 3)가 함께 바뀐다.
raw/branch-notes/feature-frontend-error-classification-boundary-contractD1(총함수 정규화)·D2(safe field 만 보존)·D3(FE-REG-ERROR single-owner)·D6(recovery action closed vocabulary) — 본 branch 가 만드는 4개 kind(FILE_PICKER_DISMISSED·FILE_REJECTED·BLOB_STORE_UNAVAILABLE·BLOB_STORE_QUOTA_EXCEEDED)는 D1 의 총함수에 매핑되어야 하고, D6 의 닫힌 action 어휘 밖의 action 을 만들 수 없다.
raw/branch-notes/feature-frontend-background-execution-worker-contractD3(worker/SW 엔트리의 상위 layer import 금지) — D5 의 OPFS 동기 경로가 WorkerTaskPort 를 요구하므로, worker 엔트리 규칙이 본 branch 의 OPFS 구현 형태를 제약한다. Cache Storage backend 의 SW 호스팅은 D1(자동 skipWaiting 금지)·D2(serviceWorkerVersion)와 파티션 시점을 공유한다.
quota 초과 시 UPLOAD_PART_STATE 가 memory 로 fallback 되지 않는다
fallback 이 기본 동작으로 새기 쉬움
negative fixture — quota 초과 강제 후 해당 key 의 fallback 시도가 예외로 거부되는지
planned
object URL 이 해제된다
브라우저가 누수를 조용히 허용
unit test — 대여/반납 카운트 일치 + adapter 파괴 후 미반납 0
planned
OPFS 순차 write 가 IndexedDB 보다 대용량에서 유리하다
mdn-file-system-access-opfs#C6 의 속도 우위는 File System Access API 대비이지 IndexedDB 대비가 아니다. FE-D027 의 backend 우선순위는 이 점에서 측정 근거가 없다
벤치마크 — 동일 크기 순차 write 지연을 IndexedDB·OPFS 비동기·OPFS 동기(worker) 3경로로 비교. 결과를 FE-D027 revisit trigger 에 연결
needs-confirmation
Cache Storage 버전 파티션이 release 간 오염을 막는다
SW 수명주기와 얽혀 있음
integration test — 이전 release 파티션이 새 release 에서 조회되지 않는지
planned
navigator.storage.persist() 가 대상 브라우저에서 실제로 허가된다
#C5 가 "browser may or may not honor" 로만 말한다. 허가 조건은 브라우저별 비공개 휴리스틱
대상 브라우저 매트릭스(FE-Q-007)에서 persist() 반환값 실측 + persisted() 재확인
needs-confirmation
재개 가능 전송이 Safari ITP 7일 규칙을 견딘다
#C7 에 따르면 사용자 상호작용 없이 7일 지나면 script 생성 데이터가 삭제된다. persist 미허가 시 방어 수단이 없다
Safari + ITP 환경에서 7일 경과 시나리오 재현 또는 제품이 7일 상한을 수용하는지 결정
needs-confirmation
File System Access picker 가 대상 브라우저에서 동작한다
#C1 이 "not Baseline… does not work in some of the most widely-used browsers" 로만 말하고 구체 목록을 주지 않는다
FE-Q-007 브라우저 매트릭스 확정 후 FE-NFR-C02 크로스브라우저 fixture 에서 두 경로 모두 실행
needs-confirmation
사용자 제스처 핸들러에서 await 이후 picker 를 호출하면 실패한다
#C2 는 transient activation 요구만 말하고 소모 시점을 명시하지 않는다
fixture — await 뒤 picker 호출이 거부되는지 확인. 거부되면 adapter 의 동기 호출 규칙이 필수임이 확정됨
needs-confirmation
Audit & Findings
/branch-spec 2026-07-28 조사에서 발견한 상위 계약의 사실 오류. 이 branch 가 자동 수정하지 않고 정합 권고만 남긴다 — FE-D027 과 FE-REG-STORAGE 는 hub 소유이며 변경은 hub §3.3 프로토콜을 따라야 한다.
Finding ID
대상
현재 서술
조사 결과
권고
EVICTION_SCOPE_DRIFT
hub §5.5 FE-REG-STORAGE.evictionOrder
"quota 압박 시 제거 순서(정수, 낮을수록 먼저)" — 주체가 명시되지 않아 브라우저 eviction 에도 적용되는 것처럼 읽힌다
mdn-storage-quotas-eviction-persistence#C1: "When an origin's data is evicted by the browser, all of its data, not parts of it, is deleted at the same time." #C3: LRU 는 origin 단위
필드 Rule 에 "애플리케이션 주도 정리 순서. 브라우저 eviction 은 origin 전량 삭제이므로 이 순서가 적용되지 않는다" 를 추가
CACHE_STORAGE_CONSTRAINT_DRIFT
hub FE-D027 / DEC-…-BINARY-STORE-001@1
"Cache Storage는 service worker 호스팅 response cache 전용이다" — 기술 제약처럼 읽힌다
mdn-object-url-cache-storage#C3: "you're not limited to only using it with service workers", Window.caches 로 접근 가능
결정 자체는 유효(정책 선택). §5.5 또는 FE-D027 rationale 에 "플랫폼 제약이 아니라 release coherence 를 위한 정책 선택" 임을 명시
NO_GROUND_TRUTH
/branch-spec §2 ca-tmpl 대조
명령은 /home/donghyeon/workspace/ca-tmpl 의 registry·코드와 대조하라고 요구
ca-tmpl 은 Gradle/Java 백엔드 전용(domain-core·adapter-web·shared-contract). package.json·vite.config·.jsx 부재. frontend 구현 repo 는 hub §0.3 대로 미식별
이 branch 의 모든 명세는 planned. actually-implemented 승격은 frontend repo 식별(FE-Q-001) 이후
CAPABILITY_NAME_COLLISION
ca-tmpl/docs/registries/capabilities.yaml vs hub FE-REG-CAPABILITY
두 registry 가 같은 "capability" 어휘를 쓴다
ca-tmpl 은 use case → infrastructure 접근 권한(READ_REPOSITORY 등), hub 는 브라우저 런타임 기능 flag(CAP_FE_BINARY_IO 등). 다른 개념
계약을 상호 참조하지 않는다. 혼동 방지를 위해 frontend 쪽은 CAP_FE_ prefix 를 유지
관심사 커버리지 (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 — "하나라도 비어 있으면 branch 는 documented-only 를 넘을 수 없다".