입력 반응과 완성 데이터 공개 계약
이슈 #1592, 2026-09-14 사용자 결정. 적용 대상은 Barbelic 모바일·PC의 기존 앱 전체와 이후 추가하는 화면이다. 관리자·랜딩의 별도 개편, 통계 계산 의미 변경, 사용자 원본 수리는 이 트랙 범위가 아니다.
최종 목표
사용자가 버튼을 누르면 데이터·코드 준비 여부와 관계없이 즉시 선택·화면 열림·작업 진행 상태가 보인다. 필요한 본문만 스켈레톤으로 기다리고, 현재 사용자·선택·저장 결과에 맞는 필수 조회와 계산이 모두 반영된 완성본을 적용할 때 함께 공개한다. 기다리는 중에도 이동·닫기·선택 변경이 가능하며, 실제 완료 시간도 아래 성능 기준을 충족한다. 이 원칙은 새 화면의 기본 구현·검증 계약이다.
일부 화면의 성공이나 공통 함수의 존재는 전체 달성이 아니다. 입력 경로 조사표, 공개 영역 조사표의 적용·동작 검증·성능·반영 상태를 최종 목표와 대조한다. 조사표는 기준 코드의 조사 기록이며 통과 증거가 아니다. 행별 적용 경계와 검증 증거의 현재 상태는 60행 적용·검증 상태표에 있다.
입력·준비·공개의 책임
| 경계 | 책임 | 완료 조건 |
|---|---|---|
| 입력 | 기존 navigation/entry intent에 대상과 작업 수명을 동기 기록하고 셸·선택·진행 표시를 연다 | 데이터를 보류해도 다음 화면 반응이 존재한다. 단순 눌림 색만으로 목적 화면 열림을 대체하지 않는다 |
| 준비 | 키·요청 수명별 필수 자원 조회, 파생 계산, 저장 결과 반영을 수행한다 | 현재 owner·기간·필터·기준일·요구 버전과 일치하고, 필수 의존성 중 미완료·오류가 없다 |
| 공개 | 준비된 같은 의미의 묶음을 한 React commit에서 적용한다 | 스켈레톤을 걷는 commit에서 값·합계·차트·목록·설명이 서로 일치한다 |
준비 작업을 시작하기 전에 입력 상태를 기록한다. 이후 오래 걸리는 동기 계산은 측정 후 작은 단위·기존 worker·필요 범위 계산으로 줄인다. startTransition이나 Suspense만으로 event/effect 안의 조회 완료나 메인 스레드 양보가 보장됐다고 간주하지 않는다.
입력 수명은 기존 owner·navigation/entry intent와 공유한다. 닫기·다른 선택·로그아웃은 이전 결과의 공개 권한을 없앤다. 늦은 응답은 새 화면을 다시 열거나 선택을 덮어쓰지 않는다. 쓰기 취소와 화면 이탈은 다르다. 이미 기기에 보존한 저장 요청·초안의 수명과 멱등 키는 기존 쓰기 계약대로 유지한다.
공통 공개 판정
- 공개 상태는
pending | ready | error로 전달한다.idle,loading,refreshing,stale,invalidated, 취소로 미완성인 상태를 기본 분기의ready로 바꾸지 않는다. resource에 값이 남아 있는 것과 공개 가능한 것은 별개다. - 유효한 완성 캐시는 같은 조건의 재방문 때 즉시 재사용한다. 무효화·요구 버전 증가·선택/계정 변경 순간부터 해당 본문은 공개하지 않는다. 새 요청 시작이나 effect를 기다리지 않는다.
- 실패는 오류·재시도·닫기가 있는 상태로 끝낸다. 실패를 빈 결과/0/성공으로 바꾸거나,
data === null만으로 끝없는 스켈레톤을 표시하지 않는다. 재시도는 실제 새 요청 상태부터 pending이다. - 빈 결과는 현재 요청이 성공하고 전체 결과가 비어 있을 때만 ready로 공개한다. 페이지 추가는 이미 완료된 페이지를 보존하고 새 페이지 영역만 기다린다. 첫 페이지의 필수 무효화는 이 예외에 해당하지 않는다.
- 공개 묶음에 무관한 탭·부가 영역을 포함하지 않는다. 홈 날짜, 프로필 기본 신원·연결 계정, 그룹 보드 내용·완료 기록처럼 독립적으로 성립하는 영역은 별도 경계로 둔다. 같은 합계를 설명하는 숫자·차트·목록은 한 묶음이다.
- 스켈레톤은 실제 레이아웃 크기를 보존하고 숫자·텍스트가 DOM/접근성 트리에 미리 노출되지 않게 한다. 제목·선택기·뒤로가기·닫기는 계속 사용할 수 있다. 로딩 완료를 늦추는 최소 표시 시간은 두지 않는다.
통계와 리포트의 추가 조건
통계는 현재 snapshot의 유효성과 서버 freshness를 함께 검사한다. 과거 응답의 stale=false가 무효화된 캐시를 다시 유효하게 만들 수 없다. applied 버전은 알려진 요청 버전·저장 영수증 요구 버전 이상이어야 한다. 조합하는 응답들의 기준일·owner·선택 범위와 적용 세대도 일치해야 한다.
리포트 상단 총 볼륨·운동일·운동 개수는 이 조건을 만족하는 개요와 선택 기간 상세를 모아 함께 공개한다. 기간 상세에는 같은 DB 읽기 스냅샷의 freshness 증거가 필요하다. 서로 다른 시점에 받은 응답이라는 이유만으로 일치한다고 가정하지 않는다. 필수 분포·집계가 미완성인 응답도 완성본으로 공개하지 않는다. 계정/기간/필터/기준일/버전 변경은 이전 기간 상세의 공개 권한을 없앤다.
현재 결함 재현: main 1711ae2e84612d8af868670a65ef3b0c48957817의 실제 resource→상태 변환→리포트 hook→Chromium DOM 경로에서 invalidated → stale → ready가 되고, 요청 시작 전 26,000kg와 숫자 스켈레톤 0개가 보였다. 서버 응답은 제어한 합성 데이터다. 이는 원인과 로컬 재현 증거이며 개별 사용자 관측 세션의 발동 사건이나 Production 통합 검증을 뜻하지 않는다.
성능 합격 기준
아래는 구현 결과를 보기 전에 고정한 이 트랙의 최종 기술 합격선이다. 과거 예시 수치나 기존 비회귀 예산을 최종 체감 목표로 재명명한 것이 아니다. 넘으면 원인을 수리하며, 측정하지 못하면 미검증으로 남긴다.
| 지표 | 최종 합격선 | 범위·근거 |
|---|---|---|
| 입력→의미 있는 첫 시각 반응 | p95 ≤ 100ms, 입력 처리 동기 작업 ≤ 50ms | 모바일·PC, 코드/데이터 응답 보류 중에도 성립. RAIL의 입력 응답 기준을 제품 목표에 채택 |
| 유효 완성 캐시 재방문 | p95 ≤ 100ms, 불필요한 재조회·본문 재마운트·스켈레톤 0회 | 동일 owner/선택/기준일/버전이고 무효화되지 않은 경우 |
| 마지막 필수 데이터 수신→완성본 공개 | p95 ≤ 100ms | 파생 계산과 commit을 포함. 인위적 대기 없음 |
| 건강한 연결의 새 조회/필수 갱신→완성본 공개 | 1년·4년 표준 fixture p95 ≤ 2,000ms, 10년 fixture p95 ≤ 5,000ms | 입력 반응과 별도 판정. 10년 개요의 기존 서버 비회귀 상한 4,035ms와 데이터 전송·조합 여유를 포함하되, 그 상한만 통과해서는 UI 합격이 아님 |
| 기존 성능·자원 예산 | 릴리스 예산 및 화면 RPC 행/바이트 상한 모두 유지 | DOM·청크·검색 인덱스·조회 횟수/중복·서버 읽기 증가를 함께 기록 |
기준 환경은 기존 U05/R04와 같은 production 빌드, Chromium, CPU 4배 스로틀, 모바일 390×844·PC 1440×1000, 격리 로컬 DB의 1/4/10년 fixture다. 건강한 연결은 오프라인·패킷 손실 없이 추가 왕복 지연 100ms를 준 조건으로 기록한다. 캐시 cold/warm/invalidated를 따로 측정한다. 대표 경로별 20회씩 두 묶음으로 p95와 최댓값을 남기고, 호스트·브라우저·SHA·fixture·네트워크 조건을 함께 저장한다. 기능상 지연·실패·경합 검사는 제어 응답으로 별도 수행하며 이를 실제 서버 완료 시간으로 보고하지 않는다.
오프라인·실패·의도적으로 보류한 요청에서 성공 시간 목표를 주장하지 않는다. 실제 요청 timeout/오류로 복구 상태가 끝나는지, 대기 중 입력과 닫기가 가능한지 검사한다. 긴 통계 재계산은 정상 완료 시간에서 숨기지 않고 별도 측정한다. 오류를 빨리 표시하거나 스켈레톤만 빨리 띄운 것으로 실제 대기 시간 개선을 통과시키지 않는다.
구현 선택과 기각한 대안
기존 resource store·owner fence·entry intent·화면 스택을 유지하고, 공통 준비 판정을 store→binding→UI까지 연결한다. 조회는 독립 의존성을 병렬로 실행하고 완성 조건만 조합한다. 저장·계산의 본래 의미는 보존한다.
고정 타이머로 스켈레톤을 숨기는 방식은 느린 응답에서 조기 공개하고 빠른 응답에서는 지연을 만든다. 캐시를 전부 삭제하는 방식은 유효 재방문과 복구를 망친다. 모든 화면을 하나의 전역 로딩으로 묶는 방식은 무관한 영역까지 막는다. 버튼 색만 바꾸는 방식은 목표 화면이 늦게 열리는 문제를 남긴다. 이 네 방식은 최종 조건을 충족하지 않아 채택하지 않는다.
검증·개발 완료 조건
새 화면과 기존 수정은 입력 경계·공개 단위·필수 의존성·요청 식별/취소·실패·유효 캐시 조건을 선언한다. 상태를 테스트가 ready로 만들어 공급하는 것만으로 검증을 끝내지 않는다. 실제 store/controller/binding/UI를 연결하고 외부 응답 순서만 제어한다.
필수 행동은 cold, warm, invalidation 후 새 요청 전, 필수 응답 하나 대기, 빈 성공, 실패/재시도, A→B→A 연속 입력, 닫은 뒤 늦은 응답, owner 전환, 저장 후 요구 세대 갱신이다.
새 화면 게이트(QA1 suite/tests/react/publicationBoundaryGate.test.mjs, 앱 npm run check의 단위 묶음에 포함): src/react/ui/{mobile,desktop}/screens/*.tsx의 모든 화면 파일은 ① 공통 경계(DataPublicationBoundary·StatDataRegion·DataRegionSkeleton·StatValueStatusContext)를 쓰거나 ② 계약 상태 prop(publicationStatus/dataStatus/*Status)을 받아 자기 영역의 pending·error를 그리거나 ③ 서버 데이터 영역이 없는 파일로 검사 안 목록에 이유와 함께 등록돼야 한다. 셋 다 아니면 검사가 실패한다. 같은 검사가 준비 상태 타입이 pending | ready | error 한 곳(src/react/contracts/dataPublication.ts)에서만 정의되고 stale/refreshing이 ready로 바뀌지 않으며, 공통 경계가 타이머 없이 보존 본문을 숨기는지(inert·visibility: hidden·aria-hidden) 함께 고정한다. 리포트 세 숫자는 각각 고정 기대값과 DOM 관측으로 함께 공개되는지 검사한다. 로컬 단위/브라우저/관련 DB 검사와 성능 증거를 대상표 각 행에 연결한다.
성능 회귀 게이트(QA1 suite/tests/react/responsivenessPhase4Gate.test.mjs·reportDetailMerge.test.mjs, 2026-09-14 Phase 4 계측에서 확정): 4배 CPU·RTT +100ms 계측이 예산을 넘긴 자리의 수리 구조를 소스 핀과 계약 검사로 고정한다. 새 코드는 같은 규칙을 따른다. ① 마지막 필수 응답의 병합은 증분이다 — 응답 하나가 도착했다고 화면의 투영 전체를 다시 계산하거나 화면 전체를 다시 그리지 않는다(리포트 applyReportDesignDetail: 결과는 처음부터 넣고 만든 투영과 같고 입력을 바꾸지 않는다). ② 앱 루트 상태를 바꾸는 입력은 화면 로컬 상태로 먼저 반응한다 — 루트 상태(표시 달·선택일·탭)의 갱신은 transition 또는 지연 값(useDeferredValue)으로 뒤따르고, 강조·제목·격자 같은 첫 반응은 클릭 처리 안에서 끝난다(모바일 탭바 #1152, 모바일 월 이동, PC 탭 판). ③ 합계 영역의 공개 상태는 그 값이 걸친 원천 전부의 상태를 합친 것이다 — 표시 단위(달)만 준비됐다고 그 단위를 넘는 합계(달 경계 주)를 공개하지 않는다(combineDataPublication). 계측 절차와 도구 원본은 작업 기록의 evidence/1592-phase4에 있고, 합격선은 검증 기록 Phase 1 표를 바꾸지 않는다.
그리기 비용 게이트(#1596, QA1 suite/tests/react/derivedIdentity.test.mjs·exerciseSynonymMatcherLazy.test.mjs·reportSectionIdentity.test.mjs·renderStructureGate.test.mjs, 앱 npm run check의 단위 묶음에 포함): 이 계약이 "무엇을 언제 보여줄지"를 정한 위에 그리기 비용 계약은 "상태가 바뀌면 누가 얼마나 다시 계산·그려지는가"를 정한다. 새 코드는 세 규칙을 따른다. ① 파생 데이터는 원천 스냅샷당 한 번 만들고, provider 값은 내용이 같으면 같은 참조를 유지한다(색인·검색 항목처럼 마운트 때 필요 없는 것은 첫 질의 때 계산). ② 화면은 섹션 단위로 나누고 섹션은 자기 입력만 받는다 — 마지막 응답이 바꾸는 섹션만 다시 그려진다. ③ 비교는 계산을 유발하지 않고(접근자가 있는 객체는 참조로만), 응답 검증은 인코딩 없이 판정할 수 있는 것을 먼저 판정한다. 배경 응답의 채택을 "한가할 때"로 미루는 방식은 계측에서 악화로 확인돼 쓰지 않는다. 판정·근거는 렌더 구조 개선 검증 기록.
렌더 불변식 게이트(#1610, 정본 렌더 불변식 계약): 입력 1건에 누가 다시 그려지는가를 불변식 표(입력 종류 21 × 허용 경계)로 고정하고 세 겹으로 검사한다 — ① 정적 규칙 R1~R6(npm run check:render, check:static 포함: 값 전체 구독·인라인 prop·memo 없는 export·판 목록 오염·루트 갱신원·표에 없는 입력 종류) ② 팬아웃 러너 scripts/render/render-fanout.mjs --gate(React Profiler 경계, 표본 40, 위반·루트 재실행 초과·판 DOM 예산 초과면 종료 코드 1; 샌드박스·브라우저가 필요해 승격 전 QA·재측정 때) ③ 실기기 텔레메트리 perf_input_response(누름→첫 반응·마지막 데이터→공개·누름→공개, 경로 토큰에 방문 판·활성 판 노드 수). 이 계약의 합격선 판정(#1592 러너 20표본 × 2묶음)은 그 위에서 한다 — 팬아웃이 성립해야 판정 수치가 코드가 아닌 기계 부하만 반영한다.
이 문서 작성·로컬 구현·로컬 통과·release 병합·Production 반영을 구분한다. #1592의 구현 go는 원격 CI 실행 권한이 아니다. 명시적 원격 CI 지시가 없는 동안 로컬 구현·수리·검증·반영 준비까지 진행하며, 원격 CI를 유발하는 단계는 미실행으로 남긴다. 사용자 요청 전체가 실제 적용·검증되기 전에는 트랙을 완료 처리하지 않는다.