365 lines
43 KiB
Markdown
365 lines
43 KiB
Markdown
---
|
||
title: branch / feature-async-ui-state-contract
|
||
source_type: branch-note
|
||
status: raw
|
||
branch: feature-async-ui-state-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, application, react, error-handling]
|
||
created: 2026-07-18
|
||
target_merge:
|
||
status_label: in-progress
|
||
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-013
|
||
kind: project-work-item
|
||
project: ca-skeleton-frontend-operational-contract
|
||
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-013
|
||
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-UI-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SERVER-STATE-001@1]
|
||
refines: []
|
||
overrides: []
|
||
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-007, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-010]
|
||
contract_packet: 1
|
||
contract_packet_sha256: 106ffdb88a0c1937bbc4d9387b99c06b90050d7e375ab4e29c7ed2b1a0dfe570
|
||
imports: [FE-OC-002@1, FE-OC-008@1, FE-OC-012@1, FE-OC-015@1, FE-OC-020@1]
|
||
delegates: [DELEG-FE-006@1]
|
||
|
||
---
|
||
|
||
# branch: feature-async-ui-state-contract
|
||
|
||
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 현재는 scaffolding 단계다.
|
||
|
||
<!-- 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`
|
||
- **완료 조건**: required와 non-blocking state matrix component test가 통과한다
|
||
|
||
<!-- section-id: inherited-project-decisions -->
|
||
### 상속한 프로젝트 결정
|
||
|
||
| Decision Ref | Project Summary | Branch Application | Source |
|
||
|---|---|---|---|
|
||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-UI-001@1` | UI composition은 React를 사용한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SERVER-STATE-001@1` | application-owned QueryCachePort와 TanStack Query adapter를 사용하고 client store에 server state를 복제하지 않는다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
|
||
|
||
<!-- section-id: branch-local-decisions -->
|
||
### 브랜치 지역 결정
|
||
|
||
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
|
||
|
||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||
|---|---|---|---|---|
|
||
|
||
<!-- section-id: declared-overrides -->
|
||
### 선언한 예외
|
||
|
||
| Override ID | Overrides | Reason | Approval | Status |
|
||
|---|---|---|---|---|
|
||
<!-- GENERATED: branch-contract:end -->
|
||
|
||
<!-- section-id: branch-goal -->
|
||
## 목표
|
||
|
||
이 브랜치는 project-wide contract `FE-OC-011`(async surface는 initial-loading·success·empty·terminal-error를 MUST 표현)의 **single owner**로서, hub §9.1 Async surface state model을 *되묻지 않고 구현할 수 있는 spec*으로 내린다. 원격 데이터에 의존하는 모든 view는 `loading` boolean 하나로 상태를 뭉개지 않고 required 4-state + non-blocking 4-state를 discrete하게 표현해야 하며(§9.1), 이 상태들을 React 함수형 컴포넌트 + 단방향 props 흐름으로 렌더한다(`FE-D004`, [[raw/official-docs/react-ui-library-official]] `REACT-UI-C1`·`REACT-UI-C5`). 부수적으로 `FE-OC-015`(operational failure를 state로 반환·render defect만 boundary throw), `FE-OC-020`(component state matrix test artifact), `FE-OC-024`(sample slice가 async surface를 fixture로 exercise)에 기여한다. 현재 frontend repository가 없으므로 본 노트의 모든 구현 주장은 `planned`이며 코드 evidence는 0건이다.
|
||
|
||
- 이슈: (없음 — repository 생성 전)
|
||
- PR: (없음)
|
||
|
||
<!-- section-id: branch-scope -->
|
||
## 범위
|
||
|
||
### 포함 범위
|
||
|
||
- `FE-OC-011` 소유: async surface의 **required visible state** 4종(`initial-loading`/`success`/`empty`/`terminal-error`)과 **non-blocking state** 4종(`refreshing`/`stale-degraded`/`mutation-pending`/`mutation-conflict`)의 discrete 표현 계약 (hub §9.1).
|
||
- 단일 `loading` boolean 금지 규칙의 코드 표현(base tagged union + non-blocking overlay flag 2축, §1) + base×overlay 합법 조합·indicator 우선순위 규칙.
|
||
- server query/mutation 신호 → `{base, overlay}` 파생 계약의 **presentation 측 소비 형태**(view-model만 소비, TanStack Query client 직접 import 금지 — §4.3/§9.2).
|
||
- `terminal-error` state가 normalized failure의 `userMessageKey` + `action`만 렌더하는 계약(§8.1/§8.4 소비).
|
||
- `FE-D004`(UI composition = React) 소유 — 함수형 컴포넌트·props 단방향 흐름을 async state 렌더 기반으로 채택.
|
||
- measurable completion: state matrix component test(base 4 + overlay 4 + 교차 2 + latch 전이 1 = 11 fixtures, `pnpm test:component` async fixtures).
|
||
|
||
### 제외 범위
|
||
|
||
> 의도적으로 제외. "이건 다른 owner 브랜치 범위"라고 답할 근거.
|
||
|
||
- **failure의 정규화(raw → 26-kind)**: `feature-frontend-error-classification-boundary-contract`(`FE-OC-008`) 소유. 본 브랜치는 normalized failure를 *소비*만 한다.
|
||
- **server state의 fetch/cache/invalidation·QueryCachePort 정의**: `feature-server-state-caching-contract`(`FE-OC-012`) 소유. 본 브랜치는 port가 노출하는 상태 신호를 *소비*한다.
|
||
- **error boundary topology·recovery 배치(boot/route/feature/async boundary 소유권)**: `feature-frontend-render-recovery-boundary-contract`(`FE-OC-015`) 소유. 본 브랜치는 "operational failure는 throw하지 않는다"는 계약만 제공.
|
||
- **component test 하네스 구성(Vitest/RTL/MSW 설정·gate 분리)**: `feature-frontend-test-taxonomy-contract`(`FE-OC-020`) 소유. 본 브랜치는 async fixture 목록·기대치만 제공.
|
||
- **loading/error live region·focus 관리의 axe 검증**: `feature-accessibility-baseline-contract` 소유(이 브랜치에 depend). async state는 a11y hook point만 노출하고 axe 규칙을 정의하지 않는다.
|
||
- **auth token lifecycle / 401 replay**: 외부 auth owner + [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] `FE-OC-010`.
|
||
- **telemetry event 정의·redaction·sink 정책**: 본 브랜치는 async state 전용 telemetry event를 정의하지 않으며 [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] `FE-OC-014`가 소유한다(hub §2.2 Q7 응답). error 표기 state가 남기는 telemetry rule은 §8.2 failure matrix의 kind별 rule을 그대로 따르고, 본 브랜치는 §8.1 금지 필드(raw body/token/stack)를 UI·telemetry 양쪽에 노출하지 않는 계약만 제공한다.
|
||
|
||
## 근거 (필수, 최소 1개+)
|
||
|
||
| Source | 정당화하는 결정 |
|
||
|---|---|
|
||
| [[raw/official-docs/react-ui-library-official]] `REACT-UI-C1` | D1 — UI composition을 React로 채택(재사용 컴포넌트 단위로 async surface 구성). `FE-D004`의 official 근거. |
|
||
| [[raw/official-docs/react-ui-library-official]] `REACT-UI-C5` | D1·D4 — 부모 state를 props로 자식에 전달하는 단방향 흐름을, discrete async state의 렌더/전파 모델로 채택. |
|
||
| [[raw/official-docs/tanstack-query-server-state-official]] `TSQ-C1` | D4 — async surface가 소비하는 "server state"(loading/staleness/refetch 신호)의 정의적 근거. 단, port 소유·구현은 `FE-OC-012`에 위임(delegated). |
|
||
|
||
## TODO
|
||
|
||
각 항목 옆 증거 등급 표기.
|
||
|
||
- [ ] base 4-state tagged union + non-blocking overlay flag 집합(2축) + `deriveAsyncState` selector 계약 정의 — 등급: `planned`
|
||
- [ ] base × overlay 합법 조합표(§1.2, 20조합) + 단일 슬롯 indicator 우선순위(§1.3) 확정 — 등급: `planned`
|
||
- [ ] server query/mutation 신호 → `{base, overlay}` 파생 매핑표 확정(server-state 계약 fix 후) — 등급: `planned`
|
||
- [ ] `terminal-error` 렌더 컴포넌트(`userMessageKey` + `action` only, raw body/stack 금지) — 등급: `planned`
|
||
- [ ] non-throw 규율 + async→render boundary handoff 계약 문서화 — 등급: `planned`
|
||
- [ ] `{base, overlay}` state matrix fixture 11종(base 4 + overlay 4 + 교차 2 + latch 전이 1, `pnpm test:component`) — 등급: `planned`
|
||
- [ ] 구현 repository·검증 evidence 식별 — 등급: `needs-confirmation`
|
||
|
||
## 진행 중 메모
|
||
|
||
- 본 노트는 `/branch-spec` self-map으로 hub `FE-OC-011` owner scope에서 도출. frontend 코드는 아직 없음 → 전부 `planned` blueprint.
|
||
|
||
## 결정 사항
|
||
|
||
> 아래 Decision Evidence Map의 prose mirror. 근거는 hub §9.1/§8/§10.1/§4 + `raw/official-docs/react-ui-library-official`.
|
||
|
||
- **D1**: UI composition은 React 함수형 컴포넌트 + 단방향 props 흐름으로 하고, async surface의 discrete state를 그 위에 렌더한다(`FE-D004`). 대안: native custom-element / 다른 framework fork(revisit trigger).
|
||
- **D2**: 원격 데이터에 의존하는 모든 async surface는 required 4-state(`initial-loading`/`success`/`empty`/`terminal-error`)를 MUST 표현한다(§9.1). 대안: 없음(surface당 불변).
|
||
- **D3**: non-blocking 4-state(`refreshing`/`stale-degraded`/`mutation-pending`/`mutation-conflict`)를 required state와 **별개 축으로** 표현한다. hub §9.1이 금지하는 것은 "`loading` boolean 하나로 empty/error/refreshing을 합치는 것"이므로, 금지 대상은 *상태 개수를 1개 boolean으로 붕괴시키는 것*이지 다축 구조 표현이 아니다.
|
||
- **D8**: async surface 상태는 **`base` (required 4 중 정확히 1개) + `overlay` (non-blocking 4의 flag 집합)** 2축으로 표현한다. required 4는 §9.1 Data 열이 상호배타(none / present / valid empty / none-or-unusable)이므로 한 시점에 정확히 하나이고, non-blocking 4는 §9.1이 "Additional"로 분류하며 `refreshing`이 "existing content 유지"를 요구하므로 base를 *대체하지 않고 겹친다*. 단일 flat 8-union은 `success`+`refreshing` 동시 성립을 표현할 수 없어 기각. overlay 간 동시 성립 시 단일 슬롯 indicator 우선순위는 `mutation-conflict` > `mutation-pending` > `stale-degraded` > `refreshing`(§구현 가이드 §1.3).
|
||
- **D4**: async state는 §1의 2축 구조(base tagged union + overlay flag set)로 표현하고 presentation은 application facade view-model만 소비한다. server query/mutation 신호 → `{base, overlay}` 파생은 application/adapter 경계에서 하며 presentation은 TanStack Query client를 직접 import하지 않는다(§4.3/§9.2).
|
||
- **D5**: `terminal-error`(및 stale-degraded/mutation-conflict의 error 표기)는 error-classification이 낸 normalized failure의 `userMessageKey` + closed `action`만 렌더하고 raw body/stack을 노출하지 않는다(§8.1/§8.4 소비).
|
||
- **D6**: async surface는 operational failure를 normal state로 반환하고 render boundary로 throw하지 않는다; render defect(programmer error/invariant breach)만 boundary가 잡는다(§10.1).
|
||
- **D7**: 완료 판정은 base 4 + overlay 4 + 교차 2 + latch 전이 1(총 11 fixture)을 결정론적으로 재현하는 component matrix test(Vitest + RTL, `pnpm test:component`)다(§20 measurable completion).
|
||
|
||
## 결정-근거 매핑
|
||
|
||
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|
||
|---|---|---|---|---|---|
|
||
| D1 | UI composition은 React 함수형 컴포넌트 + 단방향 props 흐름을 채택하고, async surface의 discrete state를 그 위에 렌더한다 (`FE-OC-011` / `FE-OC-002`) | component-based UI를 유지하는 한 React default / native custom-element·다른 framework로 project fork 시 재검토(`FE-D004` revisit trigger) | `raw/official-docs/react-ui-library-official.md#REACT-UI-C1`, `#REACT-UI-C5`; [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D004` | `official-vendor-doc` + `project-decision (accepted-documented-only)` | React 채택은 code evidence 없음(문서상 채택). 실제 컴포넌트 트리가 nesting/props 패턴을 따르는지 로컬 검증 필요(react-ui doc Usage Boundaries) |
|
||
| D2 | 원격 데이터 의존 async surface는 required 4-state(`initial-loading`/`success`/`empty`/`terminal-error`)를 MUST 표현 (`FE-OC-011`) | async surface(원격 데이터 view)가 존재하는 한 항상 4-state / 순수 정적 view(원격 데이터 없음)엔 async state 계약 불필요 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.1 required visible states 표 | `project-decision` | exhaustive coverage는 `{base, overlay}` state matrix test 11종(base 4 + overlay 4 + 교차 2 + latch 전이 1)으로만 증명(measurable completion) — 미구현 시 empty/error 누락 경로 leak |
|
||
| D3 | non-blocking 4-state(`refreshing`/`stale-degraded`/`mutation-pending`/`mutation-conflict`)를 required state와 **분리된 축**으로 표현; 금지 대상은 "`loading` boolean 하나로 empty/error/refreshing 합치기"로 한정 (`FE-OC-011`) | background activity·write-in-flight·retry-exhausted·conflict가 발생 가능한 surface에 적용 / 발생 불가한 surface는 해당 overlay 생략(단 required 4-state는 유지) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.1 "Additional non-blocking states" 표 + 인용 "`loading` boolean 하나로 empty/error/refreshing을 합치면 contract violation이다" | `project-decision` | 어떤 surface가 어떤 non-blocking state를 갖는지는 operation semantics에 의존 — surface별 적용 범위 판단 필요 |
|
||
| D8 | async surface 상태는 `base`(required 4 중 1개) + `overlay`(non-blocking 4의 flag 집합) 2축으로 표현하고, overlay 동시 성립 시 단일 슬롯 indicator 우선순위는 `mutation-conflict` > `mutation-pending` > `stale-degraded` > `refreshing` (`FE-OC-011`) | §9.1이 required/additional 2표를 유지하고 `refreshing`이 기존 content를 유지하는 한 2축 / 만약 hub가 non-blocking state를 base와 상호배타로 재정의하면 flat union으로 회귀 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §9.1 required 표(Data 열 none/present/valid empty/none-or-unusable = 상호배타) + "Additional non-blocking states" 표 + `refreshing` UI 요구 "existing content 유지" + `mutation-pending` Data "current view" | `project-decision` (구조) + `UNSUPPORTED_IMPL_DECISION` (표현 shape·indicator 우선순위) | §9.1은 overlay 동시 성립 시 렌더 우선순위를 규정하지 않음 — §1.3 우선순위는 사용자 trade-off. base×overlay 합법 조합표(§1.2)는 §9.1 Data 열에서 도출한 해석이며 hub가 명시한 표가 아님 |
|
||
| D4 | async state는 §1의 2축 `{base, overlay}`(base tagged union 1개 + non-blocking overlay flag 집합)로 표현, presentation은 application facade view-model만 소비하고 TanStack Query client를 직접 import하지 않음; server 신호 → `{base, overlay}` 파생은 application/adapter 경계 (`FE-OC-011` → `FE-OC-012` 소비) | server state가 `QueryCachePort`로 소유되는 한(`FE-D006`) 유지 / presentation 직접 import는 §4.3 dependency rule 위반이라 대안 아님 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §4.3 dependency matrix·§9.2 (presentation·application은 TanStack Query 직접 import 안 함); `raw/official-docs/react-ui-library-official.md#REACT-UI-C5`; `raw/official-docs/tanstack-query-server-state-official.md#TSQ-C1` | `project-decision` + `official-vendor-doc` | server 신호(status/fetchStatus) → `{base, overlay}` 매핑 함수 shape는 hub가 규정 안 함(§구현 가이드 UNSUPPORTED_IMPL). port 신호 형태는 `FE-OC-012` owner 소유 — 계약 fix 전엔 매핑 잠정 |
|
||
| D5 | `terminal-error`(및 error 표기 state)는 normalized failure의 `userMessageKey` + closed `action`만 렌더, raw body/stack 노출 금지 (`FE-OC-011` ← `FE-OC-008` 소비) | 모든 error 표기에서 불변 / 예외 없음 — raw 노출은 `FE-OC-008`이 금지 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §8.1 normalized failure shape·§8.4 UX action vocabulary | `project-decision (delegated consume)` | `kind → action` 계약 shape은 error-classification(`FE-OC-008`) 소유 — 그 계약 미확정 시 렌더 계약 모호(해당 브랜치 D6이 "action 실제 UI 실행은 async-ui 소유"라고 위임함) |
|
||
| D6 | async surface는 operational failure를 normal state(`terminal-error`/`stale-degraded`)로 반환하고 render boundary로 throw하지 않음; render defect만 boundary가 catch (`FE-OC-011` → `FE-OC-015` 기여) | normalized operational failure는 항상 state 반환 / programmer defect·invariant breach만 throw | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §10.1 error boundary ownership(async boundary는 normalized state를 catch, thrown render defect는 catch 안 함) | `project-decision` | boundary topology·recovery 배치는 render-recovery(`FE-OC-015`) 소유 — async surface는 "throw 안 함" 계약만 제공. 경계 계약이 어긋나면 operational failure가 render boundary로 새어 reload loop 위험 |
|
||
| D7 | 완료 판정은 `{base, overlay}` 11 fixture(base 4 + overlay 4 + 교차 2 + latch 전이 1)를 결정론적으로 재현하는 component matrix test(Vitest + RTL, `pnpm test:component`) (`FE-OC-011` → `FE-OC-020` 기여) | 2축 `{base, overlay}` 계약이 유효한 한 매트릭스 test / 대안 없음 — 완료의 유일 evidence(§20 measurable completion) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §20 measurable completion·§16 `pnpm test:component`(async fixtures)·`FE-D022` test stack | `project-decision` + `conditional-default (test stack)` | RTL/Vitest 하네스·fixture 구조는 test-taxonomy(`FE-OC-020`) 소유 — 본 브랜치는 async fixture 목록·기대치만 확정 |
|
||
|
||
## 구현 가이드
|
||
|
||
> 전부 `planned` blueprint. 경로는 hub §4.6 Planned directory blueprint에서 도출(repository 생성 시 변경 가능). 코드는 존재하지 않는다.
|
||
|
||
### 1. Async surface state machine (base tagged union + non-blocking overlay flags)
|
||
|
||
> **Trace**: D2·D3·D8 + `FE-OC-011` + hub §9.1. §9.1은 두 개의 표를 유지한다 — "Required visible states"(4) 와 "Additional non-blocking states"(4). 후자는 전자를 *대체하지 않는다*: `refreshing`의 UI 요구가 "existing content 유지"이고 `mutation-pending`의 Data가 "current view"이므로, 이 state들은 데이터를 이미 가진 base 위에 겹친다. 따라서 8개를 하나의 상호배타 union으로 뭉치면 `success`+`refreshing` 또는 `success`+`mutation-pending` 동시 성립을 표현할 수 없다. 본 절은 이를 **2축**(base 1개 + overlay flag 집합)으로 계약화한다.
|
||
>
|
||
> - **UNSUPPORTED_IMPL_DECISION**: 2축 값 객체의 구체 shape(`{ base: 'success', overlay: { refreshing: false, staleDegraded: false, mutationPending: false, mutationConflict: false } }`)·tag 필드명·모듈 경로(`src/presentation/components/async/async-surface-state.js`)·`isValidEmpty(data)` 판별자 — hub §9.1은 state 이름과 UI 요구만 규정하고 JS 표현 shape/파일 경로/empty 판별 predicate를 규정하지 않음. Trade-off: base를 tagged union으로 두어 exhaustive `switch` + RTL fixture addressability를 유지하고, overlay는 flag 집합으로 두어 동시 성립을 손실 없이 표현. hub §9.1이 실제로 금지하는 문장은 "`loading` boolean 하나로 empty/error/refreshing을 합치면 contract violation이다"이므로 금지 대상은 *단일 boolean으로의 붕괴*이고, base+overlay 구조 표현은 그 금지에 해당하지 않는다(오히려 empty/error/refreshing이 서로 구분 가능하게 남는다).
|
||
> - **UNSUPPORTED_IMPL_DECISION**: §1.3 indicator 우선순위 — §9.1은 overlay가 동시에 성립할 때 어떤 UI 요구를 우선할지 규정하지 않음. Trade-off: "사용자 조치를 요구하는 것이 조용한 배경 신호보다 우선"이라는 단일 원칙으로 정렬.
|
||
> - **UNSUPPORTED_IMPL_DECISION**: §1.1.1 `staleFailure` latch — hub §9.1은 `stale-degraded`의 진입 조건만 주고 clear/exit 조건을 규정하지 않으며, latch의 보관 위치·수명(query key 단위 / adapter 내부 vs selector 인자)과 refetch 진행 중 stale label 유지 여부도 규정하지 않음. Trade-off: latch를 `!refetchInFlight`와 곱해 read 축 두 overlay를 *정의상* 배타로 만들어(§9.2 focus refetch가 발동하는 정상 경로에서 invariant throw 회피), refetch 진행 중에는 stale label은 유지하되 manual retry affordance만 비활성화한다("아직 stale이지만 재시도 중"을 전달). alt = "동시 성립을 합법으로 허용"은 §9.1의 두 UI 요구(subtle indicator vs stale label + manual retry)가 같은 슬롯에서 충돌해 기각.
|
||
> - **UNSUPPORTED_IMPL_DECISION**: §1.2 query-less(mutation-only) surface의 base 규칙 — hub §9.1은 "원격 데이터에 의존하는 surface"만 다루고 read query가 없는 write-only surface의 base를 규정하지 않음. Trade-off: base = `success` 고정 + write 축 overlay만 허용해, form이 항상 렌더 가능하다는 사실과 §9.1 required-state 표현 의무를 동시에 만족. alt = 이런 surface를 계약 밖으로 배제하면 `mutation-pending`의 "duplicate action 차단"(§9.1) 근거가 submit form에서 사라져 기각.
|
||
> - **해석 주의(§1.2 조합표의 지위)**: §1.2 base×overlay 합법 조합표는 hub가 명시한 표가 **아니라** §9.1 Data 열(none / present / valid empty / none-or-unusable, 그리고 overlay 4종의 데이터 전제)에서 도출한 *해석*이다. 위반 시 render defect로 취급해 throw하는 근거(D6 invariant breach 경로)도 이 해석 위에 서 있다. hub가 §9.1에 조합표를 명시하면 본 절이 그것으로 대체된다.
|
||
|
||
#### 1.1 두 축
|
||
|
||
Base state (§9.1 required 표 — 한 시점에 **정확히 1개**, Data 열이 상호배타):
|
||
|
||
| base | data | activity | UI 요구 (§9.1) | 파생 신호(소비) |
|
||
|---|---|---|---|---|
|
||
| `initial-loading` | none | first request | 안정적 skeleton, focus theft 금지 | query pending & no cached data |
|
||
| `success` | present | idle | view-model render | query success & non-empty |
|
||
| `empty` | valid empty | idle | empty 사유 + 가능 시 primary action | query success & `isValidEmpty` |
|
||
| `terminal-error` | none/unusable | stopped | safe message + registry action(§3 참조) | normalized failure(retry 소진/비재시도) |
|
||
|
||
Overlay flags (§9.1 "Additional non-blocking states" 표 — **0개 이상 동시 성립**, base를 대체하지 않음):
|
||
|
||
| overlay | data 전제 | activity | UI 요구 (§9.1) | 파생 신호(소비) |
|
||
|---|---|---|---|---|
|
||
| `refreshing` | stale/present | background | 기존 content 유지 + subtle indicator | `refetchInFlight` — background refetch가 진행 중 |
|
||
| `stale-degraded` | cached | retry exhausted | stale label + manual retry | `staleFailure` latch set(직전 refetch가 재시도 소진/비재시도로 실패 & cached 존재) **AND** 현재 refetch in-flight 아님 |
|
||
| `mutation-pending` | current view | write in flight | 중복 action 차단 | mutation pending |
|
||
| `mutation-conflict` | authoritative refetch 필요 | stopped | conflict action | `CONFLICT`(409) normalized failure |
|
||
|
||
#### 1.1.1 read-overlay latch 전이 (`refreshing` ⊕ `stale-degraded`의 배타성 근거)
|
||
|
||
`stale-degraded`는 독립 flag가 아니라 **latch 1개 + in-flight 부정**의 파생값이다. read 축 전체를 `refetchInFlight`(현재 refetch 진행 여부)와 `staleFailure`(직전 refetch 실패가 아직 해소되지 않음) 두 신호로 계산한다:
|
||
|
||
```text
|
||
refreshing := refetchInFlight
|
||
stale-degraded := staleFailure && !refetchInFlight
|
||
```
|
||
|
||
`staleFailure` latch가 필요한 이유: hub §9.1은 `stale-degraded`의 진입 조건(retry exhausted)만 규정하고 **exit 조건을 규정하지 않는데**, hub §9.2의 cache default가 "refetch on focus = enabled for stale query"이므로 `stale-degraded` surface는 window focus만으로 자동 background refetch에 진입한다. latch 없이 `stale-degraded`를 "직전 실패 & cached"로만 정의하면 그 정상 경로에서 `refreshing`과 동시 성립해 §1.2 배타 불변식이 깨진다. 위 정의는 두 flag를 `refetchInFlight` 하나의 참/거짓으로 갈라 **정의상(구조적으로)** 배타로 만든다 — 런타임 검사에 의존하지 않으므로 focus refetch 경로에서 invariant가 throw되지 않는다.
|
||
|
||
| 전이 | 트리거 | latch 변화 | 결과 read overlay |
|
||
|---|---|---|---|
|
||
| `stale-degraded` → `refreshing` | 재refetch **진입** — window focus 자동 refetch(§9.2) 또는 stale label의 manual retry | `staleFailure` **유지**(clear하지 않음) | `refreshing` only |
|
||
| `refreshing` → ∅ | refetch **성공** | `staleFailure` clear | ∅ (base가 `success`/`empty`로 갱신) |
|
||
| `refreshing` → `stale-degraded` | refetch **실패** & cached 존재 | `staleFailure` set(유지) | `stale-degraded` only |
|
||
| `refreshing` → (base 전환) | refetch **실패** & cached 없음 | — | read overlay ∅ — §1.2에 따라 base = `terminal-error` |
|
||
| ∅ → `refreshing` | 최초 background refetch(직전 실패 없음) | 변화 없음(unset) | `refreshing` only |
|
||
|
||
manual retry와 focus 자동 refetch는 **같은 전이**를 쓴다(둘 다 refetch 진입). "manual retry는 foreground라 `refreshing`이 아니다"라는 구분은 두지 않는다 — 그 구분은 §9.1에 근거가 없고, focus refetch 경로가 자동이므로 배타성을 구제하지도 못한다.
|
||
|
||
#### 1.2 합법 조합 (base × overlay)
|
||
|
||
§9.1 Data 열에서 도출: 4개 overlay 모두 *이미 렌더 가능한 데이터가 존재함*을 전제(stale/present · cached · current view · authoritative refetch 필요)하므로, 데이터가 없는 base에는 붙을 수 없다.
|
||
|
||
| base | 허용 overlay | 근거 |
|
||
|---|---|---|
|
||
| `initial-loading` | 없음 (∅) | Data = none — 유지할 기존 content가 없어 "existing content 유지"·"current view"가 성립 불가 |
|
||
| `success` | 4종 모두 | Data = present |
|
||
| `empty` | 4종 모두 | Data = valid empty(유효한 데이터) — refetch·mutation 모두 성립 가능 |
|
||
| `terminal-error` | 없음 (∅) | Data = none/unusable, Activity = stopped — cached content가 남아 있다면 base는 `terminal-error`가 아니라 `success`/`empty` + `stale-degraded` |
|
||
|
||
Overlay 내부 상호배타(정의상 도출):
|
||
|
||
- `refreshing` ⊕ `stale-degraded` — §1.1.1 latch 정의(`stale-degraded := staleFailure && !refetchInFlight`)에서 **구조적으로** 도출. 직전 refetch 실패 후 focus 자동 refetch(§9.2)가 다시 걸리면 `stale-degraded → refreshing`으로 *전이*하며 동시 성립하지 않는다. 이 배타성은 런타임 assert가 아니라 파생식의 성질이다.
|
||
- `mutation-pending` ⊕ `mutation-conflict` — 전자는 "write in flight", 후자는 Activity "stopped". 동시 성립 불가.
|
||
|
||
query-less(mutation-only) surface 규칙: read query가 없는 surface(제출 전용 form 등)는 base를 `initial-loading`으로 두지 않는다. 읽을 원격 데이터가 없어 "first request 대기"가 성립하지 않고 렌더 가능한 form view가 항상 존재하므로 **base = `success` 고정**이며, write 축 overlay(`mutation-pending`/`mutation-conflict`)만 사용한다. read 축 overlay(`refreshing`/`stale-degraded`)는 성립하지 않는다. `deriveAsyncState`는 `queryResult`가 `undefined`일 때 이 규칙을 적용한다.
|
||
|
||
→ 따라서 동시 성립하는 overlay는 최대 2개(read 축 1 + write 축 1)이며, 전체 합법 조합 수는 `initial-loading`(1) + `terminal-error`(1) + (`success`·`empty`) × 3(read: none/refreshing/stale-degraded) × 3(write: none/pending/conflict) = 20이다.
|
||
|
||
#### 1.3 동시 성립 시 우선순위 (indicator precedence)
|
||
|
||
read overlay와 write overlay는 서로 다른 affordance를 점유하므로(read = content 영역 indicator/stale label, write = action 영역 차단/conflict action) **기본은 동시 렌더**다. 단일 슬롯(예: surface 헤더의 status indicator 1칸)만 있는 경우에만 다음 순서로 하나를 고른다:
|
||
|
||
```text
|
||
mutation-conflict > mutation-pending > stale-degraded > refreshing
|
||
```
|
||
|
||
원칙: 사용자 조치를 요구하며 activity가 stopped인 것 → 사용자 조작을 차단하는 것 → 수동 retry를 요구하는 것 → 조용한 배경 신호. `deriveAsyncState`는 이 우선순위를 *렌더 힌트*(`overlay.primary`)로만 계산하고, overlay flag 자체는 절대 삭제하지 않는다(삭제하면 §9.1 요구가 유실됨).
|
||
|
||
### 2. Server-signal → state 파생 (consume, not define)
|
||
|
||
> **Trace**: D4·D8 + `FE-OC-011` → `FE-OC-012` 소비. TanStack Query query/mutation 신호를 §1의 2축 상태(`{base, overlay}`)로 파생하는 순수 selector를 application/adapter 경계에 둔다. query 신호는 base + read overlay를, mutation 신호는 write overlay를 결정하며, 두 축은 독립적으로 계산된 뒤 §1.2 합법 조합표로 검증된다. presentation은 결과 view-model만 받는다.
|
||
>
|
||
> - **UNSUPPORTED_IMPL_DECISION**: query `{status, fetchStatus, data, isPlaceholderData}` 및 mutation `{status}` 튜플 → `{base, overlay}`의 구체 매핑표와 selector signature(`deriveAsyncState(queryResult, mutationResult, { isValidEmpty, staleFailure })`) — hub는 state 집합만 정의하고 TanStack 필드→state 매핑은 규정하지 않음. signature는 §1.1.1의 `staleFailure` latch를 명시 입력으로 받고(selector를 순수 함수로 유지), `queryResult`가 `undefined`이면 §1.2 query-less 규칙(base = `success`, write 축 overlay만)을 적용한다. Trade-off: 파생을 경계에 두어 presentation을 framework-neutral로 유지(§4.3), alt = page-local 파생은 dependency rule 위반이라 기각.
|
||
> - **R3(위임)**: `QueryCachePort`가 노출하는 실제 신호 형태·query key·invalidation은 `feature-server-state-caching-contract`(`FE-OC-012`)가 소유한다. 본 절은 그 신호를 *소비*하는 매핑만 명세하며, port 신호 shape이 확정되면 매핑표를 fix한다.
|
||
|
||
### 3. error-표기 state 렌더 계약
|
||
|
||
> **Trace**: D5 + `FE-OC-011` ← `FE-OC-008` 소비. error를 표기하는 state(`terminal-error`, `stale-degraded`, `mutation-conflict`)는 normalized failure의 safe 필드만 사용한다.
|
||
>
|
||
> - **UNSUPPORTED_IMPL_DECISION**: `action`(6-closed: `retry`/`reauth`/`navigate`/`reload-once`/`contact-support`/`none`) → 구체 버튼/handler 컴포넌트(`AsyncErrorSurface`) 매핑, `userMessageKey` → copy 카탈로그 lookup — hub §8.4는 action 어휘와 allowed-when/MUST-NOT만 규정하고 컴포넌트/카피 구현은 규정 안 함. Trade-off: action별 단일 presentational 컴포넌트로 고정해 테스트 대상을 좁힘; copy 카탈로그(i18n)는 본 브랜치 밖.
|
||
> - **R3(위임)**: `kind → action`·`kind → userMessageKey` 매핑 계약은 `feature-frontend-error-classification-boundary-contract`(`FE-OC-008`)가 소유(그 브랜치 D6이 "action의 실제 UI 실행은 async-ui가 소유"라고 위임). 본 절은 소비/렌더만.
|
||
|
||
렌더 불변식: raw response body·token·authorization header·full URL/query·stack·storage value를 error state UI에 노출하지 않는다(§8.1).
|
||
|
||
### 4. Non-throw 규율 + async→render boundary handoff
|
||
|
||
> **Trace**: D6 + `FE-OC-011` → `FE-OC-015` 기여. async surface는 normalized operational failure를 반드시 state로 반환하고 render boundary로 throw하지 않는다.
|
||
>
|
||
> - **UNSUPPORTED_IMPL_DECISION**: "state로 반환됐고 throw되지 않았음"을 강제하는 test 어서션 형태(예: failure 주입 후 nearest error boundary 미발동 assert) — hub는 원칙만 규정. Trade-off: integration test에서 boundary render 여부로 검증(별도 boundary mock 대신 실제 boundary 미발동 관찰).
|
||
> - **R3(위임)**: boundary 배치·소유권(boot/route/feature/async boundary)·reload loop 방지(§10.2)는 `feature-frontend-render-recovery-boundary-contract`(`FE-OC-015`)가 소유. 본 절은 async surface가 그 boundary를 발동시키지 않는다는 계약만 제공.
|
||
|
||
### 5. Component state matrix tests (measurable completion)
|
||
|
||
> **Trace**: D7·D8 + `FE-OC-011` → `FE-OC-020` 기여. base 4종과 overlay 4종을 각각 결정론적으로 재현하는 component fixture(8종) + overlay 동시 성립 우선순위 fixture(2종) + §1.1.1 read-overlay latch 전이 fixture(1종)를 작성하고 `pnpm test:component` gate(`artifacts/tests/component.xml`)에 편입.
|
||
>
|
||
> - **UNSUPPORTED_IMPL_DECISION**: fixture 파일명·경로(`tests/component/async-surface.state-matrix.test.jsx`)·RTL query 전략(role/label 기준) — hub §20은 "state matrix component tests" 결과만 요구하고 파일 배치·query 전략은 규정 안 함. Trade-off: base/overlay당 최소 1 fixture로 1:1 addressable하게 배치하고, §1.2의 20개 합법 조합 전수 대신 축별 1개 + 교차 2개 + latch 전이 1개로 축소(전수는 fixture 유지비가 계약 가치를 넘어섬). latch 전이만 예외적으로 fixture를 추가한 이유는 그것이 정적 조합이 아니라 §9.2 focus refetch가 발동시키는 *시간 축* 경로여서 정적 조합 fixture로는 재현되지 않기 때문.
|
||
> - **R3(위임)**: Vitest/RTL/MSW 하네스 구성·gate 분리·artifact 규약은 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] `FE-D022`가 규정하고, 구현 소유자는 [[raw/branch-notes/feature-frontend-test-taxonomy-contract]](`FE-OC-020`)다. 본 절은 async fixture 목록(11종)과 각 fixture의 기대 `{base, overlay}`만 확정.
|
||
|
||
| fixture | 주입 조건 | 기대 base | 기대 overlay |
|
||
|---|---|---|---|
|
||
| initial-loading | pending & no cache | `initial-loading` | ∅ |
|
||
| success | success & non-empty | `success` | ∅ |
|
||
| empty | success & valid empty payload | `empty` | ∅ |
|
||
| terminal-error | normalized failure(비재시도/재시도 소진) & cache 없음 | `terminal-error` | ∅ |
|
||
| refreshing | success & background refetch in flight | `success` | `refreshing` |
|
||
| stale-degraded | refetch 실패 & cached 존재 | `success` | `stale-degraded` |
|
||
| mutation-pending | success & mutation in flight | `success` | `mutation-pending` |
|
||
| mutation-conflict | success & `CONFLICT`(409) normalized failure | `success` | `mutation-conflict` |
|
||
| overlay-cross | refetch in flight + mutation in flight 동시 | `success` | `refreshing` + `mutation-pending`(단일 슬롯 = `mutation-pending`) |
|
||
| overlay-precedence | stale-degraded + mutation-conflict 동시 | `success` | `stale-degraded` + `mutation-conflict`(단일 슬롯 = `mutation-conflict`) |
|
||
| stale-degraded → 재refetch | `stale-degraded` 상태에서 window focus 자동 refetch 진입(§9.2) — 이어서 (a) 성공 / (b) 실패 & cached 존재 | 진입 중 `success` → (a) `success` / (b) `success` | 진입 중 `refreshing` **only**(`stale-degraded` false, `staleFailure` latch는 유지) → (a) ∅ / (b) `stale-degraded` only. 세 시점 모두 두 flag 동시 true 아님을 assert(§1.1.1 latch 전이) |
|
||
|
||
## 엣지·실패·의존
|
||
|
||
- **실패·엣지 경로**:
|
||
- `empty` vs `terminal-error` 오분류: valid empty payload를 error로 렌더하면 안 됨 → per-operation `isValidEmpty` predicate 필요(§9.1 empty = "valid empty").
|
||
- `initial-loading`: skeleton 안정성 유지 + focus theft 금지(§9.1). live region 반복 announcement 억제는 a11y 브랜치 위임.
|
||
- `refreshing` 중 background refetch 실패 → `stale-degraded`로 전이 + stale label + manual retry(§9.1), 기존 content 유지(§1.1.1 전이표).
|
||
- `stale-degraded` surface가 window focus를 되찾아 자동 refetch(§9.2 "refetch on focus = enabled for stale query")에 진입 → `staleFailure` latch는 유지한 채 `stale-degraded → refreshing`으로 전이한다. 두 flag가 동시에 true가 되지 않으므로 §1.2 read 축 배타 불변식은 이 정상 경로에서 깨지지 않는다(§1.1.1).
|
||
- `mutation-pending` 중 중복 submit → duplicate action 차단(§9.1).
|
||
- `mutation-conflict`(409) → authoritative refetch를 요구하는 conflict action(§8.2 `CONFLICT` row).
|
||
- async surface가 normalized failure를 못 만든 채 throw되는 경로: 이는 error-classification total-function(§8.2) 위반이며, 만약 새면 render boundary(`FE-OC-015`)가 최후로 catch — async surface는 이를 유발하지 않아야 함.
|
||
- **다른 계약 의존**:
|
||
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] `FE-OC-008` — normalized failure(`userMessageKey`/`action`/safe 필드)를 consume. 그 계약이 바뀌면 error 표기 state 렌더가 영향.
|
||
- [[raw/branch-notes/feature-server-state-caching-contract]] `FE-OC-012` — `QueryCachePort`의 query/mutation 상태 신호를 consume해 §1의 2축 `{base, overlay}`를 파생. port 신호 shape 변경 시 매핑 재조정. 특히 cache default "refetch on focus = enabled for stale query"(hub §9.2)가 §1.1.1 read-overlay latch 전이(`stale-degraded → refreshing`)를 발동시키는 경로이므로, focus refetch를 opt-out하는 surface는 그 전이가 manual retry로만 일어난다.
|
||
- [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] `FE-OC-015` — boundary ownership에 의존. async surface는 throw하지 않는다는 계약을 제공하고 boundary 배치는 위임.
|
||
- [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] `FE-OC-020` — component test 하네스·gate를 consume해 matrix fixture를 편입.
|
||
- [[raw/branch-notes/feature-sample-feature-slice-contract-fixture]] `FE-OC-024` — sample slice가 async surface를 fixture로 exercise(sample removal smoke 대상).
|
||
- [[raw/branch-notes/feature-accessibility-baseline-contract]] — loading/error live region·focus(§10.3)를 소유. async state는 a11y hook point만 노출.
|
||
- [[raw/branch-notes/feature-tailwind-design-token-styling-contract]] `FE-OC-011` — 본 브랜치가 요구하는 시각 primitive(안정적 skeleton, `refreshing`의 subtle indicator, `stale-degraded`의 stale label, 단일 슬롯 status indicator)의 token-driven 어휘를 소유하는 co-tenant(hub Decision Register `FE-D005`가 `FE-OC-011`에 영향). 본 브랜치는 *어떤 state가 존재하고 언제 성립하는지*를 소유하고, 그 state의 시각 표현 어휘는 위임한다. 해당 브랜치 D4가 동일 경계를 반대편에서 명시("state machine·required-state는 위임"). primitive 어휘가 §1.2 조합표의 동시 렌더(read overlay + write overlay)를 표현하지 못하면 §1.3 단일 슬롯 fallback으로 축약된다.
|
||
|
||
## 검증해야 할 주장
|
||
|
||
| Claim | Why uncertain | How to verify | Status |
|
||
|---|---|---|---|
|
||
| component matrix가 base 4 + overlay 4 + 교차 2 + latch 전이 1을 결정론적으로 재현한다 | 코드·fixture가 아직 없음 | `pnpm test:component` async state matrix fixtures(`artifacts/tests/component.xml`) — base/overlay당 최소 1 fixture + `stale-degraded → 재refetch` 전이 fixture exit 0 | `needs-confirmation` |
|
||
| `stale-degraded` 상태에서 focus 자동 refetch(§9.2)가 걸려도 `refreshing`·`stale-degraded`가 동시 true가 되지 않는다 | hub §9.1이 `stale-degraded`의 exit 조건을 규정하지 않아 latch 정의(§1.1.1)는 본 브랜치의 해석 | `deriveAsyncState` unit test — `staleFailure` latch set 상태에서 `refetchInFlight` true/false를 토글하며 두 flag의 동시 true 부재 assert + §5 `stale-degraded → 재refetch` fixture | `needs-confirmation` |
|
||
| server 신호(status/fetchStatus/data + mutation status) → `{base, overlay}` 파생이 gap 없이 exhaustive하다 | hub가 매핑표를 규정하지 않아 잠정 | `deriveAsyncState` selector unit test(모든 튜플 조합 → base 정확히 1개 + overlay flag 집합이 §1.2 합법 조합에 속함) | `needs-confirmation` |
|
||
| §1.2 합법 조합표가 실제 surface에서 위반되지 않는다(예: `terminal-error` + `refreshing` 동시 방출 없음) | 조합표는 §9.1 Data 열에서 도출한 해석이며 hub 명시 표가 아님 | `deriveAsyncState` invariant test — 불법 조합 방출 시 throw(render defect로 취급, D6의 "invariant breach" 경로). **단 read 축 배타(`refreshing` ⊕ `stale-degraded`)는 §1.1.1 파생식의 성질이라 런타임 throw 대상이 아니다** — throw가 걸리는 것은 base×overlay 조합(데이터 없는 base에 overlay 부착) 위반뿐이며, read 축은 `stale-degraded := staleFailure && !refetchInFlight`가 성립하는지 unit test로 확인한다 | `needs-confirmation` |
|
||
| error 표기 state가 raw body/stack/token을 노출하지 않는다 | 렌더 경로가 미구현 | negative test — 직렬화 후 금지 필드 부재 assert(§8.1, error-classification D2 패턴 mirror) | `needs-confirmation` |
|
||
| async surface가 operational failure에 render boundary로 throw하지 않는다 | boundary 계약·구현 미확정 | integration test — failure 주입 후 nearest error boundary 미발동 assert(§10.1) | `needs-confirmation` |
|
||
| `isValidEmpty` predicate가 valid-empty를 error로 오분류하지 않는다 | per-operation empty 판별자가 미정 | component fixture(empty payload) → `empty` state assert | `planned` |
|
||
| React 컴포넌트 트리가 nesting/props 단방향 흐름을 준수한다 | react-ui doc Usage Boundaries가 로컬 검증 요구 | 구현 후 architecture lint(`FE-OC-002` dependency-cruiser/ESLint) | `planned` |
|
||
|
||
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
|
||
|
||
| 관심사 | 상태 | owner | 심각도 | 근거 |
|
||
|---|---|---|---|---|
|
||
|
||
## 마주친 문제
|
||
|
||
- 없음 — scaffolding 단계
|
||
|
||
## 묶음 (이 branch에서 파생된 자료)
|
||
|
||
<!-- GENERATED: project-contract-imports:start -->
|
||
## 가져온 프로젝트 계약
|
||
|
||
| Ref | Owner | 요약 | Branch 적용 |
|
||
|---|---|---|---|
|
||
| `FE-OC-002@1` | [[raw/branch-notes/feature-frontend-clean-architecture-layering-contract]] | `domain <- application <- presentation` 의존 방향과 application-owned output port를 MUST 지킴 | import 참조로 적용 |
|
||
| `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-012@1` | [[raw/branch-notes/feature-server-state-caching-contract]] | query key와 invalidation은 registry factory만 MUST 사용 | import 참조로 적용 |
|
||
| `FE-OC-015@1` | [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] | expected operational error와 render defect를 MUST 분리하고 reload loop를 금지 | import 참조로 적용 |
|
||
| `FE-OC-020@1` | [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] | gate 종류별 책임·fixture·artifact를 분리하고 실패를 warning으로 낮추면 안 됨 | import 참조로 적용 |
|
||
<!-- GENERATED: project-contract-imports:end -->
|
||
|
||
### Sub-branches (세부 작업)
|
||
|
||
- 없음 — scaffolding 단계
|
||
|
||
### 오류 기록 (이 branch 작업 중 발생)
|
||
|
||
- 없음 — scaffolding 단계
|
||
|
||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||
|
||
- 없음 — scaffolding 단계
|
||
|
||
### 강의 (이 작업을 위해 학습한 강의)
|
||
|
||
- 없음 — scaffolding 단계
|
||
|
||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||
|
||
- 없음 — scaffolding 단계
|
||
|
||
## 관련 일일 노트
|
||
|
||
- 없음 — scaffolding 단계
|
||
|
||
## 완료 후 정리
|
||
|
||
- PR 링크: TODO
|
||
- 리뷰 메모: TODO
|
||
- 머지 결과 / 배포 환경: TODO
|
||
- **wiki 추출 대상**: 없음 — scaffolding 단계
|
||
- **추출하지 않을 항목**: 없음 — scaffolding 단계
|