렌더 입자성 캠페인 Phase 2 — 오너 스토어 세트 + Provider (구 W5 오너당 인스턴싱 편입)
통합 기록(형식 표준, Phase 현황·결과·남은 것): 2026-08-24-render-granularity.md
- 날짜: 2026-08-23 (Phase 1 #609 직후, 오너 결정 D2 (나): Phase 2~4 진행)
- 목표: 어댑터 훅마다
useRef로 따로 태어나던 스토어·조율자 12개를 계정(오너) 하나가 소유하는 세트로 묶고, 계정 전환을 "세트 교체 한 번"으로 만든다. 하위 바인딩(Phase 3~4)이 현재 계정의 스토어를 얻는 표준 경로(컨텍스트)를 연다.
바뀐 것
| 항목 | 파일 | 내용 |
|---|---|---|
| Phase 2-1 세트 생성 | controllers/ownerStores.ts (신설) | createOwnerStores({ closeLayerStateOnlyRef }) — 스토어 10(드래프트·달력·커스텀 종목·친구 리포트·로그테이블·내비게이션·대기 저장·계획 반영·세션 상세·토스트) + 조율자 2(통계 복구·운동 쓰기 커맨드) |
| Phase 2-2 Provider | controllers/ownerStoresContext.ts (신설) · app.tsx | OwnerStoresContext + useOwnerStores(). 세 상태 분기(loadingData·degradedReady·ready) 모두 Provider > 키 있는 Fragment 동일 모양(재마운트 방지 계약 유지) |
| Phase 2-2 어댑터 | 12개 어댑터 훅 | store/coordinator/commands를 입력으로 받는다(자체 생성 제거). 내비게이션 히스토리 설치 이펙트는 [store]로 — 세트 교체 시 이전 설치를 걷고 새 스토어에 재설치(mount-only 억제 1건 소멸) |
| Phase 2-3 adoptOwner | ownerStores.ts · appController.tsx | adoptOwnerStores(previous, …) = leaveOwnerStores(드래프트 보존이 먼저 → 비행 중 작업 무효화: 통계·로그테이블·세션 상세·쓰기·대기 저장·커스텀·친구·반영·토스트·내비) → createOwnerStores. 지휘자는 첫 마운트를 제외한 전환에서 세트를 교체하고 자기 소유 React 상태만 직접 리셋한다. 교체 뒤 revision 재발화로 새 세트에서 드래프트 복원 |
검증
- 행동 계약
ownerStores.test.mjs4: 세트 인스턴스 독립 / adopt = 보존 후 새 세트(이전 세트는 초기 리터럴·다음 오너 기록, 새 세트는 오너 미기록) / 떠나기 순서(보존 → 무효화) / 구독 분리. - 재조준:
controllerBoundaries(지휘자 = adoptOwnerStores·setPlans 선행, 군집 리셋 순서는leaveOwnerStores본문) ·backgroundTokens(app 루트 모양·startTransition 주입 위치·로그테이블 리셋 위치, ownerStores 결합 표면) ·homeBootLoadingOrder(Provider > 키 Fragment ×3) ·appShellSnapshotIntegrationContract(adopt 마커) ·logTableMonthCache. 매니페스트 등재 3건은 pending-changes 기존 항목에 사유 병합. - 전 체인
npm run check1783/1783 ·check:unused·check:dual-key· eslint 억제 11 → 10. - 로컬 스택
test:e2e-local8/8 · 브라우저 회귀 CASE-001·004·011·013·015·016 6/6(프로덕션 빌드). - 페이지 안 계정 전환 스모크(dev 서버, 임시 스크립트): 드래프트 작성 중 로그아웃 → 로그인 화면, 콘솔·페이지 오류 0 → 같은 계정 재진입 시 이어하기 배너(이전 세트가 보존한 드래프트를 새 세트가 복원).
전 → 후
| 지표 | 전 | 후 |
|---|---|---|
| 스토어·조율자 생성 지점 | 12 (어댑터 useRef) | 1 (createOwnerStores) |
| 지휘자 리셋 문장(appController) | 20 | 13 — 스토어 몫은 adoptOwnerStores 1호출 |
| 새 스토어 추가 시 리셋 누락 가능성 | 지휘자 목록에 손수 추가 | 세트에 넣으면 교체로 구조적 리셋(떠나기 부수효과만 leaveOwnerStores에 추가) |
| exhaustive-deps 리뷰 억제 | 11 | 10 |
| 렌더 효과 | — | 없음(설계상 Phase 3~4의 기반 공사) |
함정(재사용)
- 세트 교체 직후 같은 이펙트에서 새 세트를 쓰면 안 된다 — 어댑터가 다음 렌더에서
setEnv를 주입하기 전이라requireEnv()가 던진다. 복원은 revision 재발화(비전환 경로)에서. - 첫 마운트는 교체하지 않는다(렌더에서 만든 첫 세트가 첫 오너의 세트). 교체하면 내비게이션 히스토리 설치가 불필요하게 두 번 돈다.
- 이전 세트의 비행 중 작업: 새 인스턴스로 갈라지므로 늦은 응답은 구 인스턴스에 갇히지만, env 세터(React 상태)로 쓰는 경로가 있을 수 있어 떠나기에서 기존 세대 bump 리셋을 그대로 돌린다(이중 안전망).
다음
- Phase 3 파일럿 바인딩(라이브 운동 화면):
useOwnerStores()로 드래프트 스토어를 구독하는WorkoutFlowBinding, renderCtx 드래프트 키 은퇴.