3P by GN⁺ | ★ favorite | 댓글 1개
  • 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.StateSignal.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>는 사용자 정의 비교 함수 equalswatched / 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로 정리해야 함
  • 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 렌더링 스케줄링을 관리함
  • 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 같은 다른 구성이 더 적합하다고 함

댓글과 토론

Hacker News 의견들
  • 순수 JavaScript 예제가 오히려 읽고 다루기 더 쉽다고 느끼는 건 나뿐인가 싶음
    “설정이 잡음과 보일러플레이트가 많다”지만, signals 예제도 똑같이 시끄럽고 보일러플레이트가 많아 보이며 초보자가 이해하기 어려운 새 개념까지 추가함
    “counter가 2에서 4로 바뀌면 parity는 안 바뀌는데 불필요한 계산과 렌더링을 한다”는 건 성급한 메모이제이션처럼 들림
    UI의 다른 부분이 counter 업데이트에 맞춰 렌더링하고 싶다면 그 strawman 예제가 적절하지 않은 건 맞고, 그때는 signals, 이벤트 처리, 중앙 상태 저장소(Redux류) 같은 다른 방법을 쓰면 됨
    UI의 다른 부분이 isEven이나 parity에만 의존한다면 앱의 핵심 구조라면 접근을 바꿀 수 있겠지만, 대부분은 그렇지 않음. “parity에만 의존하는 render 함수가 counter를 구독해야 한다는 사실을 알아야 한다”는 것도 꼭 부당한 부담은 아니며, 순수 계산 함수는 입력을 파악하기 쉽다는 장점이 있음

    • 왜 이게 성급한 메모이제이션이라고 보는지 모르겠음. 이건 단순한 함수로 축약한 예제일 뿐이고, 사람들이 실제로 필요했던 적도 없이 이런 사용 사례를 만들어냈다고 보기는 어려움
      UI 개발에서 점점 많이 쓰이는 개념인 signals 표준화를 시도하는 건 칭찬할 만함. 보일러플레이트가 어느 정도인지, 이벤트 시스템을 직접 만들어야 하는지 같은 세부 논쟁은 차치하더라도 여러 프레임워크에서 signals를 쓰고 있다면 이유가 있을 수 있고, 시간이 걸리더라도 표준화를 시도할 만함
    • 동의함. 다만 Preact의 signal 문서를 보면 맥락이 훨씬 잘 맞음
      https://preactjs.com/guide/v10/signals
      Preact에서는 signal이 props나 context로 트리를 내려갈 때 signal 참조만 전달하고, 컴포넌트는 값이 아니라 signal을 보기 때문에 signal을 업데이트해도 컴포넌트를 다시 렌더링하지 않을 수 있음. 실제로 .value를 접근하는 트리의 컴포넌트로 바로 갈 수 있음
      또 signal은 값이 언제 접근되고 언제 업데이트되는지 추적하며, Preact에서는 컴포넌트 안에서 signal의 .value에 접근하면 그 signal 값이 바뀔 때 컴포넌트를 자동으로 다시 렌더링함
    • JavaScript에 반응성이 내장되어 있지 않으니, 반응성을 추가하면 추상화 비용이 생길 수밖에 없음. 필요할 때 쓰기 위한 것이지, 상태를 다루는 기본 방식일 필요는 없음
      경험상 큰 장점은 반응형 상태를 모듈화할 수 있다는 점임. 명령형 스타일에서는 변화를 추적하려고 추가 상태가 필요하고, 모듈성은 추상화로 달성됨. 필요할 때만 쓰면 됨
      단순하면서도 적용 가능한 예제를 만드는 건 균형의 문제임. 반응성이 명확히 이득인 경우는 대체로 더 복잡해서, 단순하지만 덜 적용 가능한 예제보다 보여주기 어려움
    • 이걸 설명하는 방식은 개선 여지가 있음. 작은 예제에서는 문제가 잘 보이지 않고, 더 큰 규모에서 드러남. PR 환영함
    • 특정 복잡도 임계값을 넘을 때 설계 변경이 필요해지는 건 피할 가치가 있음. 순수 JS 접근은 상태 그래프 복잡도 측면에서 확장 한계가 있고, 진짜 문제는 임계값 전후의 사용성이 아니라 그 임계값을 넘는 순간 사용성이 불연속적으로 바뀐다는 데 있음
  • JavaScript에 Promises가 추가될 때는 new Promise를 여기저기 써야 할까 봐 거부감이 있었음
    실제로는 new Promise를 직접 쓴 횟수를 두 손으로 셀 수 있음. 대신 특히 서드파티 라이브러리를 다룰 때 .then을 훨씬 자주 쓰게 됨
    결국 Promise가 JavaScript에 추가된 일상적 효과는, 서드파티 라이브러리가 제공하는 다양한 특수 동작과 기능에 대해 꽤 단순하고 대체로 견고하며 거의 보편적인 인터페이스를 준 것임. 파일 읽기든 API 요청이든 빌드 단계 출력이든 .then(res => …)을 쓰면 이미 작동 가능한 무언가의 절반쯤 온 느낌임
    Signal 제안이 반응형 UI 프레임워크의 캄브리아기적 폭발 속에서 비슷한 역할을 해준다면 찬성함. 더 나아가 반응성이 UI 밖으로 확장되는 데도 도움이 될 수 있음. UI 상태가 아닌 것들을 위한 점진적 재계산 상태 트리를 종종 상상해 왔음

    • Promises는 주로 async/await를 추가하기 위해 들어갔다고 봤고, 실제 삶의 질을 크게 올린 건 그쪽임. 실무에서는 new Promise를 직접 쓸 일이 드묾
      초기 Promise의 .then은 중첩 delegate보다 큰 개선이었고 단순한 체인에는 괜찮지만, 조건부로 서로 다른 Promise를 체이닝하거나 특정 체인마다 오류 처리가 다르거나 조기 반환이 필요해지면 코드가 훨씬 읽고 다루기 어려워질 수 있음
      async/await를 쓰면 Promise가 아닌 것처럼 호출을 작성할 수 있고, 특정 Promise 호출 주변에 try/catch를 쉽게 둘 수 있으며 조기 반환도 자연스럽게 가능함
  • 왜 이게 언어 일부가 되어야 하는지 모르겠음. 라이브러리로 가능하고 이미 그런 라이브러리들이 있음. 작기 때문에 코드에 포함해도 큰 부담이 아니며, 언어에 추가하는 것 자체가 목표가 되어서는 안 됨
    현재 JS UI 라이브러리들이 signals를 너무 잘 설계해서 언어 일부가 되어야 한다고 생각하는 건 오만함. signals에는 서로 다른 절충점을 가진 구현이 많고, 그중 어느 것도 JavaScript 명세에서 특별한 자리를 차지할 자격은 없음
    이런 라이브러리들이 signals를 쓰기 전에는 가상 DOM을 썼음. 다행히 가상 DOM은 JS 일부가 되지 않았는데, signals가 무엇이 다른가? 다르지 않음. 표준화해야 한다는 논거는 가상 DOM 때보다도 약함
    웹을 깨지 않고 더는 원치 않는 기능을 제거할 방법이 사실상 없는 런타임에 유행하는 것을 전부 쌓아 넣을 셈인가? 꽤 근시안적임

    • 맞는 지점이 있음. 잘못된 것을 원하지는 않지만, 올바른 것은 원함
      반응형 UI는 이겼음. 작은 애플리케이션에서도 상태 관리를 하다 보면 복잡도가 폭발하는 것이 순수 JS를 쓰기 어렵게 만드는 핵심임. 내게는 어떤 반응형 프레임워크든 순수 JS보다 낫고, 그렇다면 빠진 구성 요소가 있을지도 모름
      이제 10년쯤 지났으니 표준화할 수 있는 경계점을 생각해볼 때임. Promise처럼 제대로만 된다면 매우 흔한 사용 사례의 복잡도를 낮출 수 있음
      더 나은 평가는 “기존 반응형 프레임워크들이 이 제안을 쓸까?”라고 묻는 것임. 아니라면 왜인지, 무엇이 빠졌는지, 무엇이 불필요한지, 다른 언어의 UI와 반응성에서 배울 점은 무엇인지 봐야 함. 흩어진 경험을 정제할 가치가 있음
    • Signals를 표준화할 좋은 이유 중 하나는 디버깅이 악몽처럼 보인다는 점임. 계산된 signal의 깊은 트리가 서로를 연쇄적으로 발화시키는데, 그 연쇄 반응의 시작점을 찾아야 하는 상황을 상상해보면 됨. 표준화되면 개발자 도구가 그 주변에 만들어질 수 있음
    • 표준 라이브러리의 대부분에 대해 같은 말을 할 수 있음. 다만 동기에서 말하듯 JS의 비교적 작은 표준 라이브러리를 확장해, 흔한 작업마다 패키지를 가져오지 않아도 되게 하려는 흐름이 있음
      그 필요성은 논쟁할 수 있지만, 표준 라이브러리를 확장할 거라면 인기 있는 것을 보는 건 좋은 접근이라고 봄
      signals는 가상 DOM의 대체물이 아님
    • Observable 제안이 떠오름
  • 애플리케이션 전체에 무언가를 신호해야 할 때는 이벤트를 씀
    window.dispatchEvent(new Event('counterChange'));
    그리고 반응하고 싶은 애플리케이션의 어느 부분이든 이렇게 구독할 수 있음
    window.addEventListener('counterChange', () => { ... do something ... });
    이 방식에 뭐가 문제인가?

    • 역사적으로 이 예제야말로 웹이 jQuery로 진화하고, 거기서 Angular와 React의 세계로 갈라진 이유임
      이벤트 처리는 아주 쉽게 지저분해짐. 더 깊게 보려면 이벤트 버블링과 전파를 보면 됨
      큰 애플리케이션에는 견고한 이벤트 처리가 필요하고, 이것이 Angular, Vue 같은 프레임워크의 요즘은 잘 드러나지 않는 장점임
      프레임워크 없이 표준 이벤트 처리 API를 그대로 쓰고 싶지 않을 것임. 많은 요소에 대해 추가, 삭제, 복제, 발화, 제거, 1회 발화 등을 처리하다 보면 심각한 원치 않는 부작용이 생길 수 있음
    • 본문에 따르면 이벤트 발행자/옵저버블은 여러 번 호출될 때 불필요한 작업을 일으킴
      signals와의 차이는 최종 소비자가 값을 읽을 때만 결과값이 계산된다는 데 있음. signal에 실제 쓰기를 하는 시점과 비동기 렌더 업데이트 예약을 분리하고, 감시자가 수행하는 계산 체인은 렌더 중 한 번만 수행됨
      signal로 보내진 중간값들은 사라지므로 그 안에서 흥미로운 일을 많이 하기는 어렵고, 사실상 렌더링 주기를 조율하기 위한 고급 추상화 계층에 가까움
    • Signals도 결국 발행/구독이지만 API가 더 쓰기 좋음. 리스너가 자동으로 추가되고 해제되기 때문임
      성능도 더 좋을 수 있음. 예를 들어 두 값에 의존하는 계산이 있다고 하면 result = a ? b : 0에서 a가 거짓이면 b가 바뀌어도 재계산할 필요가 없음. signals에서는 이게 자동으로 되지만, 전통적인 발행/구독으로는 꽤 많은 코드가 필요함
    • 이런 패턴을 10년 넘게 써왔음. 어려운 점은 시간이 지나면 어떤 리스너가 다른 이벤트를 트리거하고, 또 다른 이벤트가 첫 루틴으로 돌아와 끝나지 않는 리스너 루프가 생길 수 있다는 것임
      모든 리스너가 그런 연쇄 트리거를 만들지 않는다고 보장하기도 어려움
    • 그 방식에는 제안서가 강조한 발행/구독 아키텍처의 단점이 모두 있음
  • 수십 년 동안 사람들이 왜 상태 추적과 DOM 업데이트를 그렇게 어려워하는지 이해하려고 해왔음
    물론 약간의 규율은 필요하지만, 몇 년마다 나오는 해결책들보다 훨씬 단순하게 느껴짐. Backbone, Knockout, Angular, React, 언어 자체 수정 등이 그렇고, 아마 내가 생각하는 방식이 근본적으로 다른 듯함
    함수 이름에서도 드러남. innerText를 업데이트하는 걸 “render”라고 부르는데, 실제로 렌더링하는 게 아님. 기껏해야 브라우저가 렌더링하는 것이고, 그건 페인팅과 관련된 다른 모든 작업도 마찬가지임. 가장 단순한 DOM 함수 중 하나를 복잡하게 만들려는 필사적인 시도처럼 느껴져 정말 당황스러움

    • 단순한 애플리케이션에서는 쉬움
      더 복잡해지면 쉽지 않음
    • “약간의 규율은 필요하다”는 말에서, 예전 같으면 ASM 프로그래머로서 미친 휴대용 C 프로그래머들에게 화내고, 이후에는 C 프로그래머로서 미친 메모리 안전 Java 프로그래머들에게 화냈을 것 같다는 강한 느낌이 듦
      프로그래밍의 진보는 좋은 결과를 얻기 위해 필요한 의례적이고 엄격한 규율을 제거하는 과정으로 볼 수 있음
      React가 다음 진화라는 뜻은 아니지만, signals는 분명 올바른 방향의 한 걸음임
    • 수십 년이면 긴 시간임. 브라우저마다 DOM 업데이트가 얼마나 복잡했는지 기억할 것임
      DOM을 데이터 상태와 동기화하는 것 자체는 너무 어렵지 않지만, 60fps로 매우 성능 좋게 하는 건 엄청나게 어려움. 특히 새지 않으면서도 너무 번거롭지 않은 API를 만들 때 더 그렇다
      변경을 살아 있는 DOM 트리에 변환해 반영하는 것보다, 차라리 게임처럼 canvas에 픽셀을 그리는 편이 솔직히 더 쉬울 수 있음
    • 수십만 줄 규모의 FX 거래 애플리케이션을 만들고 있음. 두꺼운 데스크톱 앱을 대체하는 수준이고, 20~30명의 개발자와 각자 개발자를 둔 여러 고객이 있음. 프레임워크 없이 해보라면 행운을 빌 수밖에 없음
    • 완전히 같은 생각임. 아주 복잡하고 상호작용이 많은 SPA도 개발해봤지만, 이런 것들이 해결한다고 하는 문제가 무엇인지 아직 마주친 적이 없음
  • Promises는 좋은 성공 사례지만, async/await가 없었다면 꼭 표준화할 필요는 없었음
    현재 초안이 Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz 등의 작성자/유지보수자 설계를 바탕으로 했다고 하는데, 기존 라이브러리 작성자들이 이 제안을 어떻게 보는지 궁금함. React가 목록에 없는 것도 흥미로움
    Signals는 채널과 조금 비슷하지만, 단일 수신자가 아니라 브로드캐스트라는 점이 다름. 이걸 활용해서 웹 워커가 onMessage 콜백 대신 채널로 통신할 수 있게 되면 멋질 듯함. 특히 Go처럼 signals/channels/promises 위에서 select할 수 있으면, 여러 동시 메시징 메커니즘을 콜백으로 관리하는 것보다 문법상 이점이 있음. 예를 들어 signals를 Promise.any에 포함할 수 있게 하는 식임

    • “async/await가 없었다면 표준화할 필요가 없었다”는 데 강하게 반대함
      x instanceof Promise는 그냥 작동하지 않음. 내 라이브러리의 then 메서드는 catch 콜백을 받는데 다른 라이브러리는 안 받는다면, 조용히 상호 운용되지 않고 감지할 방법도 없음. finally는 언제 실행되는가? 콜백이 얼마나 비동기적으로 실행될지 어떤 기대를 할 수 있는가?
      표준이 없으면 Promise를 쓰는 모든 라이브러리가 자체 폴리필을 가져와야 함. 이미 있는 것을 신뢰할 수 없기 때문임. 그리고 다른 라이브러리의 Promise도 실제로 소비할 수 없음. 기대한 방식으로 동작한다고 믿을 수 없기 때문임
      추측이 아니라 수년 동안 실제로 그랬고, 많은 사람들이 견뎌야 했던 지옥이었음
    • async/await와 무관한 표준화의 이점도 있음. JavaScript 엔진이 Promise를 많이 쓰는 애플리케이션에 이득이 되는 성능 최적화를 할 수 있었고, 표준이 아니었다면 불가능했을 것임
    • React가 목록에 없는 이유는 signals가 Preact와 달리 React 핵심 API의 일부가 아니기 때문임
      어렴풋한 직감으로는 signals가 일반화된 useEffect()와 너무 비슷해서, React에 들어오면 렌더 주기 동안 무슨 일이 일어나는지 더 혼란스럽게 만들 것 같음. 좋든 나쁘든 React는 signals와는 다른 업데이트 방식을 택함. 다만 적용 가능성에 대해서는 틀렸을 수도 있음
    • React가 이 목록에 없는 이유는 그 효과가 명령형이 아니라 선언형이기 때문임. props 변경과 재렌더는 한 단계 추상화된 선언형이라고 볼 수도 있음. useEffect는 명령형 동작을 깔끔하게 격리함
      이건 Ember 데이터 바인딩과 매우 비슷해 보이고, 결국 명령형 악몽이 될 수 있음. 기본 상태가 “발등 찍는 총”에 가깝고, 그렇게 되지 않게 막으려면 엄청난 인지 부담과 메타 패턴이 필요함
    • 이걸 “EventEmitter”라고 부르면 될지도 모름
      https://nodejs.org/en/learn/asynchronous-work/the-nodejs-eve...
  • 링크된 README의 예제를 이해하지 못했음
    // A library or framework defines effects based on other Signal primitives
    declare function effect(cb: () => void): (() => void);
    어떤 라이브러리? 어떤 프레임워크? 여기서 길을 잃었음. effect가 무엇인가?
    effect(() => element.innerText = parity.get());
    effect는 parity가 바뀔 때마다 이 람다를 호출해야 한다는 걸 어떻게 아는가? signal이 바뀔 때마다 이 람다를 호출하는가? 그렇다면 캐싱 얘기는 왜 하는가? 아마 아닐 것임
    어쨌든 작성자들이 전달하려던 것을 제대로 이해했다면 signal 아이디어 자체는 타당해 보임. 다만 이런 분리 아키텍처의 큰 문제는 애플리케이션이 충분히 복잡해지면 특정 이벤트가 왜 발생하는지 추적하다 길을 잃는다는 것임. 이상적으로는 signals가 스택 추적을 수정해서, 콜백이 호출될 때 애초에 signal을 트리거한 코드의 스택 추적을 이미 포함해야 함

    • effect라는 함수를 내보내는 라이브러리들이 여럿 있고, signal 업데이트에 반응해 임의의 코드를 실행할 수 있게 해줌. Preact 문서의 signals와 effects 입문이 좋음: https://preactjs.com/guide/v10/signals#effectfn
      이해한 바로는 이런 effect 함수는 먼저 콜백을 한 번 실행해서 실행 중 어떤 signal에 접근했는지 확인하고, 그 콜백이 의존하는 signal이 업데이트될 때마다 콜백을 다시 호출함. signal 접근이 동기적이고 단일 스레드라면, 콜백 실행 중 signal에 접근했다는 사실만으로 그 콜백이 해당 signal을 구독해야 한다고 알 수 있음
      getter로도 가능함. effect 함수가 getter 메서드에서 signal의 어떤 속성에 접근했는지 추적하는 방식이고, Vue 2가 과거에 이런 방식을 썼던 것으로 앎. 프록시로 객체 접근을 추적할 수도 있음. 제안서 예제는 signal 값에 접근하기 위해 호출하는 get 메서드가 있고, 이 메서드 실행을 통해 의존성을 추적할 수 있음
      [1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
      [2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
    • parity.get() 호출이 effect()에 전달된 함수에 대한 의존성을 등록함. parity가 업데이트되면 그 함수가 호출됨
      signal이 바뀔 때마다 호출하는 것이 아니라, 의존하는 signal이 바뀔 때만 호출함
      이 경우 parityisEven에 의존하고, isEvencounter에 의존함. 그래서 counter가 업데이트되면 전체 의존성 체인이 무효화되고, parity가 무효화되어 콜백이 다시 실행됨
    • signals 구현은 이름이 무엇이든 대체로 동적 의존성 그래프를 만들고, 노드를 읽을 때 간선이 생김. 이런 가상의 effect 같은 추적 컨텍스트에서는 읽기가 signal의 상태 노드와 effect의 계산 노드 사이에 간선을 만들고, 사실상 후자가 전자의 이후 쓰기를 구독하게 해서 언제 계산을 다시 실행할지 결정함
    • effect는 호출하고 싶은 임의의 함수임
      signals에서는 의존성 추적 메커니즘이 어떤 값을 다시 계산해야 하는지 알고, 그 결과 시스템이 어떤 함수를 다시 호출해야 하는지도 알게 됨
    • watcher가 effect 구현에 필요할 것 같음
  • 관련해서 S.js가 있음: https://github.com/adamhaile/s
    signals를 좋아함. UI를 만들 때 다른 어떤 기본 요소보다 선호하고, 어쩌면 cassowary 제약 알고리즘 정도만 예외일 수 있음. 재미로 쓰는 모든 언어에서 signals를 흉내 내보려 함
    하지만 JavaScript 언어 자체에 들어갈 물건은 전혀 아니라고 봄. 언어를 당분간 좀 놔뒀으면 함. 사람들은 이미 따라가기 힘들어하고, TC-39는 이미 사람들을 언어에서 겁먹고 멀어지게 하고 있음

  • 이건 내가 가장 좋아하는 JS effect 시스템인 MobX와 매우 비슷해 보임
    MobX 버전은 이렇다
    import { observable, computed, autorun } from 'mobx';
    const counter = observable.box(0);
    const isEven = computed(() => (counter.get() & 1) === 0);
    const parity = computed(() => isEven.get() ? "even" : "odd");
    autorun(() => { element.innerText = parity.get(); });
    setInterval(() => counter.set(counter.get() + 1), 1000);

    • MobX는 signals가 맞음. 다만 의존성을 getter로 명시적으로 추적하는 대신 프록시 객체를 통해 암묵적으로 추적하는 signals임
  • “내가 요즘 쓰는 프레임워크를 표준 라이브러리에 구워 넣자!”는 느낌임
    여자친구 이름을 몸에 문신하는 것과 비슷함

    • 이건 그런 게 아님
      대부분의 프레임워크가 사용하도록 수렴한 구성 요소를 표준 라이브러리에 넣는 것임
      Promises도 널리 쓰인 뒤 표준 라이브러리에 들어갔고, 이것도 그와 비슷함