Files
llm-wiki/raw/branch-notes/feature-frontend-binary-file-io-store-contract.md
T

355 lines
33 KiB
Markdown

---
title: branch / feature-frontend-binary-file-io-store-contract
source_type: branch-note
status: raw
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-028
kind: project-work-item
project: ca-skeleton-frontend-operational-contract
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-028
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CAPABILITY-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-BINARY-STORE-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REGISTRY-001@1]
refines: []
overrides: []
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-002, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-011]
imports: [FE-GATE-027@1, FE-OC-002@1, FE-OC-013@1, FE-OC-022@1]
delegates: []
accepts_delegations: [DELEG-FE-008@1]
contract_packet: 1
branch: feature-frontend-binary-file-io-store-contract
parent_branch:
related_projects: [ca-skeleton-frontend, ca-skeleton]
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
tags: [branch, ca-skeleton-frontend, storage, binary, file-io]
created: 2026-07-28
target_merge:
status_label: in-progress
---
# branch: feature-frontend-binary-file-io-store-contract
<!-- section-id: branch-parent -->
## 부모 (필수)
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
형제 branch (같은 부모, 이번 확장에서 함께 생성):
- [[raw/branch-notes/feature-frontend-cache-tier-cross-tab-invalidation-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`
- **완료 조건**: picker·다운로드·object URL 해제·quota·OPFS·Cache Storage fixture가 통과하고 binary I-O report가 생성된다
<!-- 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_BINARY_IO` 를 이 branch 가 소유하고 default OFF 로 유지한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-BINARY-STORE-001@1` | 로컬 바이너리 backend는 IndexedDB를 default로 하고 OPFS는 대용량 순차 write에 opt-in, Cache Storage는 service worker 호스팅 response cache 전용이다 | `BlobStorePort` adapter 의 backend 선택 순서에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REGISTRY-001@1` | route, API operation, env, storage, error, query, telemetry, release token, capability를 9개 registry로 관리한다 | `FE-REG-STORAGE` 신규 4행(`UPLOAD_PART_STATE`·`TRANSFER_OBJECT_BUFFER`·`QUERY_CACHE_SNAPSHOT`·`SW_RESPONSE_CACHE`)의 `payloadClass`·`evictionOrder` 를 소비한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
| D1 | object URL 의 생성과 해제는 `adapters/file` 이 쌍으로 소유하고 presentation 에 raw URL 문자열을 넘기지 않는다 | `local` | `raw/official-docs/mdn-object-url-cache-storage.md#C1`, `#C2` | `proposed` |
| D2 | `BlobStorePort` 는 backend 를 호출자에게 노출하지 않고 registry 의 `backend` 값으로만 선택한다 | `refines DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-BINARY-STORE-001@1` | **`UNSUPPORTED_DECISION`** — 조사 후에도 근거 없음. trade-off: backend 를 노출하면 호출부가 IndexedDB/OPFS 를 직접 분기해 `FE-OC-002` 의 port 경계가 새므로 은닉을 택했다. 비용은 backend 별 최적화 불가 | `proposed` |
| D3 | `FileDialogPort`**기준선은 `<input type=file>` + `<a download>`** 이고, File System Access picker 는 feature detection 후 얹는 progressive enhancement 다 | `local` | `raw/official-docs/mdn-file-system-access-opfs.md#C1`, `#C2`, `#C4` | `proposed` |
| D4 | picker 취소와 "user agent 가 위험하다고 판단한 파일"은 같은 `AbortError` 로 오므로 **구분하지 않고** 둘 다 `FILE_PICKER_DISMISSED` 로 정규화한다 | `local` | `raw/official-docs/mdn-file-system-access-opfs.md#C3` | `proposed` |
| D5 | OPFS 는 **비동기 API 를 기본**으로 쓰고, 동기 access handle 이 필요한 경우에만 `WorkerTaskPort` 를 경유한다 | `refines DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-BINARY-STORE-001@1` | `raw/official-docs/mdn-file-system-access-opfs.md#C5`, `#C6` | `proposed` |
| D6 | `FE-REG-STORAGE.evictionOrder`**애플리케이션 주도 정리** 순서이며 브라우저 eviction 에는 적용되지 않는다 | `local` | `raw/official-docs/mdn-storage-quotas-eviction-persistence.md#C1`, `#C3` | `proposed` |
| D7 | correctness 값을 쓰는 capability 는 boot 시 `navigator.storage.persist()` 를 1회 요청하고, **미허가 상태를 실패로 취급하지 않되 재개 보장 없음을 표면화**한다 | `local` | `raw/official-docs/mdn-storage-quotas-eviction-persistence.md#C4`, `#C5`, `#C7` | `proposed` |
`D8`(IndexedDB 스키마·버전 관리와 migration 절차)은 이번 회차 자동조사 상한(6개)을 넘어 **deferred** 다. 다음 `/branch-spec` 회차 또는 수동 조사 대상이며, 그 전까지 §구현 가이드에 IndexedDB 버전 전략을 적지 않는다.
<!-- section-id: declared-overrides -->
### 선언한 예외
해당 없음. inherited decision 과 다른 동작을 요구하지 않는다.
<!-- 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-027@1` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | picker·다운로드·quota·object URL 해제 fixture 가 실패하면 merge 를 MUST 차단 | 이 branch 가 owner 로서 fixture 와 report 를 산출 |
| `FE-OC-002@1` | [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] | `domain <- application <- presentation` 의존 방향과 application-owned output port를 MUST 지킴 | port 는 application 소유, adapter 가 구현 |
| `FE-OC-013@1` | [[raw/branch-notes/feature-frontend-storage-registry-contract]] | storage key는 namespace·version·classification을 MUST 가지며 token/secret 저장을 금지 | 바이너리 key 도 예외 없이 registry 경유 |
| `FE-OC-022@1` | [[raw/branch-notes/feature-frontend-contract-registry-governance]] | 9개 registry는 single primary owner와 compatibility impact를 MUST 기록 | `FE-REG-STORAGE` 확장의 compatibility impact 기록 |
<!-- GENERATED: project-contract-imports:end -->
<!-- GENERATED: received-delegations:start -->
### 수신한 위임
| Delegation Ref | From | Concern | Status |
|---|---|---|---|
| `DELEG-FE-008@1` | [[raw/branch-notes/feature-frontend-large-object-transfer-contract]] | `fe.deleg.binary-handle-ownership` | accepted |
<!-- GENERATED: received-delegations:end -->
<!-- GENERATED: flow:start -->
### 가져온 흐름 단계
| Stage Ref | Order | Owner | Input | Action | Output |
|---|---:|---|---|---|---|
<!-- GENERATED: flow:end -->
<!-- section-id: branch-goal -->
## 목표
`FileDialogPort``BlobStorePort` 를 정의하고, IndexedDB·OPFS·Cache Storage adapter 와 object URL 수명 계약을 고정한다. 이 계약이 없으면 컴포넌트가 `URL.createObjectURL` 을 직접 부르고 `revokeObjectURL` 을 빠뜨려 탭 수명 동안 메모리가 증가하며, quota 초과 시 correctness 값이 조용히 memory 로 fallback 된다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- `FileDialogPort` — 파일 선택(`<input type=file>` / File System Access) 과 저장 dialog
- `BlobStorePort` — IndexedDB(default) · OPFS(opt-in) · Cache Storage(SW 전용) backend
- object URL 생성·해제 쌍 관리와 누수 검출 fixture
- quota 매핑(`BLOB_STORE_QUOTA_EXCEEDED`)과 `evictionOrder` 기반 제거 순서
- `FE-REG-STORAGE` 신규 4행의 소비와 `payloadClass`·`evictionOrder` 강제
- `CAP_FE_BINARY_IO` capability 행 소유
### 제외 범위
- **use case, domain model, business rule** — 이 branch 는 port 와 adapter 계약까지만 정의한다. port 를 호출하는 use case 는 적용 프로젝트가 작성한다.
- 파일 형식별 처리 — 이미지 리사이즈·비디오 트랜스코딩·문서 파싱
- 실제 네트워크 전송 — `[[raw/branch-notes/feature-frontend-large-object-transfer-contract]]` 소유
- storage physical key·namespace·classification 정의 — `[[raw/branch-notes/feature-frontend-storage-registry-contract]]` 소유
- service worker 등록·수명주기 — `[[raw/branch-notes/feature-frontend-background-execution-worker-contract]]` 소유 (Cache Storage 는 그 위에 얹힌다)
## 근거 (필수, 최소 1개+)
| Source | 정당화하는 결정 |
|---|---|
| [[raw/official-docs/mdn-file-system-access-opfs]] | D3 picker 기준선 선택 · D4 취소 매핑 · D5 OPFS worker 제약 |
| [[raw/official-docs/mdn-storage-quotas-eviction-persistence]] | D6 `evictionOrder` 적용 범위 · D7 correctness 값의 persist 요청 |
| [[raw/official-docs/mdn-object-url-cache-storage]] | D1 object URL 수명 소유 · Cache Storage 실행 컨텍스트 전제 |
| `[[docs/superpowers/specs/2026-07-28-ca-skeleton-frontend-runtime-adapter-features-design]]` §5.1·§6.2 | port 12개 분해와 `FE-REG-STORAGE` 확장 |
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.4·§5.5·§5.11 | 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`)
<!-- section-id: decision-evidence -->
## 결정-근거 매핑
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | object URL 생성·해제를 `adapters/file` 이 쌍으로 소유 | 항상. 컴포넌트가 URL 문자열을 장기 보유해야 하는 요구(장기 미리보기)가 생기면 handle 기반 대여 API 로 재검토 | `mdn-object-url-cache-storage.md#C1`(해제는 명시 호출 필요), `#C2`(SW 미제공 사유가 memory leak) | `official-reference` + project-decision(소유 주체는 MDN 이 말하지 않음) | 해제 시점을 adapter 가 알 수 없는 사용 패턴. 대여 만료 타이머가 필요할 수 있음 |
| D2 | `BlobStorePort` 가 backend 를 노출하지 않음 | 항상. OPFS 전용 최적화가 use case 레벨에서 필요해지면 재검토 | **`UNSUPPORTED_DECISION`** — 조사 후에도 외부 근거 없음 | `project-local default` (라벨됨) | backend 별 성능 특성이 크게 다르면 추상화가 새는 지점이 생김 |
| D3 | `<input type=file>` + `<a download>` 를 기준선, File System Access picker 를 progressive enhancement | 항상. 대상 브라우저 매트릭스가 picker 전량 지원으로 확정되면(`FE-Q-007`) 기준선을 picker 로 올릴 수 있음 | `mdn-file-system-access-opfs.md#C1`(not Baseline), `#C2`(transient activation), `#C4`(secure context) | `official-reference` | 두 경로의 UX 가 달라진다 — picker 는 저장 위치 선택, fallback 은 브라우저 다운로드 폴더 고정 |
| D4 | 취소와 "위험 파일 거부"를 구분하지 않고 `FILE_PICKER_DISMISSED` 로 정규화 | 항상. 브라우저가 두 사유를 구분 가능한 신호로 분리하면 재검토 | `mdn-file-system-access-opfs.md#C3`(둘 다 같은 `AbortError`) | `official-reference` | 거부 사유를 사용자에게 설명할 수 없다. "선택된 파일이 없습니다" 수준의 중립 문구만 가능 |
| D5 | OPFS 는 비동기 API 기본, 동기 handle 이 필요할 때만 `WorkerTaskPort` 경유 | 대용량 순차 write 로 메인 스레드 블로킹이 측정될 때만 동기 경로. 그 전에는 비동기 | `mdn-file-system-access-opfs.md#C5`(동기 API 는 worker 전용), `#C6`(속도 우위의 비교 대상은 File System Access API) | `official-reference` | worker 경유는 `CAP_FE_BACKGROUND_EXEC` 활성을 전제한다 — 두 capability 가 얽힌다 |
| D6 | `evictionOrder` 는 애플리케이션 주도 정리 순서로만 유효 | 항상. 브라우저가 per-key eviction 힌트 API 를 제공하면 재검토 | `mdn-storage-quotas-eviction-persistence.md#C1`(origin 전량 삭제), `#C3`(LRU origin 단위) | `official-reference` | 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일 만에 무효가 된다. 제품이 이를 수용 가능한지 미확인 |
<!-- section-id: implementation -->
## 구현 가이드
> 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 경로에서 이름이 거짓이 되므로 중립 동사를 택했다.
| 항목 | planned 명세 | 근거 |
|---|---|---|
| 경로 | `src/adapters/file/file-dialog.adapter.js` | — (`UNSUPPORTED_IMPL_DECISION`, hub §4.6 blueprint 관례를 따름) |
| detection | `typeof window.showOpenFilePicker === 'function'` **그리고** `window.isSecureContext` 가 모두 참일 때만 picker 경로 | `#C1`, `#C4` |
| 열기 — enhanced | `showOpenFilePicker(...)``FileSystemFileHandle[]` | `#C1` |
| 열기 — 기준선 | 숨긴 `<input type="file">` 을 클릭 이벤트 핸들러 안에서 트리거 → `FileList` | `#C2`(제스처 필요) |
| 저장 — enhanced | `showSaveFilePicker(...)``FileSystemFileHandle``createWritable()` | `#C1` |
| 저장 — 기준선 | object URL + `<a download>` 클릭. **덮어쓰기 불가**이며 저장 위치를 사용자가 고를 수 없음을 UI 문구에서 약속하지 않음 | `#C1` (fallback 한계) |
| 호출 컨텍스트 | 두 경로 모두 **사용자 제스처 핸들러 안에서 동기적으로** 호출한다. `await` 이후 호출하면 activation 이 소모돼 실패한다 | `#C2` |
| 취소 | `AbortError``FILE_PICKER_DISMISSED`. **사유를 구분하지 않는다** | `#C3`, D4 |
| 제약 위반 | accept/size 검사는 adapter 가 수행 → `FILE_REJECTED`. 파일명은 telemetry 로 보내지 않음 | hub §5.8 forbiddenAttributes |
### 2. object URL 수명 — 대여(lease) 모델
> **Trace**: D1 ← `mdn-object-url-cache-storage.md#C1`(해제는 명시 호출)·`#C2`(SW 미제공 사유 = memory leak)
>
> - **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 용 카운터**로 노출 |
| service worker | object URL 을 만들지 않는다 — 플랫폼이 제공하지 않는다 (`#C2`) |
| 누수 판정 | adapter 파괴 시점의 미반납 대여 수 > 0 이면 `FE-GATE-027` 실패 |
### 3. `BlobStorePort` — backend 선택과 quota 처리
> **Trace**: D2(backend 비노출) ← 근거 없음(project-local) / D5(OPFS 비동기 기본) ← `mdn-file-system-access-opfs.md#C5`·`#C6` / D6(evictionOrder 범위) ← `mdn-storage-quotas-eviction-persistence.md#C1`·`#C3` / D7(persist) ← 같은 문서 `#C4`·`#C5`·`#C7`
>
> - **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 경계 |
<!-- section-id: edge-failure-dependency -->
## 엣지·실패·의존
- **실패·엣지 경로**
- 사용자가 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` 행은 제거 대상이 아니다
- OPFS 미지원 브라우저 → `CAPABILITY_UNSUPPORTED`, `disabledFallback: feature-hidden`
- **다른 계약 의존** (대상 브랜치 + 그 Decision ID)
- [[raw/branch-notes/feature-frontend-storage-registry-contract]] `D1`(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-contract]] `D1`(총함수 정규화)·`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-contract]] `D3`(worker/SW 엔트리의 상위 layer import 금지) — **D5 의 OPFS 동기 경로**가 `WorkerTaskPort` 를 요구하므로, worker 엔트리 규칙이 본 branch 의 OPFS 구현 형태를 제약한다. Cache Storage backend 의 SW 호스팅은 `D1`(자동 `skipWaiting` 금지)·`D2`(`serviceWorkerVersion`)와 파티션 시점을 공유한다.
- [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] `D4`(React mount 전 runtime config fetch·검증) — **D7 의 `persist()` 요청 시점**이 boot 순서(hub §4.5 6단계 capability 해석) 안에 있어야 한다. `D4` 의 mount-전 게이트가 바뀌면 persist 요청 위치도 바뀐다.
- [[raw/branch-notes/feature-frontend-large-object-transfer-contract]] `D1`(별도 credential-less transport) — `DELEG-FE-008@1` 로 본 branch 가 `File`/`Blob` handle 과 object URL 수명을 **수신**했다. 그 branch 의 전송 descriptor 형태가 바뀌면 본 branch 의 대여 handle 반환 형태가 영향받는다.
<!-- section-id: claims-to-verify -->
## 검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| picker 취소가 error surface 를 띄우지 않는다 | 취소를 실패로 처리하는 구현이 흔함 | component test — dialog 취소 후 error role 요소 0개 | `planned` |
| 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` 를 넘을 수 없다".
| 관심사 (hub §2.2) | 상태 | owner | 심각도 | 근거 |
|---|---|---|---|---|
| 1. 이 contract 가 막는 concrete failure | covered-here | — | — | §목표 — object URL 누수, quota 초과 시 correctness 값의 조용한 memory fallback |
| 2. input 과 output | covered-here | — | — | §구현 가이드 1(선택 제약 → descriptor)·3(등록 key + 바이너리 descriptor → 저장/조회/삭제 결과) |
| 3. project-wide default 와 limit | covered-here | — | — | D3(기준선 `<input>`), D5(비동기 OPFS 기본), D6(evictionOrder 적용 범위), D7(persist 1회) |
| 4. 허용되는 예외와 승인 owner | covered-here | — | — | §선언한 예외 "해당 없음" + D3·D5 의 선택 조건(브라우저 매트릭스 확정 / 블로킹 측정 시) |
| 5. 금지 구현 | covered-here | — | — | §구현 가이드 2 — raw object URL 문자열 단독 반환 금지, SW 에서 object URL 생성 금지, `evictionOrder: null` 행 정리 금지 |
| 6. failure 가 어떤 normalized error·UX 로 나타나는가 | delegated | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] `D1`·`D3`·`D6` | OK | 본 branch 는 4개 kind 를 *생산*하고 정규화 총함수·UX 어휘는 error-classification 소유 |
| 7. 어떤 telemetry 가 남고 무엇이 redacted 되는가 | covered-here | — | Should-fix | §구현 가이드 1 이 "파일명은 telemetry 로 보내지 않음" 을 명시하나, `FE-REG-TELEMETRY` 에 이 branch 전용 event 를 등록하지 않았다 — quota/eviction 관측 event 부재 |
| 8. 어떤 test 가 위반 시 실패하는가 | covered-here | — | — | §검증해야 할 주장 9행 + `FE-GATE-027` fixture 5종 |
| 9. 어떤 evidence artifact 가 생성되는가 | covered-here | — | — | hub §15.1 `FE-GATE-027``binary I-O report` |
| 10. release·rollback 에 미치는 영향 | delegated | [[raw/branch-notes/feature-frontend-background-execution-worker-contract]] `D2` | OK | Cache Storage 파티션이 release 토큰(`serviceWorkerVersion`)에 묶이며, rollback 시 되돌림 판정은 release branch 소유(`DELEG-FE-011@1`) |
**판정: Covered (missing 0)**`🔴 missing` 0건, `Should-fix` 1건(관심사 7 telemetry event 미등록).
## 마주친 문제
없음.
## 묶음 (이 branch에서 파생된 자료)
### Sub-branches (세부 작업)
아직 없음.
### 오류 기록 (이 branch 작업 중 발생)
아직 없음.
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
아직 없음.
### 강의 (이 작업을 위해 학습한 강의)
아직 없음.
### job-posting tie-ins (이 작업에서 파생된 글감)
아직 없음.
## 관련 일일 노트
- 아직 없음
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목: 없음
- `locally-verified` 항목: 없음
- `prod-verified` 항목: 없음
- **추출하지 않을 항목**: 현재 전 항목 `planned`