docs(branch): 런타임 capability branch-note 6개 스캐폴딩

WI-...-028~033 에 대응하는 FE-OC-027~032 owner 노트.

- 각 노트의 제외 범위에 'use case·domain model·business rule' 을 명시해
  이 skeleton 이 port 와 adapter 계약까지임을 못박았다
- 근거 절에 '근거 등급 경계' 문단을 넣어 project-local default 와
  외부 근거를 구분했다 (수집은 FE-Q-011~014 가 소유)
- 구현 가이드 절은 비워 두고 사유를 남겼다 — 근거 raw 없이 채우면
  모든 cell 이 UNSUPPORTED_IMPL_DECISION 이 된다
- multi-protocol 노트는 '신규 port 0개' 를 제외 범위에 명시했다.
  port 가 늘어나면 그 자체가 회귀 신호다
This commit is contained in:
DongHyeonka
2026-07-28 14:36:43 +09:00
parent 7b5ff2f80b
commit d1ae257efe
6 changed files with 1501 additions and 0 deletions
@@ -0,0 +1,266 @@
---
title: branch / feature-frontend-realtime-subscription-lifecycle-contract
source_type: branch-note
status: raw
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-032
kind: project-work-item
project: ca-skeleton-frontend-operational-contract
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-032
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CAPABILITY-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REALTIME-TRANSPORT-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REALTIME-LIFECYCLE-001@1]
refines: []
overrides: []
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-006, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-007, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-013]
imports: [FE-GATE-031@1, FE-OC-007@1, FE-OC-008@1, FE-OC-011@1, FE-OC-025@1, FLOW-FE-EVENT-003@1, FLOW-FE-EVENT-004@1, FLOW-FE-EVENT-005@1]
delegates: [DELEG-FE-007]
accepts_delegations: []
contract_packet: 1
branch: feature-frontend-realtime-subscription-lifecycle-contract
parent_branch:
related_projects: [ca-skeleton-frontend, ca-skeleton]
tags: [branch, ca-skeleton-frontend, realtime, sse, websocket, push]
created: 2026-07-28
target_merge:
status_label: in-progress
---
# branch: feature-frontend-realtime-subscription-lifecycle-contract
<!-- section-id: branch-parent -->
## 부모 (필수)
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
형제 branch (같은 부모, 이번 확장에서 함께 생성):
- [[raw/branch-notes/feature-frontend-binary-file-io-store-contract]]
- [[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-background-execution-worker-contract]]
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `2`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: backoff·resume·구독 해제·이벤트 검증 fixture와 `FE-RB-006` drill이 통과한다
<!-- 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_REALTIME` 을 이 branch 가 소유하고 default OFF 로 유지한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REALTIME-TRANSPORT-001@1` | 실시간 transport는 SSE를 우선하고 양방향이 필요하면 WebSocket, 둘 다 불가할 때만 최소 간격·backoff·visibility gating을 갖춘 bounded polling을 쓴다 | `RealtimeSubscriptionPort` adapter 선택 순서에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REALTIME-LIFECYCLE-001@1` | 실시간 연결은 full jitter backoff와 30초 cap을 쓰고 재시도 상한 후 terminal 상태로 전이하며 resume은 Last-Event-ID 또는 cursor로 수행하고 unmount 시 구독을 해제한다 | 연결 수명주기 상태 기계와 해제 계약에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
| D1 | 구독 해제는 `RealtimeSubscriptionPort` 가 반환하는 handle 로만 수행하고, adapter 내부에 열린 연결 목록을 유지해 파괴 시 전부 닫는다 | `refines DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REALTIME-LIFECYCLE-001@1` | 근거 raw 미수집 (`FE-Q-011`) | `proposed` |
| D2 | backoff jitter 의 random source 는 주입 가능해야 한다 (결정론 테스트 요건) | `local` | `DELEG-FE-005` 선례 — `feature-api-client-response-envelope-contract` 가 같은 문제를 이미 위임함 | `proposed` |
| D3 | 구독별로 독립적인 full jitter 를 쓰고, 추가로 전역 동시 재연결 상한을 둔다 | `local` | 근거 raw 미수집 (`FE-Q-011`) | `proposed` |
<!-- section-id: declared-overrides -->
### 선언한 예외
해당 없음.
<!-- 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-031@1` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | backoff·resume·구독 해제·이벤트 검증 fixture 가 실패하면 merge·release 를 MUST 차단 | 이 branch 가 owner 로서 fixture 와 report 를 산출 |
| `FE-OC-007@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | JSON envelope와 payload는 boundary에서 runtime schema를 MUST 통과 | 인바운드 프레임도 같은 규칙 적용 |
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | 실시간 실패 4종의 정규화 대상 |
| `FE-OC-011@1` | [[raw/branch-notes/feature-async-ui-state-contract]] | async surface는 initial-loading, success, empty, terminal-error를 MUST 표현 | §9.5 실시간 state 를 이 4-state 에 매핑 |
| `FE-OC-025@1` | [[raw/branch-notes/feature-frontend-operational-runbook-contract]] | boot, chunk mismatch, API degradation, telemetry failure, rollback, realtime 연결, background 실행 runbook을 MUST 유지 | `FE-RB-006` 의 기술 escalation 대상 |
<!-- GENERATED: project-contract-imports:end -->
<!-- GENERATED: received-delegations:start -->
### 수신한 위임
없음. 이 branch 는 `DELEG-FE-007` 의 delegator 다.
<!-- GENERATED: received-delegations:end -->
<!-- GENERATED: flow:start -->
### 가져온 흐름 단계
> `FLOW-FE-EVENT-001`·`002` 는 이 branch 가 **소유**하므로 import 하지 않는다.
| Stage Ref | Order | Owner | Input | Action | Output |
|---|---:|---|---|---|---|
| `FLOW-FE-EVENT-003@1` | 3 | `feature-runtime-schema-validation-contract` | unvalidated event JSON | event envelope 공유 스키마 검증 | discriminated event envelope |
| `FLOW-FE-EVENT-004@1` | 4 | `feature-runtime-schema-validation-contract` | discriminated event envelope | `eventSchema` per-event 검증 | 검증된 event payload |
| `FLOW-FE-EVENT-005@1` | 5 | `feature-frontend-error-classification-boundary-contract` | 검증된 event payload 또는 실패 신호 | 정규화된 이벤트 또는 failure 반환 | application event 또는 normalized failure |
<!-- GENERATED: flow:end -->
<!-- section-id: branch-goal -->
## 목표
`RealtimeSubscriptionPort``PushSubscriptionPort` 를 정의하고, 연결·재연결·재개·이벤트 검증·해제를 계약한다. 이 계약이 없으면 끊김에 상한 없는 즉시 재연결을 걸어 backend 를 증폭 공격하고, 라우트 이탈 후에도 WebSocket 이 열린 채 남아 연결 수가 단조 증가하며, 미검증 프레임이 상태에 병합되어 `TypeError` 가 렌더 트리 깊은 곳에서 늦게 터진다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- `RealtimeSubscriptionPort` — SSE·WebSocket·bounded polling adapter
- `PushSubscriptionPort` — 권한 요청, VAPID 공개키 기반 구독 등록
- 연결 수명주기 상태 기계와 §9.5 surface state
- full jitter backoff, 30초 cap(`REALTIME_RECONNECT_CAP_MS`), 재시도 상한 후 terminal 전이
- resume — `Last-Event-ID` / cursor, gap 감지
- `FLOW-FE-EVENT-001`·`002` 소유 (프레임 수신, transport decode)
- 구독 해제 계약과 누수 검출(`SUBSCRIPTION_LEAKED` fixture)
- bounded polling 의 최소 간격·backoff·visibility gating
- `FE-RB-006` 의 기술 escalation
- `CAP_FE_REALTIME` capability 행 소유
### 제외 범위
- **use case, domain model, business rule** — port 와 adapter 계약까지만 정의한다
- 이벤트의 도메인 의미와 상태 병합 정책
- service worker 등록·수명주기 — `DELEG-FE-007` 로 [[raw/branch-notes/feature-frontend-background-execution-worker-contract]] 에 위임 (WebPush 는 SW 위에서 동작)
- push 발송 서버와 VAPID 개인키 관리 — `FE-Q-014`
- 인바운드 프레임의 스키마 **검증 구현**`FLOW-FE-EVENT-003`·`004` 는 [[raw/branch-notes/feature-runtime-schema-validation-contract]] 소유
- unary protocol adapter — [[raw/branch-notes/feature-frontend-multi-protocol-api-transport-contract]] 소유
## 근거 (필수, 최소 1개+)
| Source | 정당화하는 결정 |
|---|---|
| `[[docs/superpowers/specs/2026-07-28-ca-skeleton-frontend-runtime-adapter-features-design]]` §5.1·§7.5·§9.2 | port 분해, 이벤트 흐름 5단계, 실시간 surface state |
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §7.5·§9.1 | 기존 retry backoff·jitter 정책과 async state 모델 |
**근거 등급 경계**: `FE-D032`(transport 선택 순서)와 `FE-D033`(연결 수명주기)의 rationale 은 각각 `project-local default, 외부 source claim 아님``재연결 폭주·구독 누수 억제, project decision` 이다. SSE(WHATWG HTML `EventSource`)·WebSocket·Web Push 명세를 다룬 raw 자료는 이 repo 에 없다(`FE-Q-011`). 30초 cap 은 측정값이 아니라 초기 default 다.
## TODO
- [ ] `RealtimeSubscriptionPort` 인터페이스와 해제 handle 확정 — 등급: `planned`
- [ ] `PushSubscriptionPort` 인터페이스 확정 — 등급: `planned`
- [ ] transport 선택 순서(SSE → WebSocket → polling) 구현 — 등급: `planned`
- [ ] full jitter backoff + cap + terminal 전이 (주입 가능 random source) — 등급: `planned`
- [ ] resume cursor 와 gap 감지 — 등급: `planned`
- [ ] `FLOW-FE-EVENT-001`·`002` 프레임 수신·디코드 — 등급: `planned`
- [ ] 구독 해제 계약과 누수 fixture — 등급: `planned`
- [ ] bounded polling 의 visibility gating — 등급: `planned`
- [ ] §9.5 surface state 컴포넌트 매핑 — 등급: `planned`
- [ ] `FE-RB-006` drill assertion — 등급: `planned`
- [ ] `FE-GATE-031` realtime lifecycle report 산출 — 등급: `planned`
## 진행 중 메모
`reconnecting``disconnected` 의 구분이 이 branch 의 핵심 UX 계약이다. 전자를 `terminal-error` 로 표시하면 사용자가 불필요하게 새로고침하고, 후자를 `refreshing` 으로 표시하면 영원히 오지 않는 데이터를 기다린다. 구현 시 두 상태가 같은 컴포넌트 분기로 합쳐지지 않는지 리뷰에서 확인한다.
`D2`(주입 가능 random source)는 `DELEG-FE-005` 가 이미 같은 문제를 다뤘다 — `feature-api-client-response-envelope-contract``feature-frontend-clean-architecture-layering-contract` 에 주입 형태를 위임했다. 이 branch 는 그 결과를 재사용하고 새 위임을 만들지 않는다.
## 결정 사항
- 2026-07-28: 재시도 상한 후 terminal 전이 / 이유: 무한 재시도는 backend 증폭이고 사용자에게도 상태를 숨김 / 검토한 대안: 무한 재시도 + 긴 cap / 근거: `FE-D033`
- 2026-07-28: 구독별 독립 jitter + 전역 동시 재연결 상한 / 이유: 구독이 여럿이면 같은 시점에 만료해 thundering herd 가 됨 / 검토한 대안: 전역 단일 backoff / 근거: `FE-RISK-016`
<!-- section-id: decision-evidence -->
## 결정-근거 매핑
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | handle 기반 해제 + adapter 내부 연결 목록 | 항상 | 없음 — `FE-Q-011` | `project-local default` | adapter 파괴 시점이 명확하지 않은 사용 패턴이 있을 수 있음 |
| D2 | 주입 가능 random source | 항상. 결정론 테스트 요건 | `DELEG-FE-005` 선례 | `project precedent` | 주입 형태가 layering branch 결정에 종속 |
| D3 | 구독별 독립 jitter + 전역 상한 | 구독이 2개 이상일 때. 단일 구독이면 전역 상한은 무의미 | 없음 — `FE-Q-011` | `project-local default` | 전역 상한이 정상 재연결을 지연시킬 수 있음 |
<!-- section-id: implementation -->
## 구현 가이드
> 근거 raw 자료(`FE-Q-011`) 수집 전까지 비워 둔다. SSE 의 재연결·`Last-Event-ID` 동작과 WebSocket close code 해석은 명세를 읽지 않고 쓸 수 없다.
<!-- section-id: edge-failure-dependency -->
## 엣지·실패·의존
- **실패·엣지 경로**
- 최초 연결 실패 → `REALTIME_CONNECT_FAILED`, backoff 재시도, `connecting` 유지
- 재시도 상한 소진 → `REALTIME_DISCONNECTED` terminal. **자동 재시도 없음**, 사용자 주도 retry
- resume cursor 로 메울 수 없는 공백 → `REALTIME_RESUME_GAP`, 권위 데이터 refetch 권고
- 프레임 스키마 위반 → `EVENT_SCHEMA_MISMATCH`, 해당 프레임만 드롭하고 **연결은 유지**
- transport decode 실패 → 프레임을 버리되 연결을 즉시 끊지 않음(`FLOW-FE-EVENT-002` 불변식)
- unmount → 구독 해제. 해제 누락은 `SUBSCRIPTION_LEAKED` fixture 가 잡는 결함이지 런타임 kind 가 아님
- 알림 권한 거부 → `PUSH_PERMISSION_DENIED`, 재요청 반복 금지
- SSE·WebSocket 모두 미지원 → bounded polling 으로 강등(`degraded-alternative`)
- **다른 계약 의존**
- [[raw/branch-notes/feature-runtime-schema-validation-contract]] 의 `FLOW-FE-EVENT-003`·`004` 에 의존
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] 의 `FLOW-FE-EVENT-005` 총함수에 의존
- [[raw/branch-notes/feature-async-ui-state-contract]] 의 4-state 모델에 의존 — §9.5 가 그 위에 매핑됨
- [[raw/branch-notes/feature-frontend-background-execution-worker-contract]] 의 SW 등록에 의존 (`DELEG-FE-007`, WebPush 전용)
- [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] 의 주입 형태 결정에 의존 (`DELEG-FE-005` 결과 재사용)
<!-- section-id: claims-to-verify -->
## 검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| unmount 후 열린 구독이 0이다 | 해제 누락이 조용히 누적됨 | fixture — 컴포넌트 mount/unmount 반복 후 adapter 내부 연결 목록 길이 0 (`FE-NFR-017`) | `planned` |
| 재연결 간격이 30초 cap 을 넘지 않는다 | jitter 계산이 cap 을 초과하기 쉬움 | 결정론 fake clock test — 모든 시도 간격 ≤ cap (`FE-NFR-016`) | `planned` |
| 상한 소진 후 자동 재시도가 멈춘다 | 무한 루프가 조용히 남음 | fixture — 상한 도달 후 추가 연결 시도 0 | `planned` |
| 미검증 프레임이 application 에 도달하지 않는다 | 검증을 우회하는 빠른 경로가 생기기 쉬움 | negative fixture — 스키마 위반 프레임 주입 후 application 콜백 미호출 | `planned` |
| 스키마 위반 프레임 하나가 연결을 끊지 않는다 | 방어적으로 끊는 구현이 흔함 | fixture — 위반 프레임 후 정상 프레임 수신 확인 | `planned` |
| resume gap 이 실제로 감지된다 | 서버가 gap 을 알려주지 않으면 감지 방법이 제한적 | integration test — 이벤트 ID 불연속 주입 후 `REALTIME_RESUME_GAP` | `needs-confirmation` |
| 다중 구독 재연결이 동시에 몰리지 않는다 | 독립 jitter 만으로 충분한지 불확실 | 부하 fixture — N개 구독 동시 끊김 후 재연결 시각 분포 (`FE-RISK-016`) | `needs-confirmation` |
| bounded polling 이 백그라운드 탭에서 멈춘다 | visibility gating 누락이 흔함 | fixture — `document.hidden` 상태에서 요청 0 | `planned` |
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
미생성.
## 마주친 문제
없음.
## 묶음 (이 branch에서 파생된 자료)
### Sub-branches (세부 작업)
아직 없음.
### 오류 기록 (이 branch 작업 중 발생)
아직 없음.
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
아직 없음.
### 강의 (이 작업을 위해 학습한 강의)
아직 없음.
### job-posting tie-ins (이 작업에서 파생된 글감)
아직 없음.
## 관련 일일 노트
- 아직 없음
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목: 없음
- `locally-verified` 항목: 없음
- `prod-verified` 항목: 없음
- **추출하지 않을 항목**: 현재 전 항목 `planned`