266 lines
18 KiB
Markdown
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 상태로 유지한다.
|