#1592 반응성 재설계 검증 기록
최종 목표와 합격 기준은 입력·공개 계약이다. 전체 상태는 진행 중이다. 아래의 단계/부분 성공을 앱 전체 적용이나 Production 완료로 해석하지 않는다.
Phase 1 — 기준선과 대상
기준 앱 main 1711ae2e84612d8af868670a65ef3b0c48957817, 독립 앱 브랜치 feat/1592-responsive-loading, 문서 브랜치 docs/1592-responsive-loading. 로컬 목적 release는 main에서 만든 release/v0.17.12이며 원격에는 아직 없다. 문서 최초 계약 커밋 cf3164acae19a22d5248dfe79f7b6e3f0557a292.
입력 32개 패밀리와 공개 계약 26행을 모바일 19개 stack kind·PC 8개 root binding과 대조했다. 분류가 겹치므로 합산하지 않는다. 단위 기준선 npm run check: 총 3,947 / 성공 3,799 / 실패 0 / 제외 148, 103.34초. 실제 resource→리포트 hook→DOM에서 무효화 직후 26,000kg 선공개를 재현했다. 서버 응답은 제어한 합성 데이터다.
Phase 2 — 공통 상태와 리포트
- resource의
stale/refreshing/error/empty의미를 공개 상태로 보존한다. 무효화 시 cache 보유와 공개 권한을 분리하고, 필수 의존성 중 오류 또는 미완료가 있으면 완성본을 공개하지 않는다. - 개요·선택 기간 상세의 owner/기간/필터/기준일·통계 적용 세대를 대조한다. 상세 freshness는 숫자와 같은 STABLE SQL snapshot에서 읽는다. 분포 미완성·세대 불일치는 오류/재시도로 끝나며, 상세 무응답도 10초 요청 timeout 뒤 복구할 수 있다. timeout은 성공이나 성능 합격을 뜻하지 않는다.
- 리포트의 실제 resource→공통 변환→repository/codec→hook→projection→모바일/PC DOM 검사를 추가했다. 테스트는 외부 응답을 제어하며 준비 상태를 대신 ready로 만들지 않는다. 초기 미확인 cache, 요청 전 invalidation, 필요한 응답 하나 대기, 세 숫자 일괄 공개, 실패/재시도, 세대 불일치, 미완성 분포, 기간/필터/owner 변경과 늦은 응답, 취소, 유효 재방문, 정상 0과 timeout을 포함한다.
| 검증 | 결과 | 한계 |
|---|---|---|
| 공통 상태·숫자 준비 단위 | 6/6 성공 | 실제 네트워크/페인트 시간 아님 |
| 기존 리포트 전환 + 새 resource 공개 브라우저 | 16/16 성공, 실패/제외 0, 13.81초 | 제어 응답 Chromium. 모바일 390×844·PC 1440×1000, 성능 합격 측정과 별도 |
| 리포트 pgTAP | 36/36 assert 성공 | 독립 로컬 DB, Production 아님 |
| 상세 DTO 단위 + 실제 DB 두 연결 경합 | 7/7 성공, 실패/제외 0 | 실제 새 세션 저장·worker commit. 기존 조회는 500kg/세대1 유지, 다음 조회는 1100kg/세대2 |
| SQL candidate→전체 migration 재생→schema snapshot→extract/check | 성공, 223 migrations·612 objects | 빈 격리 DB의 로컬 재생. 배포/Production 미실행 |
앱 npm run check 최종 | 총 3,953 / 성공 3,804 / 실패 0 / 제외 149 / 취소 0, 94.12초 | 제외는 완료가 아님. 새 DB 검사는 위 별도 필수 DB 실행에서 통과 |
검사 중 발견한 누락은 모두 로컬 수리했다. migration tail 배포 상수, schema 변경의 DB 객체 장부 fingerprint, 새 의미 선택자 등록을 갱신했다. 중간 check의 단위 실패 3건은 DB 장부 2건·선택자 등록 1건이며 해당 검사 21/21 재검증 후 최종 check를 통과했다. 브라우저 harness의 최초 번들 실패는 테스트 namespace resolveDir 누락으로 수리했다. 검사를 완화하거나 skip을 추가하지 않았다. 원격 Full CI는 실행하지 않았다.
서버 변경은 함수 결과의 freshness 한 필드이며 기존 payload 필드와 숫자는 동일했다. 실제 로컬 fixture에서 JSON 38,101→38,326바이트(+225), 교차 warm 25회 p95 11.069→10.387ms. 작은 표본의 차이를 성능 개선으로 주장하지 않는다. 신규 migration 위험 분류는 low/function이고 사용자 원본 DML·재계산·백필은 없다. migration 번호는 최종 목적 release 장부와 다시 맞춘다.
Phase 3 — 앱 전체 입력·공개 경계 적용
Codex가 2026-09-14 13:42까지 공유 작업 트리에 쌓은 Phase 3 변경(제품 135개 파일·테스트 80개 파일)을 이어받은 세션(ccc9d751-3fd7-4472-b59a-308316f50038)이 통합·검증했다. 행별 적용 경계와 증거는 60행 적용·검증 상태표에 있다.
- 입력·준비·공개 분리: 완료 기록 수정·계획 수정·세션/그룹 보드에서 운동 시작은 조회 전에 닫을 수 있는 준비 셸을 먼저 열고, 같은 intent 번호로 canonical detail·카탈로그·custom 등록을 기다린 뒤 초안을 공개한다. 늦은 응답은 닫힌 화면을 다시 열지 못한다.
- 공통 공개 경계
DataPublicationBoundary/StatDataRegion: pending·error에서 기존 본문은 mounted+inert+visibility hidden으로 보존하고, pending만 스켈레톤, error는 재시도로 끝낸다. 홈·PC 홈(연간 포함)·홈 날짜·달력 월/일/기간·PC 리포트 날짜/기간·피드·검색·프로필/연결/커스텀/차단·그룹 목록/라운지/보드/기록/상세·알림·수동 기록·회복·PC 기록표·PC 운동 그룹·종목 상세(PR 조각·연도·보관)에 연결했다. - 리포트 외 통계 숫자도 같은 기준: 무효화·재확인 중 이전 숫자는 공개하지 않고, 필수 응답이 모두 같은 세대로 갖춰진 commit에서 함께 공개한다.
- PC 저장 중 닫기(W03): 저장 Promise가 걸린 채 닫기/Escape를 눌러도 즉시 닫히고 입력·저장은 유지된다. 실패하면 같은 편집기와 입력을 다시 연다.
| 검증(모두 로컬, 이어받은 세션 실행) | 결과 | 한계 |
|---|---|---|
Codex 트리 이어받은 직후 통합 npm test | 총 4,001 / 성공 3,850 / 실패 2 / skip 149 → 소스 앵커 검사 2건 재조준 후 총 4,001 / 성공 3,852 / 실패 0 / skip 149 | 앵커 재조준은 PR 기록 보관 구조 분리·홈 부팅 스켈레톤 위치 변경에 따른 것. 단언 완화·skip 추가 없음 |
| Codex 마지막 편집 대상 browser | PC 기록표 3/3, PR 종목 상세(M/PC) 11/11 | 제어 응답 Chromium |
check:static(utf8·migration·deployment·dead-css·dual-key·ts-scope·boundary·tsconfig·lint·types·test-manifest·error-cases) | 통과 | CRLF 신규 파일 5개는 LF 정규화 |
목적 release release/v1.0.0(9a37e73c) 병합 후 npm run check | 정적 통과, 단위 총 4,001 / 성공 3,852 / 실패 0 / skip 149, 74.0초 | QA1 0bb392c0 준비(1,252파일) 상태에서 실행. skip은 완료가 아님 |
QA1 저장소 npm run check | 이관 원본 1,210·조정 39 검증 통과, runner 회귀 17/17 | QA1 원격 게시 전 로컬 브랜치 |
목적 release에는 #1591로 앱 테스트·검사 실행기가 QA1로 이관되어 있었다. 이 트랙이 고친 이관 원본 39개와 새 검사 42개는 QA1 브랜치 qa1/1592-responsive-loading(0bb392c0, suite tree f798c634)의 suite/에 두고 migration-adaptations.json에 원본과 달라진 파일의 sha256과 이유를 적었다. 앱 브랜치는 병합에서 해당 파일의 추적을 끝내고 qa1.lock.json을 그 커밋으로 고정했다(908e7b29). QA1 원격 게시와 앱 push는 사용자 지시 범위에서 실행한다.
Phase 4 — 성능 계측·수리·게이트
이어받은 세션이 Phase 1 합격선(위 표, 변경 없음)을 실제 규모로 쟀다. 절차와 도구 원본은 작업 기록의 evidence/1592-phase4에 있다.
- 환경: 로컬 샌드박스 Supabase(마이그레이션 전체 적용, 통계 worker 자연 수렴)에 실제 계정 3개(history-1y 260회 · 4y 1,043회 · 10y 2,609회,
scripts/performance/workload/generate.mjsseed 1284)를 실제 저장 RPC(save_session_v5)로 적재. production 빌드(npm run build, 소스 head·diff·번들 sha를 지문으로 연결, 지문이 어긋나면 러너가 거부)를vite preview로 띄우고 Chromium(Playwright/CDP) CPU 4배·RTT +100ms, 390×844(모바일)·1440×1000(PC). - 측정 방법(러너
runner.mjs): 클릭 capture 시각 t0 → 첫 의미 있는 시각 반응(제목/스켈레톤/강조가 바뀐 첫 rAF) t1 → 마지막 필수 RPC 수신(Network.loadingFinished) t2 → 완성 DOM 공개(pending 표시 0개 + 기대값 일치) t3. 동기 처리는 devtools.timeline의 클릭 dispatch 작업 길이. warm은 같은 브라우저에서 다른 탭을 거쳐 재방문(재조회·재마운트·스켈레톤 관측). cold는 새 context. 20회 × 2묶음. 늦은 정정(ready 뒤 값 변경)·필수 RPC 미관측·오류 관측은 실패 사유로 기록. - 시나리오: 리포트 열기(M/PC,
get_volume_overview+get_report_design_v1, 기대값 = 적재한 마지막 해 기록 수 177), 달력 진입(M/PC, 표시 달), 달력 월 이동(M; PC는 이웃 달 선조회로 cold를 분리할 수 없어 제외). - 첫 계측(앱
7e4f3762, 1년, 묶음 1)이 넘긴 곳과 수리(앱1e3f4253): 리포트 M 마지막 응답→완성 p95 657ms(전체 투영 재계산+전체 재렌더 → 증분 병합applyReportDesignDetail), 모바일 월 이동 동기 67~99ms(루트 상태 동기 갱신 → 로컬 urgent 상태+transition), PC 탭 전환 동기 87~144ms·첫 반응 117~168ms(판 전환이 클릭 안 → 지연 판useDeferredValue), PC 달력 진입 20회 중 10회 늦은 정정(달 경계 주 합계가 표시 달만으로 공개 → 걸친 달 상태 결합). - 2차 계측(앱
1e3f4253)이 드러낸 것과 수리(앱1d88578c): PC 첫 방문 첫 반응이 915~982ms로 밀렸다 — 바깥 Suspense 경계 하나가 내용을 보이는 동안 지연 렌더 안의 청크 대기를 React가 이전 판을 붙든 채 미룬다 → 판마다 Suspense 경계. 추적에서 러너의 관측 루프(매 프레임 강제 layout, 프레임당 70~130ms)가 가장 긴 작업으로 잡혀 DOM 변경 프레임에서만 판정하도록 바꿨다(판정식·합격선 불변). 리포트 화면이 종목 필터 목록 identity로 동의어 검색기를 다시 만들지 않게applyReportDesignDetail이 목록을 바꾸지 않을 때 같은 배열을 돌려준다. - 게이트: QA1
responsivenessPhase4Gate.test.mjs(네 수리의 소스 핀) ·reportDetailMerge.test.mjs(증분 병합 = 일괄 생성, 입력 불변, 다른 기간/필터/단위 무시) · 계약 문서 §검증·개발 완료 조건의 개발 규칙 ①~③.
Phase 4 결과 — 1년 이력, 묶음 1 (p95 ms; 동기 처리는 max; 전 7e4f3762 → 1차 1e3f4253 → 2차 1d88578c)
1차는 옛 관측 루프, 2차는 DOM 변경 프레임 관측이다(1차 수치에는 러너 관측 비용이 섞여 있다). 원본 JSON은 앱 작업 트리 test-results/phase4/evidence/{before,after-1e3f4253}/와 evidence/(2차)에 있다.
| 시나리오 · 화면 · 캐시 | 입력→첫 반응 (≤100) | 동기 처리 (≤50) | 마지막 데이터→공개 (≤100) | 입력→완성 (1·4년 ≤2,000) | 관측 / 재마운트 | 2차 판정 |
|---|---|---|---|---|---|---|
| 리포트 열기 · M · cold | 360 → 240 → 339 | 24 → 15 → 17 | 657 → 487 → 562 | 1,469 → 1,222 → 1,286 | 20/20 | 첫 반응·공개 미달, 완성 ✓ |
| 리포트 열기 · M · warm | 136 → 120 → 106 | 25 → 19 → 19 | — | 136 → 120 → 106 | 재마운트 0 | 재방문 106(6ms 초과) |
| 리포트 열기 · PC · cold | 44 → 982 → 80 | 32 → 37 → 38 | 386 → 831 → 550 | 544 → 982 → 714 | 20/20 | 공개 미달, 나머지 ✓ |
| 리포트 열기 · PC · warm | 117 → 135 → 139 | 87 → 36 → 38 | — | 117 → 135 → 139 | 재마운트 20/20 | 재방문 미달(재마운트 구조) |
| 달력 진입 · PC · cold | 62 → 915 → 101 | 48 → 57 → 53 | 251 → 603 → 268 | 533 → 915 → 550 | 10/20 → 20/20 → 20/20(늦은 정정 0) | 첫 반응 101·동기 53·공개 미달, 완성 ✓ |
| 달력 진입 · PC · warm | 168 → 189 → 207 | 144 → 151 → 159 | — | 168 → 189 → 207 | 재마운트 0 | 미달 |
| 달력 진입 · M · cold | 87 → 81 → 80 | 36 → 29 → 30 | 206 → 172 → 172 | 504 → 460 → 467 | 20/20 | 공개 미달, 나머지 ✓ |
| 달력 진입 · M · warm | 296 → 140 → 298 | 23 → 23 → 17 | — | 296 → 140 → 298 | 재마운트 0 | 미달(묶음 간 편차 140/298) |
| 달력 월 이동 · M · cold | 83 → 35 → 38 | 83 → 25 → 28 | 315 → 273 → 244 | 485 → 599 → 586 | 20/20 | 공개 미달, 첫 반응·동기 ✓ |
| 달력 월 이동 · M · warm | 73 → 39 → 39 | 67 → 33 → 25 | — | 73 → 39 → 39 | 재마운트 0 | 합격 |
수리로 확인된 개선: 모바일 월 이동(동기 83→28, 첫 반응 83→38, 재방문 합격), PC 달력 늦은 정정 10/20→0, PC 리포트 동기 87→38, 리포트 M 마지막 응답→공개 657→~500, PC 첫 방문 회귀(915/982) 복구(101/80).
남은 미달과 원인(추적 근거, 모두 구조 항목 — 합격선은 바꾸지 않는다):
- 마지막 필수 데이터→완성 공개 100ms — cold 전 경로(M 달력 172~244, PC 달력 268, PC 리포트 550, M 리포트 562). 추적: 마지막 응답 뒤 화면 전체 React 재렌더(리포트 M 239~318ms) + 첫 공개 layout·paint(70~130ms). 4배 CPU에서 100ms는 실제 25ms라, 마지막 응답이 바꾸는 부분만 다시 그리는 구조(섹션 단위 memo·안정 identity, 공개 전 숨은 본문의 layout 선반영)가 필요하다. 이번 트랙은 증분 병합까지 했고 섹션 memo는 미착수.
- 유효 캐시 재방문 100ms — 리포트 M 106, PC 139(재마운트 20/20), 달력 PC 207, M 140~298. 원인: 앱 컨트롤러가 provider 값을 매 렌더 새 객체로 만들어 탭 하나 변경에 mounted 바인딩 전부가 다시 그려진다(4배 CPU 100ms대); PC는 비활성 판을 unmount하는 구조라 재방문 = 재마운트.
- 입력→첫 반응 100ms·동기 50ms — 리포트 M cold 첫 반응 240~339: 홈 진입 직후 도착하는 대용량 응답 처리(
get_volume_overview618KB·get_exercise_catalog814KB의 코덱·저장, 4배 CPU에서 80~230ms 작업)와 겹쳐 입력 처리가 밀린다. PC 달력 동기 53(cold)/159(warm)는 2와 같은 원인.
invalidated 모드(저장→통계 세대 갱신→재공개)는 현재 러너로 재현할 수 없어 미측정이다. 4년·10년과 1년 묶음 2는 아래 표.
Phase 4 결과 — 규모별 (앱 1d88578c, DOM 변경 프레임 관측; p95 ms, 동기 처리는 max; 표본 20/20 전부 관측)
| 시나리오 · 화면 · 캐시 | 1년 묶음 2 | 4년 묶음 1 | 10년 묶음 1 | 판정(세 규모 공통) |
|---|---|---|---|---|
| 리포트 열기 · M · cold (첫 반응 / 동기 / 데이터→공개 / 완성) | 360 / 19 / 515 / 1,252 | 369 / 16 / 574 / 1,385 | 556 / 15 / 574 / 1,656 | 완성 ✓(2,000·5,000 안), 첫 반응·공개 미달 |
| 리포트 열기 · M · warm (재방문) | 106 | 106 | 120 | 재방문 6~20ms 초과 |
| 리포트 열기 · PC · cold | 81 / 33 / 556 / 711 | 83 / 34 / 533 / 816 | 83 / 34 / 515 / 898 | 첫 반응·동기·완성 ✓, 공개 미달 |
| 리포트 열기 · PC · warm | 154(재마운트 20) | 150(재마운트 20) | 138(재마운트 20) | 재방문 미달(재마운트 구조) |
| 달력 진입 · PC · cold | 113 / 58 / 251 / 548 | 111 / 56 / 278 / 566 | 100 / 54 / 242 / 548 | 완성 ✓, 첫 반응 0~13ms·동기 4~8ms 초과, 공개 미달, 늦은 정정 0 |
| 달력 진입 · PC · warm | 174 (동기 134) | 187 (동기 139) | 186 (동기 130) | 미달 |
| 달력 진입 · M · cold | 82 / 37 / 165 / 458 | 91 / 32 / 34 / 330 | 87 / 35 / 40 / 337 | 4·10년 합격, 1년은 공개 165~172 미달(두 묶음 동일) |
| 달력 진입 · M · warm | 277 | 222 | 269 | 미달 |
| 달력 월 이동 · M · cold | 40 / 29 / 297 / 620 | 44 / 28 / 353 / 555 | 35 / 12 / 418 / 622 | 첫 반응·동기·완성 ✓, 공개 미달(규모에 따라 244→353→418) |
| 달력 월 이동 · M · warm | 38 (동기 29) | 37 (동기 30) | 40 (동기 34) | 합격 |
규모 결론: 새 조회 입력→완성 p95는 1년 ≤1,286 · 4년 ≤1,385 · 10년 ≤1,656으로 합격선(2,000 / 5,000) 안이고, 재방문은 규모와 무관하게 같은 값이다(캐시 재사용 확인). 규모에 따라 커지는 것은 마지막 데이터→공개(월 이동 244→418, 리포트 M 515→574)로 원인 1과 같다. 달력 진입 M cold가 4·10년에서만 합격하고 1년에서 두 묶음 모두 165~172ms인 것은 원인을 확인하지 못했다(1년 fixture의 현재 달 세션 수 차이로 추정, 미검증).
Phase 4 통합 검증
| 검증(로컬, 이어받은 세션) | 결과 | 한계 |
|---|---|---|
| 수정·게이트 관련 단위(responsivenessPhase4Gate·reportDetailMerge·desktopFeatureBindings·backgroundTokens·tabSwitchInstantResponse·publicationBoundaryGate 등) | 52/52 | 소스 핀 위주, 렌더 비용은 계측이 판정 |
| 브라우저 publication 스위트(calendar·report·reportPeriodTransition·desktopVolume·desktopHome·homeCompleteReveal·mobileShellContinuity·friendScope·desktopLogTable·prDetail) | 49/49 | 제어 응답 Chromium |
ci:precheck-local --release origin/release/v1.0.0 2차(앱 1d88578c, QA1 67de12ac 준비 1,257파일) | static 통과 · unit-1 2,073/0/64 skip · unit-2 1,790/0/85 skip · database 통과(db reset·schema 스냅샷·pgTAP 122파일/2,453 assert·동시 저장·dirty 병합·체크포인트·worker·큰 이력 격리), 7분 51초 | release/v1.0.0 head 9a37e73c 그대로(추가 병합 없음). 브라우저·viewport 레인은 precheck 범위 밖 |
ci:precheck-local --release origin/release/v0.19.0 3차(목적 release 변경 후: main b622cc80·#1594 병합 145923ff, QA1 8d0e48f5 핀 eae61116) | 전 레인 통과 — unit-1 2,073/0/64 · unit-2 1,790/0/85 · database 통과, 8분 31초 | 직전 실행에서 designDeliveryPrimitives.test.mjs 파일 단위 1회 실패(단언 정보 없음) → 단독·재실행 통과. #1594가 [skip ci]로 main에 들어가며 남긴 고정 스위트 불일치 14건은 QA1 8d0e48f5에서 기대값을 통일 문구에 맞춰 해소 |
ci:precheck-local --release origin/release/v0.19.0 4차(큐 1차 요청 뒤 base가 #1597로 이동 → base 병합 ee846213, QA1 a0e0fad1 = 이 트랙 + #1597 핀 e9622131) | 전 레인 통과 — unit-1 2,074/0/64 · unit-2 1,790/0/85 · database 통과, 9분 12초 | 재요청 run 34845690717 |
전체 잔여
상태표의 부분 증거 18행 보강, invalidated 모드(저장→통계 세대 갱신→재공개) 실측, 최종 precheck·반영 준비가 남았다. 원격 CI·원격 branch/PR·QA1 게시·release 병합·Production 배포는 미실행이다. 로컬 구현 go를 원격 CI 실행 승인으로 확대하지 않는다.