Files
llm-wiki/raw/branch-notes/feature-frontend-render-recovery-boundary-contract.md
T

337 lines
38 KiB
Markdown

---
title: branch / feature-frontend-render-recovery-boundary-contract
source_type: branch-note
status: raw
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-015
kind: project-work-item
project: ca-skeleton-frontend-operational-contract
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-015
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-UI-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-ARCHITECTURE-001@1]
refines: []
overrides: []
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-007, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-013]
contract_packet: 1
branch: feature-frontend-render-recovery-boundary-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, error-handling, react]
created: 2026-07-18
target_merge:
status_label: in-progress
contract_packet_sha256: 2c336d6be17e196dcbbbf75409f97f8ff916672d7b634db5e6cf3e568000b57b
imports: [FE-OC-008@1, FE-OC-011@1, FE-OC-014@1]
accepts_delegations: [DELEG-FE-006@1]
---
# branch: feature-frontend-render-recovery-boundary-contract
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출한다. 현재는 `/branch-spec` 자동 채움 단계이며 frontend 코드가 없으므로 모든 진술은 `planned`다.
<!-- section-id: branch-parent -->
## 부모 (필수)
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: boot·route·feature·async boundary ownership과 recovery fixture가 검증된다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-UI-001@1` | UI composition은 React를 사용한다 | React render boundary와 recovery surface에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-ARCHITECTURE-001@1` | domain, application, presentation, adapters, bootstrap 책임을 분리한다 | boot·route·feature·async boundary ownership과 adapter seam에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
| D1 | operational failure와 render defect를 분리한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D2 | boot·route·feature·async boundary ownership을 고정한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D3 | route별 error surface owner를 하나로 제한한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D4 | release pair별 controlled reload를 한 번으로 제한한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D5 | boot validation failure 시 product route 대신 boot shell을 렌더한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D6 | render failure telemetry를 best-effort로 emit한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
없음.
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
이 브랜치는 project-wide 계약 `FE-OC-015`("expected operational error 와 render defect 를 MUST 분리하고 reload loop 를 금지")를 *구현 착수 가능한 상세 명세*로 내린다. 핵심은 두 불변식이다. (1) **분리(separation)** — 정규화된 *운영 실패*(error-classification 이 낸 26-kind operational error)는 컴포넌트의 *정상 state* 로 반환되며 render error boundary 로 throw 하지 않는다. render boundary 가 잡는 것은 *programmer defect 또는 invariant breach*(렌더 도중 던져진 예외)뿐이다([[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.1 마지막 문단, §8.2 `RENDER_FAILURE` 행). (2) **reload loop 금지** — chunk/deploy mismatch 복구용 자동 reload 는 §10.2 의 5개 controlled 조건과 `CHUNK_RELOAD_GUARD` 를 만족할 때 정확히 한 번만 허용되고, 같은 release pair 에서 두 번째 실패가 나면 auto reload 를 멈추고 rollback/support surface 로 넘어간다. 이 브랜치는 boot/route/feature/async 4계층 error boundary 의 *ownership*(무엇을 잡고·무엇을 안 잡고·어떻게 복구하는가)을 §10.1 매트릭스로 고정하고, 그 산출물을 세 계약에 기여한다 — `FE-OC-005`(route-level error/loading surface owner 와의 이중 소유 금지), `FE-OC-011`(async surface 의 terminal-error state 를 boundary 가 아닌 정상 state 로 소비), `FE-OC-025`(boot·chunk mismatch runbook 이 호출할 boundary/reload 메커니즘 제공). UI 기술은 React(`FE-D004`), 라우팅은 React Router Declarative Mode(`FE-D008`)를 전제한다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- **operational-error vs render-defect 분리 계약** — 정규화된 운영 실패는 정상 state 로 반환, render boundary 는 defect/invariant breach 만 catch (hub §10.1·§8.2). 등급 `planned`.
- **4계층 error boundary ownership 매트릭스** — boot shell / route boundary / feature boundary / async boundary 각각의 catches / does-not-catch / recovery 명세 (hub §10.1). 등급 `planned`.
- **controlled reload + `CHUNK_RELOAD_GUARD` state machine** — §10.2 의 5개 조건, release pair 당 1회, 2번째 실패 시 rollback/support (hub §10.2·§5.5·§8.4 `reload-once`). 등급 `planned`.
- **boot error shell** — §4.5 boot order 2~4단계 실패 시 product route 미마운트, boot error shell 만 렌더 (hub §4.5·§8.2 `BOOT_CONFIG_FAILURE`). 등급 `planned`.
- **render-failure telemetry hook** — `ui.render.failed`(route_id·build_id·component_boundary) best-effort emit, sink 실패가 복구를 막지 않음 (hub §5.8·§10.1). 등급 `planned`.
- **recovery fixtures / boundary 테스트** — §20 Measurable completion("boot/route/feature/async boundary ownership + recovery fixtures") + §8.5 관련 negative fixture. 등급 `planned`.
### 제외 범위
> 의도적 제외. 각 항목은 소유 브랜치를 명시(CLAUDE.md §15.5 R3 OUT_OF_BRANCH_SCOPE 방지). 아래 sibling 링크는 hook quirk 회피를 위해 `FE-OC-###` 계약 ID 로만 짝지음(§4b).
- **실패의 정규화(어떤 exception → 어떤 kind)와 26-kind enum·`action` vocabulary** — [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] 의 `FE-OC-008` 소유. 본 브랜치는 정규화된 kind + `action`(특히 `reload-once`/`retry`/`navigate`)을 *소비*해 boundary 배치·복구만 결정한다.
- **async surface state 렌더링**(initial-loading/success/empty/terminal-error 스켈레톤·문안) — [[raw/branch-notes/feature-async-ui-state-contract]] 의 `FE-OC-011` 소유. 본 브랜치는 "operational 실패는 boundary 가 아닌 정상 state 로 간다"는 *seam* 만 정의한다.
- **route registry schema(`errorSurface`/`loadingSurface`/`chunkId` 필드)와 navigation guard** — [[raw/branch-notes/feature-routing-navigation-guard-contract]] 의 `FE-OC-005` 소유. 본 브랜치는 route boundary 가 그 owner 필드를 *채우되* 스키마·guard 로직은 정의하지 않는다.
- **`CHUNK_RELOAD_GUARD` storage row 등록**(namespace/version/classification/quota fallback) — [[raw/branch-notes/feature-frontend-storage-registry-contract]] 의 `FE-OC-013` 소유. 본 브랜치는 guard 의 *의미*(reload loop 차단)만, 키 등록은 위임.
- **release manifest·`DEPLOY_MISMATCH` 신호 생성 + rollback 실행** — [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] 의 `FE-OC-016` 소유. 본 브랜치는 그 신호를 *소비*해 controlled reload/rollback surface 로 분기만 한다.
- **runbook 의 trigger/window/escalation/evidence** — [[raw/branch-notes/feature-frontend-operational-runbook-contract]] 의 `FE-OC-025` 소유. 본 브랜치는 그 runbook 이 호출할 boundary/reload 메커니즘만 제공한다.
- **telemetry transport/queue/redaction sink** — [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] 의 `FE-OC-014` 소유. 본 브랜치는 `ui.render.failed` payload 계약만.
## 근거 (필수, 최소 1개+)
| Source | 정당화하는 결정 |
|---|---|
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | §10.1 error boundary ownership 매트릭스·§10.2 reload loop prevention·§8.2 `RENDER_FAILURE`/operational-vs-defect note·§4.5 boot order·§5.5 `CHUNK_RELOAD_GUARD`·§5.8 `ui.render.failed`·§8.4 `reload-once`·§9.3 route error owner 중복 금지 — `FE-OC-015` 의 project-decision SSOT. D1·D2·D3·D4·D5·D6 근거. |
| [[raw/official-docs/react-ui-library-official]] | `FE-D004`(UI composition = React). **error boundary claim 은 이 자료에 없음**`REACT-UI-C1` 은 "React 는 컴포넌트로 구성된다"만 증명하므로 render boundary *기술 전제*(React 사용)만 근거하고, boundary API 는 아래 web-research 로 보강. D2·D5 부분 근거. |
| [[raw/official-docs/react-router-official]] | `FE-D008`(routing = React Router Declarative Mode). `REACT-ROUTER-C1`/`C4` 가 client-side route 선언을 근거. **route error element API 는 이 발췌 범위 밖**(archived doc 이 명시) → route boundary 의 정확한 error element 형태는 `UNSUPPORTED_IMPL_DECISION`. D3 부분 근거. |
| react.dev 웹 조사(2026-07-19, `react.dev/reference/react/Component`) — 미아카이브 | React error boundary 메커니즘: `static getDerivedStateFromError`(+옵션 `componentDidCatch`)를 가진 컴포넌트가 자식이 *렌더 중* 던진 에러를 잡되 **event handler·async 코드·boundary 자신이 던진 에러는 잡지 않는다**. 이 사실이 "operational 실패는 boundary 로 throw 하지 않는다"(D1)를 강화. **후속: raw/official-docs 로 정식 아카이브 필요**(Claims To Verify). |
## TODO
각 항목 옆에 증거 등급 표기. 현재 frontend 코드가 없으므로 전부 `planned`.
- [ ] 4계층 boundary 컴포넌트 배치(`src/presentation/boundaries/*` + boot shell) — boot/route/feature/async catches·does-not-catch·recovery 구현 (D1/D2/D5) — 등급: `planned`
- [ ] operational-vs-defect seam — 정규화된 kind 를 정상 state 로, defect 만 boundary 로 라우팅하는 경계 wiring (D1) — 등급: `planned`
- [ ] `CHUNK_RELOAD_GUARD` controlled reload state machine — 5조건 순서 + release pair 2회차 중단 (D4) — 등급: `planned`
- [ ] boot error shell — composition root 가 §4.5 2~4단계 실패 시 product route 미마운트 (D5) — 등급: `planned`
- [ ] `ui.render.failed` best-effort telemetry hook — boundary catch 시 emit, 재귀·차단 없음 (D6) — 등급: `planned`
- [ ] recovery fixtures — reload-loop deterministic test + operational-vs-defect fixture + boot invalid-config → boot shell (D1~D5) — 등급: `planned`
## 진행 중 메모
없음 — `/branch-spec` 자동 채움 단계. 코드 미착수. React error boundary 는 class-component 전용 API(`getDerivedStateFromError`)라는 점을 web 조사로 확인했고, 정식 아카이브는 후속 dispatch 로 남긴다.
## 결정 사항
> 아래 6개 결정은 Decision Evidence Map 의 prose mirror. 근거는 대부분 hub 의 project decision(§10·§8·§4.5·§5)이며, D2/D3 는 외부 자료(React·React Router·react.dev web) 가 기술 전제로 병행 근거.
- 2026-07-19: **operational error 와 render defect 를 분리 — 정규화된 운영 실패는 정상 state 로 반환, render boundary 는 defect/invariant breach 만 catch** / 이유: 운영 실패를 boundary 로 throw 하면 async·event-handler 경로에서 애초에 안 잡히고(React error boundary 는 그 경로를 catch 하지 않음) 정상 복구 UX(재시도·stale)를 blank crash 로 격하 / 검토한 대안: 모든 실패를 throw 해 단일 boundary 로 처리 — React 가 event/async 를 안 잡으므로 불완전, hub §10.1 default 위반으로 기각 / 근거: hub §10.1·§8.2, react.dev error boundary 조사.
- 2026-07-19: **boot / route / feature / async 4계층 boundary ownership 을 §10.1 매트릭스로 고정** / 이유: 계층마다 catch 대상·복구가 달라(config vs lazy chunk vs subtree defect vs 정규화 state) 단일 boundary 는 복구 granularity 를 잃음 / 검토한 대안: 전역 단일 boundary — route 1개·lazy chunk 0·외부 API 0 throwaway(hub §0.4)에서만 / 근거: hub §10.1·§4.5·§9.3.
- 2026-07-19: **route error surface 의 이중 소유 금지 — route element 와 React error boundary 중 route 당 정확히 하나가 owner** / 이유: 둘 다 소유하면 같은 render 실패를 두 번 처리하거나 복구가 충돌 / 검토한 대안: 둘 다 두고 우선순위 규칙 — 복잡·모호로 기각 / 근거: hub §9.3 의 *비중복 owner* 원칙(이 결정의 실제 grounding), React Router `REACT-ROUTER-C1`/`C4`(Declarative Mode 의 client-side route 선언). **전제의 한계 명시**: Declarative Mode 가 *route 레벨 error API 자체*(존재 여부·형태)를 제공하는지는 아카이브된 발췌 범위 밖이므로 미확정이다 — 즉 이 결정이 강제하는 것은 "route element 계층에 error API 가 있으면 boundary 와 이중 소유하지 말 것"이라는 비중복 규칙이지, 그 API 의 존재를 주장하는 것이 아니다. 정식 아카이브는 source 후속(Claims To Verify 마지막 행).
- 2026-07-19: **controlled reload 는 `CHUNK_RELOAD_GUARD` 로 release pair 당 1회, 2회차 실패 시 중단** / 이유: `ChunkLoadError`/deploy mismatch 를 무한 reload 로 대응하면 boot loop / 검토한 대안: guard 없는 즉시 reload — §8.4 가 금지(`reload-once` MUST NOT: session guard 없이 반복 reload) / 근거: hub §10.2 5조건·§5.5·§8.4·§8.2.
- 2026-07-19: **boot config/release 검증 실패 시 product route 미마운트, boot error shell 만 렌더** / 이유: 잘못된 config 로 앱을 띄우면 endpoint mismatch·secret 노출·부분 렌더 위험 / 검토한 대안: 실패해도 기본값으로 진행 — §4.5 가 2~4단계 실패를 hard stop 으로 규정, 기각 / 근거: hub §4.5·§8.2 `BOOT_CONFIG_FAILURE`.
- 2026-07-19: **render-failure telemetry(`ui.render.failed`)는 best-effort, sink 실패가 복구·재렌더를 막지 않음** / 이유: 관측이 복구를 blocking 하면 안 됨(operational isolation) / 검토한 대안: 전송 보장 채널 — audit 채널은 별도 owner(FE-OC-014), 기각 / 근거: hub §5.8·§10.1, `FE-D021`.
## 결정-근거 매핑
> `Supporting Claims` 는 hook quirk 회피를 위해 hub 는 plain-text 경로(`...frontend-operational-contract.md §X`)로, official-doc claim 은 plain-text `raw/official-docs/<slug>.md#<CLAIM>` 로, `FE-D###` 는 hub 경로에만 붙여 sibling branch 링크 근처에 두지 않는다(§4b).
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | operational error(정규화된 26-kind 운영 실패)는 정상 컴포넌트 state 로 반환하고 render error boundary 로 throw 하지 않음; boundary 는 programmer defect / invariant breach(렌더 중 throw)만 catch (`FE-OC-015`) | error-classification 이 실패를 총함수로 정규화하는 한(hub `FE-OC-008`) 이 default 유지 / "throw 후 boundary 처리" 대안은 정규화 계층이 없을 때만인데 hub 가 그것을 강제하므로 분기 없음(불변식) | `...frontend-operational-contract.md` §10.1 마지막 문단·§8.2 `RENDER_FAILURE` 행 및 total-function 문단; `raw/official-docs/react-ui-library-official.md#REACT-UI-C1`(React 사용); react.dev `Component`(error boundary 는 event handler·async·boundary 자체 throw 를 catch 안 함 → operational 을 throw 로 흘리면 애초에 미포착) | `project-decision + official-vendor-doc(web, 미아카이브)` | 총함수적 분리는 fixture 로만 증명 — operational 실패가 실수로 throw 되거나 boundary 가 실제 defect 를 operational 로 오분류하면 crash/은닉. exhaustive boundary fixture 필요 |
| D2 | boot shell / route boundary / feature boundary / async boundary 4계층 ownership 을 §10.1 매트릭스(각 계층의 catches·does-not-catch·recovery)로 고정 | client SPA + lazy route chunk + async 데이터(React·React Router·TanStack Query) 구성인 한 4계층 default / 전역 단일 boundary 는 route 1개·lazy chunk 0·외부 API 0 throwaway prototype(hub §0.4 escape)에서만 | `...frontend-operational-contract.md` §10.1 boundary 매트릭스·§4.5 boot order(2~4단계 실패→boot error shell)·§9.3 route 동작; `raw/official-docs/react-ui-library-official.md#REACT-UI-C1` | `project-decision` | async boundary 는 실제로 "throw 를 잡는 boundary"가 아니라 정규화 state 소비 surface(§10.1 행) — `FE-OC-011` 과의 소유 seam 이 모호하면 이중 처리. seam 명세 필요 |
| D3 | route 당 error surface 는 React Router route element 와 React error boundary 중 *정확히 하나*가 owner; 이중 소유 금지 (규칙의 grounding 은 hub §9.3 비중복 owner 원칙이며, Declarative Mode 의 route-error API 존재 자체를 주장하지 않음) | Declarative Mode(`FE-D008`) 의 route element 계층이 error surface 를 소유할 수 있는 한 route 별 owner 를 하나 지정 / 그 계층에 error API 가 없으면 owner 는 전부 React error boundary 로 고정(규칙 자체는 유지, 위반 여지 소멸) / Data/Framework Mode 로 전환되면(그 mode 의 `errorElement`/loader 계약) 재도출 | `...frontend-operational-contract.md` §9.3("route error element 와 React error boundary 의 owner 를 중복하지 않는다"); `raw/official-docs/react-router-official.md#REACT-ROUTER-C1`, `#REACT-ROUTER-C4`; `...frontend-operational-contract.md` `FE-D008` | `project-decision + conditional-default(React Router)` | archived router doc 은 error element API 를 다루지 않음 → 정확한 error element 형태는 `UNSUPPORTED_IMPL_DECISION`; owner 선정 규칙이 route 별로 일관되지 않으면 §9.3 위반 |
| D4 | chunk/deploy mismatch 복구 자동 reload 는 §10.2 5조건(kind∈{`CHUNK_LOAD_FAILURE`,`DEPLOY_MISMATCH`}·release manifest fetch 성공·active release≠current build·`CHUNK_RELOAD_GUARD` unset·guard 선기록 후 reload)을 모두 만족할 때 release pair 당 1회; 같은 pair 2회차 실패 시 auto reload 중단→rollback/support | mismatch 가 감지되고 manifest 가 *더 새로운* release 를 확인할 때만 reload / manifest fetch 실패·같은 pair 이미 guard·storage 불가면 no auto reload(update/support surface) | `...frontend-operational-contract.md` §10.2 5조건·§5.5 `CHUNK_RELOAD_GUARD`(sessionStorage / session / no second auto reload)·§8.4 `reload-once`(MUST NOT: session guard 없이 반복 reload)·§8.2 `CHUNK_LOAD_FAILURE`/`DEPLOY_MISMATCH` 행 | `project-decision` | guard 가 sessionStorage → StoragePort unavailable(private mode)·cross-tab 시 guard 미지속 가능 → fail-safe 로 no-auto-reload 강등 필요. deterministic reload test 로 2회차 중단 증명 |
| D5 | boot config/release 검증(§4.5 2~4단계) 실패 시 product route 를 mount 하지 않고 boot error shell 만 렌더; telemetry adapter 생성 실패(7단계)는 console-safe fallback 으로 계속 | boot order 2~4단계(runtime config fetch·schema·compatibility·release manifest) 실패 → boot error shell / telemetry 등 비필수 adapter 실패 → 계속 진행 | `...frontend-operational-contract.md` §4.5 boot order + 실패 규칙·§8.2 `BOOT_CONFIG_FAILURE`/`RELEASE_MANIFEST_FAILURE` 행 | `project-decision` | boot shell 자체가 실패한 config 에 의존하면 안 됨(zero-config 로 렌더 가능해야) — 미검증 시 boot shell 이 같은 실패로 재크래시. boot invalid-config fixture 필요 |
| D6 | render-failure telemetry(`ui.render.failed`: route_id·build_id·component_boundary)는 best-effort emit, sink/queue 실패가 복구·재렌더를 막지 않음 | telemetry 가 best-effort isolation(`FE-D021`)인 한 항상 non-blocking / 전송 보장이 필요한 audit event 는 별도 owner(`FE-OC-014`) 채널이므로 본 결정 밖 | `...frontend-operational-contract.md` §5.8 `ui.render.failed` event·§10.1 feature boundary recovery; `...frontend-operational-contract.md` `FE-D021` | `project-decision (transport delegated to FE-OC-014)` | boundary 의 `componentDidCatch` 안 telemetry 호출이 throw 하면 boundary 자신이 throw(react.dev: boundary 자체 throw 는 미포착) → 상위 boundary 로 전파. emit 은 try/catch 로 감싸야 함 |
## 구현 가이드
> `planned` blueprint. 경로는 hub §4.6 Planned directory blueprint(`src/presentation/boundaries/`, `src/bootstrap/`) + §5 registry 에서 유도되므로 grounded 하지만 repository 생성 시 바뀔 수 있어 전체 `planned`. sibling 소유 detail 은 §범위 Out of scope 로 위임하고 여기 남기지 않는다(R3).
### 1. 4계층 error boundary 배치 (`src/presentation/boundaries/` + boot shell)
> **Trace**: D1 + D2 + D5 / `FE-OC-015`·hub §10.1 매트릭스·§4.5. React error boundary 는 `static getDerivedStateFromError`(+옵션 `componentDidCatch`)를 가진 컴포넌트가 자식의 *렌더 중* throw 를 catch(react.dev 조사).
>
> - **UNSUPPORTED_IMPL_DECISION**: boundary 구현 방식(hand-rolled class vs `react-error-boundary` 라이브러리) — hub·archived doc 미규정. hand-rolled class 컴포넌트(외부 의존 0) 제안. trade-off: 라이브러리는 reset/fallback API 가 편하지만 supply-chain(`FE-OC-018`) 표면 추가; class 는 boilerplate 지만 의존 0.
> - **UNSUPPORTED_IMPL_DECISION**: boundary 컴포넌트·파일명(hub 는 `presentation/boundaries/` 폴더만 grounding) — `RouteErrorBoundary.jsx`/`FeatureErrorBoundary.jsx`/`BootErrorShell.jsx` 제안. trade-off: 이름 임의, "presentation/boundaries 내부 + 계층당 1 컴포넌트" 제약만 유지하면 계약 동등.
| 계층 | catches (hub §10.1) | does NOT catch | recovery | planned 배치 |
|---|---|---|---|---|
| boot shell | config/release/bootstrap 실패 | product route error | config refetch·support·rollback signal | `src/bootstrap/` composition root (D5) |
| route boundary | route 의 lazy chunk / render 실패 | expected API result(정규화 state) | route retry 또는 controlled reload(D4) | `presentation/boundaries/` route 래핑 |
| feature boundary | 컴포넌트 subtree render defect | 정규화된 operational failure | component reset | `presentation/boundaries/` subtree 래핑 |
| async boundary | 정규화된 query/mutation state | throw 된 render defect | registry `action` | `FE-OC-011` async surface 와 공유 seam(D2) |
### 2. operational-vs-defect seam (정규화 kind 라우팅)
> **Trace**: D1 / `FE-OC-015`·hub §10.1·§8.2. error-classification(`FE-OC-008`)이 낸 정규화 kind 를 *소비*만 하며 정규화 자체는 하지 않는다(R3 위임).
>
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 분기 원칙은 hub §10.1(operational→정상 state, defect→boundary)이 직접 grounding. 실제 kind→state/action 매핑 값은 `FE-OC-008`/`FE-OC-011` 소유.
```text
정규화된 failure(kind, action) 수신 → async/feature 계층의 정상 state 로 렌더 (terminal-error/stale-degraded 등, action 은 FE-OC-011 소유)
렌더 중 throw(non-normalized) 발생 → 가장 가까운 feature/route boundary 가 catch → RENDER_FAILURE recovery
boundary 가 catch 한 값이 정규화 실패로 판명 → 재-throw 금지, RENDER_FAILURE 로 처리(operational 은닉 방지는 fixture 로 검증)
```
### 3. controlled reload + `CHUNK_RELOAD_GUARD` state machine
> **Trace**: D4 / `FE-OC-015`·hub §10.2·§5.5·§8.4. guard 저장은 StoragePort 경유(`FE-OC-013` 소유 registry 의 `CHUNK_RELOAD_GUARD` row 를 *소비*).
>
> - **UNSUPPORTED_IMPL_DECISION**: reload 결정 로직 위치(boundary 내부 vs release adapter) — hub 미규정. release adapter(`ReleaseInfoPort` 구현, §4.4)가 mismatch 판정, boundary 는 그 결과로 reload/rollback surface 분기 제안. trade-off: adapter 집중이 test 용이하나 boundary→adapter 호출 경계 추가.
> - **UNSUPPORTED_IMPL_DECISION**: guard 값 shape(§5.5 는 "session / no second auto reload"만) — `<activeReleaseId>:<currentBuildId>` pair 키 + boolean 제안. trade-off: pair 키여야 "같은 pair 2회차"를 판별; 단일 flag 면 서로 다른 release 간 오차단.
> - **UNSUPPORTED_IMPL_DECISION**: dirty-state 선경고 훅의 호출 위치 — [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.2 의 5조건은 warn-first 를 포함하지 않고, §16.2 immediate containment 1단계가 "current user input 이 있으면 destructive reload 전에 경고"를 *별도로* 규정한다(두 절의 결합 지점은 hub 미규정). 조건 4 통과 후·조건 5(guard 기록 → reload) 직전 호출 제안. trade-off: 이 위치면 경고가 실제 reload 직전 1회만 뜨고 사용자가 취소해도 guard 미기록이라 이후 재시도가 가능하다; 앞으로 당기면(조건 1 직후) mismatch 도 아닌 경우까지 경고해 소음이 된다.
```text
1. failure kind ∈ {CHUNK_LOAD_FAILURE, DEPLOY_MISMATCH} ? 아니면 → reload 안 함
2. release manifest fetch 성공 ? 실패 → no reload, update/support(RELEASE_MANIFEST_FAILURE 는 FE-OC-016 소유)
3. active release ≠ current build ? 같으면 → no reload(mismatch 아님)
4. CHUNK_RELOAD_GUARD[pair] unset ? set 이면 → auto reload 중단, rollback/support surface
4b. dirty-state 선경고 훅(runbook 소유) 호출 → 사용자가 취소하면 reload 안 함(guard 미기록)
5. guard[pair] 기록 후 → 1회 reload
```
> **각주 — dirty-state seam 교차 참조**: 위 4b 는 본 브랜치가 새로 만드는 정책이 아니라 *이미 존재하는 두 계약을 명시적으로 잇는 자리*다. [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §19 의 `FE-RISK-009`("chunk auto reload 가 user input 손실", mitigation = dirty-state guard + one reload cap)는 두 짝으로만 닫힌다 — *one reload cap* 은 본 절의 guard 가 제공하고, *dirty-state guard(warn-first)* 는 §16.2 immediate containment 1단계가 규정한다.
>
> 그 warn-first step 과 위 risk row 의 owner 는 [[raw/branch-notes/feature-frontend-operational-runbook-contract]] (`FE-OC-025`) 이다. 본 브랜치는 훅을 *호출*만 하며 "무엇이 dirty 인가"의 판정 기준·경고 문안·취소 UX 는 그 소유다(R3 위임). 이 seam 을 적지 않으면 본 절의 자동 reload 가 runbook 의 warn-first 가정을 조용히 우회하고, 두 문서가 *암묵적으로만* 일관된 상태로 남는다.
### 4. boot error shell
> **Trace**: D5 / `FE-OC-015`·hub §4.5·§8.2. composition root(`src/bootstrap/composition-root.js`, §4.5)가 boot order 를 소유.
>
> - **UNSUPPORTED_IMPL_DECISION**: boot shell 컴포넌트명·위치(hub 는 `bootstrap/` 만) — `src/bootstrap/BootErrorShell.jsx` + composition-root 가 2~4단계 실패 시 이것만 mount 제안. trade-off: 이름 임의; "zero runtime config 로 렌더 가능 + product route 미마운트" 제약만 유지.
- boot order §4.5 의 2단계(runtime config fetch)~4단계(release manifest 정합) 실패 → `BOOT_CONFIG_FAILURE`/`RELEASE_MANIFEST_FAILURE` → boot error shell 만 렌더(product route 미마운트).
- 7단계(telemetry adapter) 생성 실패 → console-safe fallback, boot 계속(§4.5).
- boot shell 은 실패한 config 에 의존 불가 — build-time 상수(§6.1 build-time public)만 참조.
### 5. render-failure telemetry hook
> **Trace**: D6 / `FE-OC-015`·hub §5.8·§10.1. transport/redaction sink 는 `FE-OC-014` 소유(R3) — 본 절은 emit 시점·payload 계약만.
>
> - **UNSUPPORTED_IMPL_DECISION**: 없음(payload 는 §5.8 이 required attribute 를 grounding). emit 실패 격리 방식만: `componentDidCatch` 내 emit 을 try/catch 로 감싸 재귀·전파 차단 제안(react.dev: boundary 자체 throw 는 상위로 전파).
- `componentDidCatch`(또는 등가 hook)에서 `ui.render.failed`{route_id, build_id, component_boundary} best-effort emit.
- emit 은 try/catch — 실패해도 fallback UI 렌더·recovery 를 막지 않음(§10.1·§5.8).
### 6. boundary 테스트 (§20 Measurable completion)
> **Trace**: D1 + D2 + D3 + D4 + D5 / hub §20("boot/route/feature/async boundary ownership + recovery fixtures")·§8.5·§10.2.
>
> - **UNSUPPORTED_IMPL_DECISION**: test 파일 경로·러너 배치(hub §4.6 은 `tests/component|integration` 폴더만) — `tests/component/boundaries/*` + `tests/integration/reload-guard/*` 제안. trade-off: 경로 임의, "component 레벨 boundary + integration 레벨 reload state machine" 계약만 유지.
| Fixture | 기대 결과 |
|---|---|
| async operation 실패(정규화 kind) | boundary 미발동, async surface 의 terminal-error/stale state 로 렌더(operational 은 정상 state) |
| 컴포넌트 render 중 throw | 가장 가까운 feature/route boundary 가 catch → `RENDER_FAILURE` recovery |
| boundary 자체 throw | 상위 boundary/boot shell 로 전파(react.dev), 무한 루프 없음 |
| `CHUNK_LOAD_FAILURE` 1회차 + manifest 새 release | guard 기록 후 1회 reload |
| 같은 release pair 2회차 실패 | auto reload 중단 → rollback/support surface(§10.2) |
| StoragePort unavailable | guard 미지속 → fail-safe no-auto-reload |
| boot invalid runtime config | product route 미마운트, boot error shell 렌더(D5) |
## 엣지·실패·의존
- **실패·엣지 경로**:
- *boundary 자체가 render 중 throw* → React error boundary 는 자신이 던진 에러를 catch 하지 않음(react.dev) → 상위 boundary 또는 boot shell 이 처리. 최상위(boot shell)까지 throw 되면 최소 static crash surface.
- *event handler / async(setTimeout 등)에서 발생한 에러* → React error boundary 미포착(react.dev) → 반드시 error-classification 이 정규화한 operational failure 로 다뤄 정상 state 로 표현(D1). boundary 에 의존하면 blank crash.
- *StoragePort unavailable/quota*(private mode 등) → `CHUNK_RELOAD_GUARD` 미지속 → fail-safe 로 auto reload 강등(no reload, update/support). guard 부재를 "unset"으로 오해해 무한 reload 하면 안 됨.
- *release manifest fetch 실패* → controlled reload 2단계 불충족 → no reload; `RELEASE_MANIFEST_FAILURE` 자체 생성은 `FE-OC-016` 소유.
- *route element 와 boundary 이중 소유* → 같은 실패 두 번 처리/복구 충돌 → route 당 owner 1개(D3)로 정적 방지.
- **다른 계약 의존**(hook quirk 회피: sibling 링크는 `FE-OC-###` 로만 짝지음):
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`) — 정규화된 kind + `action`*생성*. 그 계약(어떤 exception→어떤 kind, operational vs `RENDER_FAILURE` 구분)이 바뀌면 본 브랜치 seam(D1) 재조정. 해당 sibling 은 error boundary·reload-guard 소유를 이미 본 브랜치로 위임함.
- [[raw/branch-notes/feature-async-ui-state-contract]] (`FE-OC-011`) — async surface state(initial-loading/success/empty/terminal-error) 렌더를 *소유*. 본 브랜치는 "operational 은 boundary 아닌 정상 state" seam(D2)만; state 문안·스켈레톤은 그 소유.
- [[raw/branch-notes/feature-routing-navigation-guard-contract]] (`FE-OC-005`) — route registry(`errorSurface`/`chunkId`)를 *소유*. route boundary 가 그 owner 필드를 채우되 스키마는 그 소유(D3).
- [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016`) — release manifest·`DEPLOY_MISMATCH` 신호·rollback 실행을 *생성/소유*. 본 브랜치는 소비해 reload/rollback 분기(D4).
- [[raw/branch-notes/feature-frontend-storage-registry-contract]] (`FE-OC-013`) — `CHUNK_RELOAD_GUARD` storage row 를 *소유*. 본 브랜치는 guard 의미만(D4).
- [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] (`FE-OC-004`) — build/runtime/secret 분리와 **boot 전 runtime config 검증을 *소유*하며, 그 검증 실패가 본 브랜치 boot error shell 을 발동시키는 `BOOT_CONFIG_FAILURE` 신호를 *생성*** 한다(D5). hub §20 이 그 브랜치의 measurable completion 을 "build/runtime/secret registry + boot invalid matrix" 로 규정하므로 *어떤 config 가 invalid 인가*의 판정은 그쪽 소유이고, 본 브랜치는 그 신호를 소비해 "product route 미마운트 + shell 렌더" 분기만 한다. invalid matrix 의 kind 매핑(§8.2 행)이 바뀌면 D5·구현 가이드 §4 재조정.
- [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] (`FE-OC-002`) — **boot error shell 을 호스팅하는 composition root(`bootstrap` 단일 root)를 *소유*** 한다(hub §4.2 `bootstrap` 행: config load·adapter construction·DI·React mount). 본 브랜치는 그 host 를 *소비*하는 쪽이며, root 가 adapter 를 주입하는 wiring 컨벤션(주입 순서·DI 형태·단일 root 불변식)은 그쪽 소유다(R3 위임). 본 브랜치가 명세하는 것은 hub §4.5 boot order 2~4단계 실패 시의 *분기 규칙*(shell 만 mount)뿐이다(D5).
- [[raw/branch-notes/feature-frontend-operational-runbook-contract]] (`FE-OC-025`) — boot·chunk mismatch runbook 을 *소유*. 본 브랜치가 제공하는 boundary/reload 를 *소비*. 역방향으로, runbook 이 소유한 destructive-reload 선경고 step 을 본 브랜치 reload state machine 이 *호출*한다(구현 가이드 §3 각주).
- [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`FE-OC-014`) — telemetry transport/sink 를 *소유*. 본 브랜치는 `ui.render.failed` payload 계약만(D6).
## 검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| reload loop 가 실제로 차단된다 — 같은 release pair 2회차 실패 시 auto reload 안 함 | state machine·guard 미구현 | deterministic reload-guard test — 1회차 reload 기록 후 2회차 → reload 미호출 assert(§10.2) | `needs-confirmation` |
| operational 실패가 render boundary 에 절대 도달하지 않고, defect 만 도달한다 | seam 미구현, 오분류 가능 | fixture: async operational 실패 → terminal-error state(boundary 미발동) / 렌더 throw → boundary catch → `RENDER_FAILURE` | `needs-confirmation` |
| boot error shell 이 실패한 runtime config 에 의존하지 않고 렌더된다 | boot shell 미작성 | boot invalid-config matrix → product route 미마운트 + shell 렌더, shell 이 runtime config 미참조 assert | `needs-confirmation` |
| boundary 의 `componentDidCatch` telemetry emit 이 재귀·전파를 일으키지 않는다 | emit try/catch 미구현 | telemetry sink throw mock → boundary 가 재-throw 안 하고 fallback 렌더 assert | `needs-confirmation` |
| StoragePort unavailable 시 guard 가 fail-safe(no-auto-reload)로 강등된다 | fallback 경로 미설계 | storage unavailable mock → reload 미호출 + update/support surface assert | `needs-confirmation` |
| React error boundary 가 event-handler·async·자체 throw 를 catch 하지 않는다는 전제 | react.dev web 조사만, vault 미아카이브 | `react.dev/reference/react/Component``wiki-source-summarizer``raw/official-docs/` 정식 아카이브(verbatim quote + self-grep) 후 D1 근거 승격 | `planned` |
| route 당 error surface owner 가 정확히 하나다(이중 소유 없음) | route element API 미확정(archived doc 미포함) | route element vs boundary owner 지정 규칙 test + React Router error element 공식 문서 보강 | `planned` |
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
> `/coverage` 가 채우는 생성물이며 손으로 유지하지 않는다.
| 관심사 | 상태 | owner | 심각도 | 근거 |
|---|---|---|---|---|
| TODO — `/coverage` 실행 전 | missing | (없음) | 미평가 | TODO |
## 마주친 문제
없음 — scaffolding 단계
## 묶음 (이 branch에서 파생된 자료)
<!-- GENERATED: received-delegations:start -->
### 수신한 위임
| Delegation Ref | From | Concern | Status |
|---|---|---|---|
| `DELEG-FE-006@1` | [[raw/branch-notes/feature-async-ui-state-contract]] | `fe.deleg.reload-once-action` | accepted |
<!-- GENERATED: received-delegations:end -->
<!-- GENERATED: project-contract-imports:start -->
## 가져온 프로젝트 계약
| Ref | Owner | 요약 | Branch 적용 |
|---|---|---|---|
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | import 참조로 적용 |
| `FE-OC-011@1` | [[raw/branch-notes/feature-async-ui-state-contract]] | async surface는 initial-loading, success, empty, terminal-error를 MUST 표현 | import 참조로 적용 |
| `FE-OC-014@1` | [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] | telemetry는 best-effort이며 render·API success를 차단하면 안 되고 PII·token을 전송하면 안 됨 | import 참조로 적용 |
<!-- GENERATED: project-contract-imports:end -->
### Sub-branches (세부 작업)
없음 — scaffolding 단계
### 오류 기록 (이 branch 작업 중 발생)
없음 — scaffolding 단계
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
없음 — scaffolding 단계
### 강의 (이 작업을 위해 학습한 강의)
없음 — scaffolding 단계
### job-posting tie-ins (이 작업에서 파생된 글감)
없음 — scaffolding 단계
## 관련 일일 노트
없음 — scaffolding 단계
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목:
- `locally-verified` 항목:
- `prod-verified` 항목:
- **추출하지 않을 항목** (planned / documented-only / abandoned):