Files
llm-wiki/raw/branch-notes/feature-async-ui-state-contract.md
T

43 KiB
Raw Blame History

title, source_type, status, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256, imports, delegates
title source_type status branch parent_branch related_projects governing_docs tags created target_merge status_label id kind project work_item inherits refines overrides depends_on contract_packet contract_packet_sha256 imports delegates
branch / feature-async-ui-state-contract branch-note raw feature-async-ui-state-contract
ca-skeleton-frontend
ca-skeleton
raw/project-notes/ca-skeleton-frontend-operational-contract.md
branch
ca-skeleton
frontend
application
react
error-handling
2026-07-18 in-progress BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-013 project-work-item ca-skeleton-frontend-operational-contract WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-013
DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-UI-001@1
DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SERVER-STATE-001@1
WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-007
WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-010
1 106ffdb88a0c1937bbc4d9387b99c06b90050d7e375ab4e29c7ed2b1a0dfe570
FE-OC-002@1
FE-OC-008@1
FE-OC-012@1
FE-OC-015@1
FE-OC-020@1
DELEG-FE-006@1

branch: feature-async-ui-state-contract

Layer: raw/branch-notes/ — 단일 브랜치의 TODO·결정·진행 기록. 현재는 scaffolding 단계다.

부모 (필수)

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: required와 non-blocking state matrix component test가 통과한다

상속한 프로젝트 결정

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

브랜치 지역 결정

기존 branch-local 결정은 아래 ## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.

Decision ID Decision Relation Supporting Claims Status

선언한 예외

Override ID Overrides Reason Approval Status

목표

이 브랜치는 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: (없음)

범위

포함 범위

  • 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-011FE-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-011FE-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-011FE-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-011FE-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.jsisValidEmpty(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 전이 (refreshingstale-degraded의 배타성 근거)

stale-degraded는 독립 flag가 아니라 latch 1개 + in-flight 부정의 파생값이다. read 축 전체를 refetchInFlight(현재 refetch 진행 여부)와 staleFailure(직전 refetch 실패가 아직 해소되지 않음) 두 신호로 계산한다:

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-degradedrefreshing 재refetch 진입 — window focus 자동 refetch(§9.2) 또는 stale label의 manual retry staleFailure 유지(clear하지 않음) refreshing only
refreshing → ∅ refetch 성공 staleFailure clear ∅ (base가 success/empty로 갱신)
refreshingstale-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 내부 상호배타(정의상 도출):

  • refreshingstale-degraded — §1.1.1 latch 정의(stale-degraded := staleFailure && !refetchInFlight)에서 구조적으로 도출. 직전 refetch 실패 후 focus 자동 refetch(§9.2)가 다시 걸리면 stale-degraded → refreshing으로 전이하며 동시 성립하지 않는다. 이 배타성은 런타임 assert가 아니라 파생식의 성질이다.
  • mutation-pendingmutation-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)는 성립하지 않는다. deriveAsyncStatequeryResultundefined일 때 이 규칙을 적용한다.

→ 따라서 동시 성립하는 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칸)만 있는 경우에만 다음 순서로 하나를 고른다:

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-011FE-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를 순수 함수로 유지), queryResultundefined이면 §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-011FE-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-011FE-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-011FE-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]다. 본 절은 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는 이를 유발하지 않아야 함.
  • 다른 계약 의존:

검증해야 할 주장

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 축 배타(refreshingstale-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에서 파생된 자료)

가져온 프로젝트 계약

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 참조로 적용

Sub-branches (세부 작업)

  • 없음 — scaffolding 단계

오류 기록 (이 branch 작업 중 발생)

  • 없음 — scaffolding 단계

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

  • 없음 — scaffolding 단계

강의 (이 작업을 위해 학습한 강의)

  • 없음 — scaffolding 단계

job-posting tie-ins (이 작업에서 파생된 글감)

  • 없음 — scaffolding 단계

관련 일일 노트

  • 없음 — scaffolding 단계

완료 후 정리

  • PR 링크: TODO
  • 리뷰 메모: TODO
  • 머지 결과 / 배포 환경: TODO
  • wiki 추출 대상: 없음 — scaffolding 단계
  • 추출하지 않을 항목: 없음 — scaffolding 단계