fix(core): 다중 progressive 데이터소스의 initLocal 유실 수정

_localInit 은 단일 슬롯이고 updateTemplateData 가 얕은 스프레드로 교체했다.
소비부(DynamicRenderer useEffect)는 React commit 이후에 실행되므로,
initLocal 을 가진 progressive 소스가 둘 이상이면 두 payload 가 같은 commit
사이에 도착할 때 먼저 온 것이 한 번도 관측되지 않고 사라졌다.

무조건 누적 병합은 refetchDataSource 시 소비가 끝난 payload 를 재적용해
사용자 폼 편집을 되돌린다. 그래서 아직 관측되지 않은 슬롯만 병합하고,
관측된 슬롯은 종전대로 교체한다. 소비 여부는 소비부가 기록한 슬롯 참조로
판정해 해시 계산식을 생산부에 복제하지 않는다.

레이아웃 전환으로 _local 을 리셋할 때 추적 레지스트리도 함께 비운다.

검증: CPU 6배 스로틀로 경합 창을 강제로 열어 수정 전 0/8 → 수정 후 8/8.
동반: template-engine.test.ts 의 vi.mock 이 공유 런타임 export 를 빠뜨려
스위트가 로드되지 않던 것과, 코어 spec 2개가 없는 fixture 를 import 해
playwright 수집이 실패하던 것을 함께 복구했다.
This commit is contained in:
HeuJung
2026-07-10 19:27:27 +09:00
parent 051ed1e528
commit 12968b76ca
13 changed files with 611 additions and 25 deletions
+34
View File
@@ -185,6 +185,40 @@ G7이 자동으로 주입하는 `_global` 속성입니다. 레이아웃에서
- 상세 설명: [`docs/extension/plugin-development.md`](../extension/plugin-development.md) "폼 상태 정합성" 섹션
- 구현 참고: [`DynamicRenderer.tsx`](../../resources/js/core/template-engine/DynamicRenderer.tsx) `performStateUpdate` 상단 주석 (~50줄)
### `_localInit` 은 단일 슬롯이 아니다 (engine-v1.52.2+)
데이터소스의 `initLocal` 이 만든 초기화 payload 는 `dataContext._localInit` 한 키로 전달된다.
생산부는 `updateTemplateData`(3개 write site), 소비부는 `DynamicRenderer` 의 `_localInit` useEffect 다.
소비는 React commit **이후**에 일어난다. progressive 데이터소스는 응답이 오는 대로 각자 독립적으로
`updateTemplateData({ _localInit })` 를 호출하므로, `initLocal` 을 가진 progressive 소스가 둘 이상이면
두 호출이 같은 commit 사이에 들어올 수 있다. 이때 슬롯을 교체하면 먼저 도착한 payload 가
한 번도 관측되지 않고 사라진다.
`_localInit` 슬롯 병합과 `__g7LocalInitTracking` 레지스트리의 단일 소유자는
[`localInitSlot.ts`](../../resources/js/core/template-engine/localInitSlot.ts) 다.
```text
❌ updateTemplateData 에서 _localInit 을 얕은 스프레드(...data)로 교체
❌ 소비 여부와 무관하게 _localInit 을 무조건 누적 병합
(소비가 끝난 payload 가 refetchDataSource 시 재적용되어 사용자 폼 편집을 되돌린다)
❌ 소비 여부 판정을 위해 해시 계산식을 생산부에 복제
(생산·소비 양쪽의 판정 기준이 어긋난다 — 슬롯 참조 비교를 쓴다)
❌ __g7LocalInitTracking 을 레이아웃 전환 시 리셋하지 않음
(_local 은 비웠는데 추적 해시가 남아 새 레이아웃의 동일 payload 가 "이미 적용됨" 으로 건너뛰어진다)
```
```text
✅ 아직 관측되지 않은(unconsumed) 슬롯만 누적 병합 — mergeLocalInitSlot()
✅ 관측 시 슬롯 참조 기록 — markLocalInitConsumed(). 적용/건너뜀 여부와 무관하게 호출
✅ 레이아웃 전환으로 _local 을 리셋할 때 추적 레지스트리도 함께 초기화 — resetLocalInitTracking()
✅ _forceLocalInit 은 한쪽에만 있으면 보존, 양쪽에 있으면 최신 타임스탬프 (refetchOnMount 강제 초기화 유실 방지)
✅ 소비부는 A(setLocalDynamicState)/B(_globalSetState) 양쪽을 갱신 — 이 대칭을 깨지 않는다
```
> 이 결함은 정상 CPU 에서 간헐적이라 놓치기 쉽다. CPU 스로틀링(6배 이상)으로 commit 을 지연시켜야
> 경합 창이 결정적으로 열린다.
### 라이프사이클 (SPA 네비게이션)
SPA navigate 시 `_local` 상태는 **레이아웃 이름 기준으로 선택적 초기화**됩니다: