Files
clean-architecture-frontend…/docs/architecture/optional-adapter-recipes.md
T

266 lines
18 KiB
Markdown

# Optional frontend adapter recipes
이 문서는 도메인과 무관한 선택형 frontend capability를 실제 프로젝트에
도입하는 실행 가이드다. 기본 스켈레톤에는 vendor runtime을 설치하지 않는다.
`RECIPE_AVAILABLE`은 catalog에 복사해 좁힐 recipe가 있다는 availability
표시다. runtime 구현이나 제품 선택·조립 상태가 아니다. 현재 catalog의 product
selection과 production composition은 별도로 `NOT_SELECTED`/미설치다.
일부 browser-native capability에는 dependency 없는
`referenceRuntime.status=AVAILABLE_NOT_COMPOSED` 구현이 함께 있지만, 이것도
제품 dataset·owner·policy가 정해져 bootstrap에 연결되기 전에는 설치된 기능이나
production readiness를 뜻하지 않는다.
## 1. 현재 상태와 파일 지도
| 항목 | 경로 | production 포함 |
| --- | --- | --- |
| 선택/금지/예산 SSOT | `config/recipes/frontend-capability-recipes.json` | 정책만 |
| catalog JSON schema | `schemas/config/frontend-capability-recipes.schema.json` | 아니오 |
| TypeScript port | `recipes/frontend-capabilities/contracts.ts` | 아니오 |
| fake/unavailable | `recipes/frontend-capabilities/fake-adapters.ts` | 아니오 |
| contract test | `tests/recipes/optional-capability-contracts.test.ts` | 아니오 |
| file/IndexedDB/OPFS/Cache 심층 계약 | `recipes/frontend-capabilities/browser-file-storage-contracts.ts` | 아니오 |
| 심층 deterministic fake | `recipes/frontend-capabilities/browser-file-storage-fakes.ts` | 아니오 |
| 심층 contract test | `tests/recipes/browser-file-storage-contracts.test.ts` | 아니오 |
| browser data current-status ledger | `docs/architecture/browser-data-capability-completion-ledger.md` | 문서만 |
| 심층 설계/ADR | `docs/architecture/browser-file-and-origin-storage.md`, `decisions/VD-11-browser-file-and-origin-storage.md` | 문서만 |
| client cache scope/persistence 설계 | `docs/architecture/client-cache-and-storage.md`, `decisions/VD-13-client-cache-scope-and-persistence.md` | 문서만 |
| realtime/Web Push/Polling 설계와 reference runtime | `docs/architecture/realtime-events-web-push-and-bounded-polling.md`, `decisions/VD-28-realtime-events-web-push-and-bounded-polling.md`, `src/adapters/realtime`, `src/adapters/web-push` | composition 전에는 tree-shaken |
| transfer/CDN 설계/ADR | `docs/architecture/presigned-transfer-and-image-cdn.md`, `decisions/VD-12-presigned-transfer-and-image-cdn.md` | 문서만 |
| Range/background 결정 | `docs/architecture/decisions/VD-14-resumable-download-and-background-transfer.md` | 문서만 |
| storage lifecycle/migration 결정 | `docs/architecture/decisions/VD-15-origin-storage-lifecycle-and-migration.md` | 문서만 |
| transfer composition/Image provider 결정 | `docs/architecture/decisions/VD-16-browser-transfer-composition-and-image-delivery.md` | 문서만 |
| REST/GraphQL/Connect/gRPC-Web·Protobuf/REST Gateway·Schema·Mapper·Server State 설계 | `docs/architecture/api-contract-schema-mapper-and-server-state.md`, `docs/architecture/protobuf-browser-transport-and-rest-gateway.md`, `decisions/VD-23-api-transport-selection-and-rest-execution.md`, `decisions/VD-24-runtime-schema-and-boundary-mapper.md`, `decisions/VD-25-server-state-cache-lifecycle.md`, `decisions/VD-26-persisted-graphql-operation.md`, `decisions/VD-27-grpc-web-unary-and-server-stream.md`, `decisions/VD-29-connect-web-and-browser-protobuf-runtime.md`, `decisions/VD-30-protobuf-contract-and-rest-gateway.md` | 문서만 |
| provider-neutral Browser RPC V3 계약/port/runtime | `src/contracts/browser-rpc.ts`, `src/application/ports/browser-rpc`, `src/adapters/browser-rpc`, `tests/unit/browser-rpc` | composition 전에는 tree-shaken |
| API contract/server-state 복구 runbook | `docs/operations/api-contract-and-server-state-recovery.md` | 문서만 |
| storage 복구 runbook template | `docs/operations/browser-file-storage-recovery.md` | 문서만 |
| transfer/CDN 복구 runbook template | `docs/operations/browser-transfer-recovery.md` | 문서만 |
| browser-native reference runtime | `src/adapters/browser-files`, `src/adapters/browser-transfer`, `src/adapters/storage/indexeddb`, `src/adapters/storage/opfs`, `src/adapters/cache-storage` | composition 전에는 tree-shaken |
| reference runtime browser evidence | `tests/browser-capabilities` | 아니오 |
| 정적/번들 gate | `scripts/check-optional-recipes.ts` | build 도구 |
| negative fixture | `scripts/check-optional-recipe-fixtures.ts` | 아니오 |
| 완전 제거 gate | `scripts/test-optional-recipe-removal.ts` | 아니오 |
| native reference runtime 제거 gate | `scripts/test-browser-file-storage-runtime-removal.ts` | 아니오 |
현재 `productionRuntimeDependencies`는 빈 배열이며 12개 recipe 모두 선택되지
않았다. `recipes/`의 TypeScript example은 선택 시 복사하고 좁힐 출발점이고,
`src/adapters`의 browser-native reference runtime은 공통 lifecycle·failure
mechanism을 재사용할 수 있는 실제 구현이다. 제품 feature는 이 runtime에
schema/codec/query와 dataset 정책을 주입하고 더 좁은 facade 뒤에서 조립한다.
### Reference runtime bundle budget
`referenceRuntime`이 있는 recipe는 catalog의 `sourceRoots`를 실제 budget entry로
사용한다. gate는 중첩 root의 실행 가능한 source를 중복 제거하고 경로순으로
정렬한 뒤, 모든 module을 하나의 synthetic entry에 포함한다. 이 entry는 Vite
production mode, ES2022/ES module, esbuild minify로 build하며 tree-shaking을
명시적으로 끈다. 따라서 synthetic consumer가 호출 여부를 알 수 없다는 이유로
validation, quota, integrity, cleanup 같은 fail-closed guard가 예산에서 빠지지
않는다. 생성된 모든 chunk/asset의 Node zlib gzip byte 합계를 catalog의
`bundleBudgetGzipBytes`와 비교하고 결과와 SHA-256을
`artifacts/quality/optional-recipes.json`에 기록한다.
이 synthetic build는 설치 크기 상한을 검증하기 위한 것이며 product bootstrap에
runtime을 compose하지 않는다. 별도의 production manifest/module-inventory
검사는 선택되지 않은 runtime source가 실제 `dist`에 없는지 계속 검증한다.
2026-07-28 기준 동일 설정의 `offline-indexeddb` 실측은 32,930 gzip bytes였다.
기존 8,000 bytes 값은 bundle 측정 없이 선언된 값으로 실제 reference runtime과
일치하지 않아 약 9% headroom을 둔 36,000 bytes로 교정했다.
`service-worker-pwa` 10,000 bytes와 `file-transfer` 52,000 bytes는 현재 실측을
수용하므로 유지한다. 이후 source가 예산을 넘으면 gate를 우회하거나 예산을
자동 인상하지 않고, output artifact와 변경 이유를 검토해야 한다.
## 2. 어느 경계에 두는가
| capability 성격 | port 소유자 | adapter 방향 | concrete 위치 예 |
| --- | --- | --- | --- |
| application이 외부 결과를 요청 | application | outbound | `src/adapters/<capability>` |
| URL/browser event가 의도를 전달 | application input | inbound | `src/presentation/adapters` |
| React rendering behavior만 교체 | presentation | local facade | `src/presentation/<capability>` |
| feature 전용 protocol | feature application | in/out 분리 | `src/features/<name>/adapters` |
WebSocket 연결 생성, reconnect와 credential attachment는 outbound다. 수신 JSON
검증과 application input 호출은 inbound다. Service Worker update event,
BroadcastChannel event도 같은 원칙을 적용한다. generated DTO와 vendor SDK
type은 facade 밖으로 노출하지 않는다.
## 3. 12개 recipe 선택표
| recipe | 설치하는 경우 | 설치하면 안 되는 경우 | 핵심 fallback |
| --- | --- | --- | --- |
| realtime | foreground ordered event 또는 duplex protocol이 확정됨 | focus refetch/manual refresh가 충분하거나 ordering·replay owner 없음 | bounded polling 또는 stale UI |
| offline/IndexedDB/OPFS | structured offline data/queue 또는 large local binary가 제품 요구 | credential, partition/retention/recovery 미정, HTTP cache로 충분 | read-only/online-only 또는 승인된 bounded Blob |
| Service Worker/Cache Storage | install/offline shell 또는 public HTTP representation cache owner 승인 | auth/private/opaque cache, update/rollback UX 없음 | hosting cache 기반 network app |
| file/picker/download | selection/preview/upload/download와 bounded memory/integrity 요구 | backend 재검증 없음, whole-buffer large file, long-lived credential URL | native input + authorized direct download |
| generated API | versioned source와 drift CI가 있음 | DTO가 domain/UI로 노출됨 | typed request builder + schema |
| feature flag | rollout/kill switch owner와 default 있음 | authorization에 사용 | typed local default |
| Web Worker | profiler가 main-thread 병목을 증명 | 단순 network I/O | chunked/deferred execution |
| multi-tab | 비민감 event 동기화가 필요 | server가 conflict authority | focus 시 authoritative refresh |
| browser permission | user gesture 기반 기능 필요 | boot 요청, denied UX 없음 | manual input/instruction |
| client workflow | cross-page client-only state가 실재 | query/server state 복제 | URL/local/context/query |
| large data UI | 실측 scale이 budget 초과 | pagination으로 충분, a11y 미정 | accessible pagination |
| analytics/error sink | provider·consent·retention 승인 | arbitrary payload/redaction 우회 | bounded local diagnostics |
정확한 failure matrix, security/privacy, gzip budget과 제거 순서는 JSON catalog가
SSOT다. 문서와 catalog가 다르면 gate가 검사하는 catalog를 우선 고치고 이 표도
같이 갱신한다.
Web Push target은 현재 13번째 설치 recipe가 아니다. 기존 `realtime`,
`service-worker-pwa`, `browser-permission`의 인접 경계를 조합해야 하는 별도
`NOT_SELECTED` capability다. 실제 선택 전 VD-10 amendment와 machine-readable
catalog에 permission, subscription/backend provider, worker handler, notification
policy와 removal source를 명시하며, foreground realtime이 선택됐다는 이유로
Web Push를 함께 설치하지 않는다.
## 4. 공통 구현 순서
1. 문제를 vendor 이름이 아닌 capability와 측정값으로 기록한다.
2. catalog의 trigger와 forbidden 조건을 모두 검토한다.
3. project owner, security/privacy reviewer, gzip budget과 재검토 날짜를 VD-10
amendment에 기록한다.
4. existing URL/local/context/query/application port로 해결되지 않는지 확인한다.
5. 필요한 contract만 `recipes`에서 해당 application/presentation 경계로 복사해
실제 payload와 failure union으로 좁힌다.
6. concrete SDK는 `src/adapters/...` 또는 local presentation facade adapter에서만
import한다.
7. composition root가 concrete adapter를 주입한다. page/use case가 constructor를
직접 호출하지 않는다.
8. fake, unavailable, timeout/cancel, cleanup, malformed input, redaction과
integration test를 작성한다.
9. runtime config schema, dependency inventory/approval, SBOM, bundle budget,
browser support와 runbook을 갱신한다.
10. 실제 provider integration과 negative behavior가 통과한 뒤에만 catalog 상태를
별도 project catalog에서 `INSTALLED`로 바꾼다.
## 5. capability별 필수 검증
### Realtime
- runtime schema로 envelope/version/event ID/sequence/timestamp를 검증한다.
- reconnect는 exponential backoff 상한, visibility/offline 상태, auth refresh와
resume token expiry를 정의한다.
- duplicate/out-of-order는 domain use case에 전달하기 전에 정책화한다.
- route unmount/logout에서 unsubscribe하고 heartbeat timer를 종료한다.
- SSE는 active document의 one-way stream, WebSocket은 duplex protocol,
Web Push는 Service Worker가 받는 background notification hint, Polling은
visible/finite HTTP scheduling policy로 분리한다.
- 기본 event 효과는 query namespace invalidation과 authoritative refetch다.
gap, cursor expiry와 queue overflow에서는 delta 적용을 중단하고 snapshot으로
복구한다.
- Web Push permission은 user action에서만 요청하고 subscription endpoint/key와
payload를 storage, URL, telemetry나 application state에 노출하지 않는다.
- detailed status, target contract와 구현 work package는
[Realtime events, Web Push, and bounded polling](./realtime-events-web-push-and-bounded-polling.md)을
따른다. 현재 concrete runtime과 product selection은 없다.
### IndexedDB/OPFS
- 기존 동기식 preference `StoragePort`에 넣지 않고 feature-specific async
repository와 large-object port를 사용한다.
- raw database/transaction/store/index/schema version은 adapter 밖으로 노출하지
않는다. request success가 아니라 transaction complete 이후에만 성공이다.
- DDL schema와 record codec version을 분리하고 schema upgrade는 additive,
data migration은 resumable bounded batch로 실행한다.
- versionchange/blocked/future schema를 read-only/online-only 상태로 드러내며
자동 reload와 database 삭제를 금지한다.
- OPFS는 immutable bytes만 소유하고 IndexedDB journal이
`PREPARING -> FILES_READY -> COMMITTED -> CLEANED` commit authority를 가진다.
- quota, corrupt row/manifest, migration checkpoint, worker crash, storage eviction,
N-1 rollback을 fixture와 실제 browser에서 검증한다.
### Cache Storage/Service Worker
- Cache Storage는 public HTTP representation 전용이며 application repository나
query cache가 아니다.
- auth, cookie-dependent, private, personal, no-store, opaque, redirect, 206을
거절하고 query/Vary exact match를 보존한다.
- candidate 전체의 type/size/integrity가 검증된 뒤에만 release를 활성화하고
verified previous release를 rollback용으로 유지한다.
- stale worker loop를 막고 unregister와 parsed owned-cache cleanup을 별도 lifecycle로
검증한다.
### File/Blob/picker/download, presigned transfer와 generated API
- native File/Blob/handle은 transient adapter vault 안에 두고 application에는
opaque ref, untrusted metadata와 bounded range/chunk만 전달한다.
- native input을 baseline으로 두고 system picker는 user activation 안에서만
progressive enhancement한다. dismissal은 failure가 아니다.
- upload는 client MIME을 신뢰하지 않고 count/size/signature/server rejection,
resumable session, part checksum/idempotency, quarantine을 다룬다.
- presigned URL은 application에 raw URL로 노출하지 않고 in-memory identity
capability로 보관한다. method/resource 또는 session-part/offset/length/checksum,
expiry, origin/path/query/header를 정확히 묶고 data-plane fetch는 credential,
redirect, referrer와 cache를 fail-closed 정책으로 제한한다.
- resume는 IndexedDB checkpoint만 신뢰하지 않고 server status와 다시 선택한
source의 part digest를 대조한다. checkpoint에는 URL/query/signed header/token을
저장하지 않으며 explicit abort가 불명확하면 reconcile 전까지 유지한다.
- progress는 unknown total을 허용하며 navigation/unmount에서 AbortSignal로
취소한다. server upload session은 별도 abort/TTL cleanup이 필요하다.
- 큰 download는 single `Uint8Array`/Blob이 아니라 browser handoff 또는
backpressure stream을 사용하고 handoff와 confirmed save를 구분한다.
- Image CDN은 raw transform URL builder가 아니라 opaque asset과
composition-registered preset으로만 responsive descriptor를 만든다. immutable
revision, format/width/pixel/decode/cache/expiry와 CDN origin을 검증한다.
- object URL은 explicit lease로 만들고 replacement/unmount에서 revoke한다.
- generated code는 facade 뒤 DTO이며 runtime response schema와 contract drift
gate를 유지한다.
### Flag/worker/multi-tab/browser
- flag unknown/unavailable/stale에서 명시적 typed fallback을 사용하고 access
control로 사용하지 않는다.
- worker는 task ID/generation/cancel을 사용해 stale result를 폐기하고 crash를
normalized failure로 바꾼다.
- multi-tab은 source/event/version으로 self-echo와 duplicate를 막고 payload를
비민감 invalidation hint로 제한한다.
- browser permission은 user gesture에서만 요청하고 denied/dismissed/unsupported를
서로 다른 UX 결과로 처리한다.
### Client workflow/large data/analytics
- workflow store는 server entity/collection을 복제하지 않고 query key나 ID 참조만
보관한다. logout/reset/version mismatch 정책을 테스트한다.
- virtualization은 profiler와 production-like row count로 정당화하며 keyboard,
focus restoration, screen reader와 stale row identity를 검증한다.
- analytics는 essential diagnostics와 consent-required event를 분리하고 closed
event/attribute registry, pre-queue redaction, sampling, bounded queue와
retention을 적용한다.
## 6. 검증 명령
```bash
corepack pnpm check:types:recipes
corepack pnpm test:recipes
corepack pnpm test:browser-capabilities
corepack pnpm build
corepack pnpm check:optional-recipes
corepack pnpm check:optional-recipe-fixtures
corepack pnpm test:optional-recipe-removal
corepack pnpm test:browser-file-storage-removal
```
negative gate는 cleanup 누락, unselected dependency, local adapter 밖 vendor
import, credential localStorage/URL/telemetry 경로, workflow store의 server-state
복제, production source의 recipe import와 선택 전 reference runtime composition을
거절한다. removal gate는 recipe와 recipe test를 삭제한 임시 사본에서 base
typecheck, architecture, test와 build를 실행한다.
별도 native-runtime removal gate는 browser file/storage source와 전용 test,
catalog metadata를 제거한 임시 사본에서 typecheck, architecture, 전체 base test,
build와 optional catalog 검사를 다시 실행한다.
## 7. 제거 체크리스트
1. 신규 호출과 background 작업을 중지한다.
2. subscription, worker, channel, media track, observer를 cleanup한다.
3. persisted store/cache/event queue의 migrate 또는 purge 정책을 실행한다.
4. composition registration과 runtime config를 제거한다.
5. concrete adapter, facade/port와 vendor dependency를 제거한다.
6. dependency baseline, SBOM과 bundle baseline을 갱신한다.
7. typecheck/test/build, production bundle absence와 도메인 기능 fallback을
검증한다.
provider 장애 시 fake로 바꾸어 production을 PASS 처리하지 않는다. 문서화된
unavailable fallback만 사용하고 provider가 필수인 promotion은
`FAIL_UNVERIFIED` 또는 blocked 상태로 유지한다.