- TC39의 JavaScript Signals 제안은 UI 상태와 계산된 상태를 효율적으로 추적하기 위한 반응형 원시값을 표준화하려는 초기 방향이며, 현재 Stage 1 수준의 초안임
- 제안은 애플리케이션 개발자가 직접 쓰는 표면 API보다 프레임워크가 공유할 수 있는 Signal 그래프의 핵심 의미론과 자동 추적 메커니즘에 초점을 둠
Signal.State,Signal.Computed,Signal.subtle.Watcher가 핵심 API이며, 계산은 lazy evaluation, 캐싱, 자동 의존성 추적, glitch-free 실행을 지향함- 내장 Signals의 목표는 Angular, Ember, MobX, Preact, Qwik, Solid, Svelte, Vue 등 여러 프레임워크 간 상호운용성을 높이고 DevTools의 디버깅·성능 분석 지원 가능성을 여는 것임
- 제안 그룹은 Stage 2 이전에 여러 프로덕션급 polyfill, 프레임워크 통합, 대규모 애플리케이션 검증, 성능 벤치마크를 거치려 하며, 표준화까지는 최소 2~3년 이상이 걸릴 수 있음
JavaScript Signals 제안의 위치
- JavaScript Signals는 TC39 프로세스의 Stage 1 제안으로 소개됨
- 현재 문서는 ES2015의
Promise표준화 이전에 있었던 Promises/A+와 비슷하게, JavaScript 생태계의 공통 방향을 맞추려는 시도임 - 직접 실험할 수 있는 polyfill이 제공됨
- 제안 챔피언과 원저자는 여러 프레임워크·라이브러리의 설계 입력을 바탕으로 현재 초안을 구성함
표준화가 겨냥하는 문제
- 복잡한 UI는 값을 저장하고, 계산하고, 무효화하고, 동기화하고, 뷰 계층으로 푸시해야 하며, Signals는 이런 반복 작업을 위한 상태 관리 인프라를 제공하려 함
- Vanilla JS 예제에서는
counter,isEven,parity,render가 직접 얽히며 다음 문제가 생김- 상태와 렌더링 시스템이 강하게 결합됨
counter가 2에서 4로 바뀌는 경우처럼parity가 변하지 않아도 불필요한 계산과 렌더링이 발생함- 다른 UI 조각이
counter,isEven,parity중 일부만 구독하려 할 때 수동 구독과 해제 관리가 복잡해짐 - pub/sub를 여러 단계에 추가하면 보일러플레이트와 구독 bookkeeping이 늘고 메모리 누수 위험이 생김
- Signals 기반 예제는
Signal.State와Signal.Computed를 사용해 값, 계산, 부수효과를 하나의 방식으로 다룸- 수동 구독이 필요 없음
- 계산된 Signal이 의존하는 Signal을 자동으로 찾음
- 계산은 값이 명시적으로 요청될 때만 실행됨
- 계산된 Signal은 마지막 값을 캐시함
제안의 핵심 API
Signal<T>는get(): T를 가진 읽기 가능한 값으로 정의됨Signal.State<T>는 쓰기 가능한 Signal임- 생성자는 초기값과 옵션을 받음
get()으로 값을 읽고set(t)로 값을 변경함
Signal.Computed<T>는 다른 Signal에 기반한 계산 Signal임- 콜백이 반환하는 값으로 계산됨
- 의존성을 자동 추적함
- 값은 lazy하게 계산되고 캐시됨
Signal.subtle은 프레임워크 작성자나 DevTools 구현에 가까운 고급 API를 담음untrack(cb)는 추적 없이 Signal을 읽게 함currentComputed()는 현재 추적 중인 computed Signal을 반환함introspectSources,introspectSinks,hasSinks,hasSources는 그래프 관찰용 API임Watcher는 Signal 변경을 감지해 프레임워크 수준의 effect와 스케줄링을 구현하는 기반임
SignalOptions<T>는 사용자 정의 비교 함수equals와watched/unwatched훅을 지원함
작동 방식과 실행 모델
- Signal은 시간이 지나며 변할 수 있는 데이터 셀을 나타내며,
state또는computed로 나뉨 - 계산된 Signal은 실행 중 읽은 Signal을 자동으로 기록하고, 이후 읽힐 때 이전 의존성이 변했는지 확인함
- 계산은 pull-based임
- 의존성이 바뀌어도 즉시 재계산하지 않음
- 누군가
.get()으로 읽을 때 필요한 경우 재계산함
- State Signal의 쓰기는 동기적으로 반영됨
.set()이후 그 값에 의존하는 computed Signal을 읽으면 필요 시 즉시 재계산됨- 내장 batching은 없음
- Watcher의
notify콜백은.set()중 동기적으로 실행될 수 있음- 다만
notify중에는 Signal을 읽거나 쓸 수 없음 - 실제 읽기·쓰기 작업은 이후에 스케줄해야 함
- 다만
- computed Signal의 콜백이 예외를 던지면 해당 예외도 값처럼 캐시되고, Signal을 다시 읽을 때 다시 던져짐
표준화 동기
- 각 프레임워크의 Signal 구현은 자체 자동 추적 메커니즘을 갖고 있어, 모델·컴포넌트·라이브러리를 프레임워크 사이에서 공유하기 어려움
- 제안의 목표는 반응형 모델을 렌더링 뷰에서 분리하는 것임
- 개발자가 렌더링 기술을 바꿔도 비 UI 코드를 다시 쓰지 않도록 함
- 여러 문맥에서 공유할 수 있는 반응형 모델을 JavaScript로 만들 수 있게 함
- 성능과 메모리 측면에서는 내장 구현이 JS 구현보다 작은 상수 계수만큼 효율적일 가능성이 있지만, 엔진이 마법처럼 알고리듬을 바꾸지는 않는다고 명시함
- DevTools 측면에서는 내장 Signals가 다음 정보를 더 잘 보여줄 수 있음
- computed Signal 체인의 call stack
- Signal 간 참조 그래프
- 메모리 사용량 디버깅에 필요한 의존 관계
- 표준 라이브러리에 포함될 경우 번들 크기 축소, 안정성과 품질 향상, 프로젝트 간 공통 어휘 형성 같은 부수 효과도 기대됨
설계 목표와 제약
- 핵심 기능은 쓰기 가능한 Signal, 계산된 Signal, dirty 상태에 대한 반응, 프레임워크 자체 스케줄링,
untrack, 여러 코드베이스의 합성을 포함함 - 계산된 Signal은 glitch-free를 목표로 함
- 불필요한 계산을 피하기 위해 그래프의 잠재적으로 dirty한 부분을 위상 정렬해 실행함
- 중복 계산을 제거하려 함
- 프레임워크가 자체 스케줄링을 할 수 있도록 Promise 스타일의 내장 강제 스케줄링은 넣지 않음
- 동기 반응 콜백의 오용을 막기 위해 Watcher의
notify안에서는 Signal 읽기와 쓰기를 금지함 untrack은 안전하지 않은 탈출구로 취급됨- 추적 없이 읽은 Signal이 계산 결과에 영향을 주면, 해당 Signal 변경 시 computed Signal이 업데이트되지 않을 수 있음
- API는 프레임워크 구현의 기반이 되는 것을 우선하며, 일반 앱 개발자에게 특별히 ergonomic하게 설계된 것은 아님
effect와 Watcher
- 제안에는
effect()같은 내장 함수가 없음 - effect 스케줄링은 프레임워크 렌더링 주기, disposal, 소유권 관리와 얽히기 때문에 JavaScript 표준 API가 직접 해결하지 않음
- 대신
Signal.subtle.Watcher가 effect 구현의 저수준 기반을 제공함- watched Signal의 의존성이 변경되면
notify가 호출됨 getPending()으로 아직 dirty한 Signal을 확인할 수 있음- dispose가 필요한 effect는
unwatch로 정리해야 함
- watched Signal의 의존성이 변경되면
- Watcher가 보고 있는 Signal은 내부 state가 도달 가능하면 계속 살아 있을 수 있으므로, effect 정리 시
Watcher.prototype.unwatch호출이 필요함
현재 초안에서 빠진 기능
- Async는 현재 모델에 포함되지 않음
- Signals는 항상 동기적으로 평가 가능한 값으로 다뤄짐
- loading 상태를 예외로 모델링하는 방법이 일부 가능하며, 개선 논의는 Issue #30에 있음
- Transactions도 포함되지 않음
- 화면 전환에서 “from” 상태와 “to” 상태를 동시에 유지하려면 Signal 그래프 상태를 fork하는 문제가 생김
- 관련 논의는 Issue #73에 있음
- 일부 convenience methods도 현재 초안에서 빠져 있음
- 빠진 기능들은 프레임워크 간 합의 부족과 상위 계층에서 우회 가능하다는 점 때문에 제외됐지만, 프로토타입 이후 다시 검토될 수 있음
개발 계획과 표준화 일정
- 이 제안은 2024년 4월 TC39 Stage 1 안건에 올라 있으며, 문서상 현재는 Stage 0처럼 볼 수 있다고 설명함
- Stage 2를 제안하기 전 다음 작업을 계획함
- 여러 프로덕션급 polyfill 구현 개발
- 다양한 프레임워크 테스트와 test262 스타일 테스트 통과
- thorough signal/framework benchmark set을 통한 성능 검증
- 대표적인 여러 JS 프레임워크와 일부 대규모 애플리케이션에 제안 API 통합
- API 확장 가능성에 대한 이해와 포함 여부 결정
- 제안 그룹은 잘못된 형태의 Signals가 너무 빨리 표준화되는 일을 피하기 위해 보수적으로 추진하려 함
- FAQ는 표준 Signals가 브라우저 전반에서 polyfill 없이 쓰이기까지 최소 2~3년은 걸릴 것으로 예상함
- 현재 polyfill은 사용할 수 있지만, 검토 과정에서 API가 변할 수 있으므로 안정성에 의존하지 않는 것이 좋다고 안내함
FAQ에서 정리된 사용 모델
- 내장 Signals는 렌더링 기술과 독립적임
- VDOM을 쓰는 Preact, native DOM을 쓰는 Solid, 혼합 방식을 쓰는 Vue 같은 형태 모두 가능하다고 설명함
- 앱 개발자는 대체로 프레임워크를 통해 Signals를 쓰는 편이 적합함
- 프레임워크가 Watcher,
untrack, ownership, disposal, DOM 렌더링 스케줄링을 관리함
- 프레임워크가 Watcher,
- SSR, hydration, resumability와 함께 사용할 수 있음
- Qwik은 Signals를 이 속성들과 함께 사용하고 있으며, 제안 측은 Qwik의 resumable Signals를 State와 Computed 조합으로 모델링할 수 있다고 봄
- Signals와
Proxy는 상호 보완적임- Proxy는 얕은 객체 연산을 가로채고, Signals는 데이터 셀의 의존성 그래프를 조율함
- Proxy 뒤에 Signals를 두면 nested reactive structure를 더 ergonomic하게 만들 수 있음
- Signals는 스트림이 아니라 현재 값을 나타내는 셀임
- State Signal에 연속으로 두 번 쓰고 아무 작업도 하지 않으면 첫 번째 쓰기는 computed Signal이나 effect에 보이지 않을 수 있음
- 문서는 이를 glitch-free 실행의 반대편에 있는 특성으로 보고, 스트림에는 async iterable이나 observable 같은 다른 구성이 더 적합하다고 함