43 KiB
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 |
|
|
|
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 |
|
|
1 | 106ffdb88a0c1937bbc4d9387b99c06b90050d7e375ab4e29c7ed2b1a0dfe570 |
|
|
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).- 단일
loadingboolean 금지 규칙의 코드 표현(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-errorstate가 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:componentasync 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축) +
deriveAsyncStateselector 계약 정의 — 등급:planned - base × overlay 합법 조합표(§1.2, 20조합) + 단일 슬롯 indicator 우선순위(§1.3) 확정 — 등급:
planned - server query/mutation 신호 →
{base, overlay}파생 매핑표 확정(server-state 계약 fix 후) — 등급:planned terminal-error렌더 컴포넌트(userMessageKey+actiononly, 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-specself-map으로 hubFE-OC-011owner scope에서 도출. frontend 코드는 아직 없음 → 전부plannedblueprint.
결정 사항
아래 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이 금지하는 것은 "loadingboolean 하나로 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+ closedaction만 렌더하고 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 목록·기대치만 확정 |
구현 가이드
전부
plannedblueprint. 경로는 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으로 두어 exhaustiveswitch+ RTL fixture addressability를 유지하고, overlay는 flag 집합으로 두어 동시 성립을 손실 없이 표현. hub §9.1이 실제로 금지하는 문장은 "loadingboolean 하나로 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
staleFailurelatch — 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 실패가 아직 해소되지 않음) 두 신호로 계산한다:
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칸)만 있는 경우에만 다음 순서로 하나를 고른다:
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의staleFailurelatch를 명시 입력으로 받고(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:componentgate(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 전이) |
엣지·실패·의존
- 실패·엣지 경로:
emptyvsterminal-error오분류: valid empty payload를 error로 렌더하면 안 됨 → per-operationisValidEmptypredicate 필요(§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-degradedsurface가 window focus를 되찾아 자동 refetch(§9.2 "refetch on focus = enabled for stale query")에 진입 →staleFailurelatch는 유지한 채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.2CONFLICTrow).- 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 RegisterFE-D005가FE-OC-011에 영향). 본 브랜치는 어떤 state가 존재하고 언제 성립하는지를 소유하고, 그 state의 시각 표현 어휘는 위임한다. 해당 브랜치 D4가 동일 경계를 반대편에서 명시("state machine·required-state는 위임"). primitive 어휘가 §1.2 조합표의 동시 렌더(read overlay + write overlay)를 표현하지 못하면 §1.3 단일 슬롯 fallback으로 축약된다.
- raw/branch-notes/feature-frontend-error-classification-boundary-contract
검증해야 할 주장
| 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에서 파생된 자료)
가져온 프로젝트 계약
| 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 단계