풀스택 경계의 통합 단위 = reactive state 그 자체. 서버의 signal/model이 클라이언트에서 같은 객체로 보인다.
// 서버 model이 클라이언트에 그대로 비친다
const todos = createReflectedModel('Todos')
todos.add('buy milk') // RPC인지 로컬 메서드인지 의식 안 함
todos.all.value[0].done.value = true // signal 토글 = 서버 상태 토글
통합 단위가 컴포넌트(RSC)도 함수 호출(server functions)도 DOM diff(LiveView)도 아니다. 다른 진영이 “서버 코드를 어떻게 부를까“를 풀 때, 여기는 “서버 상태를 어떻게 살게 할까”를 푼다. 태그라인 “as if they lived on the client”이 그 직접 표현.
참고
코어는 createSignal·batch·untrack·createId 4개를 이름으로만 import하고 구현은 절대 고르지 않는다. 번들러가 타깃별 sibling(index.solid.ts … index.qwik.ts)으로 redirect → 런타임엔 각 프레임워크의 네이티브 signal이 그대로 들어가 추상화 세금이 0.
핵심은 런타임 DI vs 빌드 타임 DI. 런타임 어댑터는 간접층이 배포물에 잔존해 인라이닝·tree-shaking을 막는다. 빌드 타임 swap은 seam이 출력물에서 증발한다 — createSignal이 문자 그대로 shallowRef가 된다. (monomorphization vs virtual dispatch, #ifdef vs 런타임 if)
가능했던 이유: 계약을 Signal<T> = get value / set value 단 둘로 좁힌 것(narrowest interface). .value shape로 모인 Vue/Preact/Qwik은 re-export 한 줄로 충족된다.
날카로운 반례 — React만 예외. 네이티브 signal이 없어 ~100줄 pub/sub을 직접 구현한다. 빌드 swap이 어댑터를 없앤 게 아니라, 런타임 방식이었다면 모두가 졌을 비용을 React 번들 하나에 가둔 것.
대가: universal 번들 불가(타깃당 아티팩트 N개), 코어는 4-함수 계약 밖의 reactivity를 못 쓴다. 계약을 좁게 유지할 수 있느냐 = 패턴 성립 여부.