3P by neo 8달전 | favorite | 댓글 1개

JavaScript Signals 표준 제안 초안

  • JavaScript에서의 신호(signals)에 대한 초기 공통 방향을 설명하는 문서로, ES2015에서 TC39에 의해 표준화된 Promises 이전의 Promises/A+ 노력과 유사함.
  • 이 노력은 JavaScript 생태계를 조정하는 데 중점을 두고 있으며, 이 조정이 성공적이면 그 경험을 바탕으로 표준이 등장할 수 있음.
  • 여러 프레임워크 작성자들이 반응성 코어를 뒷받침할 수 있는 공통 모델에 대해 협력하고 있음.
  • 현재 초안은 Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz 등의 저자/유지 관리자들로부터의 설계 입력을 기반으로 함.

배경: 왜 신호인가?

  • 복잡한 사용자 인터페이스(UI)를 개발하기 위해, JavaScript 애플리케이션 개발자들은 상태를 저장, 계산, 무효화, 동기화 및 효율적인 방식으로 애플리케이션의 뷰 레이어로 푸시할 필요가 있음.
  • UI는 단순한 값 관리뿐만 아니라 다른 값이나 상태에 의존하는 계산된 상태를 렌더링하는 것을 자주 포함함.
  • 신호의 목표는 이러한 애플리케이션 상태를 관리하기 위한 인프라를 제공하여 개발자들이 반복적인 세부 사항보다 비즈니스 로직에 집중할 수 있도록 하는 것임.
예시 - VanillaJS 카운터
  • counter라는 변수가 있고, 이 변수가 변경될 때마다 DOM에 카운터의 짝수 여부를 업데이트하고자 함.
  • Vanilla JS에서는 다음과 같은 코드가 있을 수 있음:
let counter = 0;
const setCounter = (value) => {
  counter = value;
  render();
};

const isEven = () => (counter & 1) == 0;
const parity = () => isEven() ? "even" : "odd";
const render = () => element.innerText = parity();

// Simulate external updates to counter...
setInterval(() => setCounter(counter + 1), 1000);
  • 이 코드에는 몇 가지 문제가 있음:
    • counter 설정이 잡음이 많고 보일러플레이트가 많음.
    • counter 상태가 렌더링 시스템과 밀접하게 연결됨.
    • counter가 변경되었지만 parity가 변경되지 않는 경우(예: 2에서 4로 변경), 불필요한 계산과 렌더링을 수행함.
    • UI의 다른 부분이 counter 업데이트 시에만 렌더링하고자 하는 경우.
    • isEven이나 parity에만 의존하는 UI의 다른 부분이 counter와 직접 상호 작용하지 않고는 업데이트할 수 없음.

신호 소개

  • 모델과 뷰 간의 데이터 바인딩 추상화는 JS나 웹 플랫폼에 그러한 메커니즘이 내장되어 있지 않음에도 불구하고 오랫동안 UI 프레임워크의 핵심이었음.
  • JS 프레임워크와 라이브러리 내에서는 이러한 바인딩을 나타내는 다양한 방법에 대해 많은 실험이 이루어졌으며, "Signals"라고 종종 불리는 상태 또는 다른 데이터에서 파생된 계산을 나타내는 일급 반응형 값 접근 방식의 힘이 입증됨.
  • 신호 API를 사용하여 위의 예제를 다시 상상해보면 다음과 같음:
const counter = new Signal.State(0);
const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);
const parity = new Signal.Computed(() => isEven.get() ? "even" : "odd");

// A library or framework defines effects based on other Signal primitives
declare function effect(cb: () => void): (() => void);

effect(() => element.innerText = parity.get());

// Simulate external updates to counter...
setInterval(() => counter.set(counter.get() + 1), 1000);

신호 표준화에 대한 동기

상호 운용성
  • 각 신호 구현은 자체 자동 추적 메커니즘을 가지고 있어, 다른 프레임워크 간에 모델, 컴포넌트, 라이브러리를 공유하기 어려움.
  • 이 제안의 목표는 반응형 모델을 렌더링 뷰로부터 완전히 분리하여 개발자가 새로운 렌더링 기술로 이전할 때 비-UI 코드를 다시 작성하지 않아도 되게 하거나, 다른 맥락에서 배포될 수 있는 공유 반응형 모델을 JS로 개발할 수 있게 하는 것임.
성능/메모리 사용
  • 일반적으로 사용되는 라이브러리가 내장되어 있기 때문에 코드를 덜 전송하는 것은 항상 작은 잠재적 성능 향상을 가져올 수 있지만, 신호 구현은 일반적으로 꽤 작기 때문에 이 효과가 매우 클 것으로 기대하지 않음.
개발자 도구
  • 기존 JS 언어 신호 라이브러리를 사용할 때는 계산된 신호 체인을 통한 호출 스택, 신호 간 참조 그래프 등을 추적하기 어려움.
  • 내장 신호는 JS 런타임과 개발자 도구가 신호를 검사하는 데 개선된 지원을 제공할 수 있게 함.
부수적인 이점
표준 라이브러리의 이점
  • 일반적으로 JavaScript는 상당히 최소한의 표준 라이브러리를 가지고 있었지만, TC39의 추세는 JS를 고품질의 내장 기능 세트가 있는 "배터리 포함" 언어로 만드는 것임.
HTML/DOM 통합 (미래의 가능성)
  • W3C와 브라우저 구현자들은 현재 HTML에 네이티브 템플릿을 도입하기 위한 작업을 진행 중임.
  • 이러한 목표를 달성하기 위해서는 결국 HTML에 반응형 원시값이 필요함.

신호 설계 목표

  • 기존 신호 라이브러리는 핵심에서 그리 다르지 않음.
  • 이 제안은 많은 라이브러리의 중요한 특성을 구현함으로써 그들의 성공을 바탕으로 구축하고자 함.

핵심 기능

  • 상태를 나타내는 Signal 타입, 즉 쓰기 가능한 Signal.
  • 다른 신호에 의존하고 게으르게 계산되고 캐시되는 계산/메모/파생된 Signal 타입.
  • JS 프레임워크가 자체 스케줄링을 할 수 있도록 함.

API 스케치

  • 초기 신호 API 아이디어는 아래와 같음. 이것은 단지 초기 초안이며, 시간이 지남에 따라 변경될 것으로 예상됨.
namespace Signal {
  // A read-write Signal
  class State<T> implements Signal<T> {
    // Create a state Signal starting with the value t
    constructor(t: T, options?: SignalOptions<T>);
    
    // Get the value of the signal
    get(): T;
    
    // Set the state Signal value to t
    set(t: T): void;
  }
  
  // A Signal which is a formula based on other Signals
  class Computed<T> implements Signal<T> {
    // Create a Signal which evaluates to the value returned by the callback.
    // Callback is called with this signal as the this value.
    constructor(cb: (this: Computed<T>) => T, options?: SignalOptions<T>);
    
    // Get the value of the signal
    get(): T;
  }
  
  // This namespace includes "advanced" features that are better to
  // leave for framework authors rather than application developers.
  // Analogous to `crypto.subtle`
  namespace subtle {
    // Run a callback with all tracking disabled (even for nested computed).
    function untrack<T>(cb: () => T): T;
    
    // Get the current computed signal which is tracking any signal reads, if any
    function currentComputed(): Computed | null;
    
    // Returns ordered list of all signals which this one referenced
    // during the last time it was evaluated.
    // For a Watcher, lists the set of signals which it is watching.
    function introspectSources(s: Computed | Watcher): (State | Computed)[];
    
    // Returns the Watchers that this signal is contained in, plus any
    // Computed signals which read this signal last time they were evaluated,
    // if that computed signal is (recursively) watched.
    function introspectSinks(s: State | Computed): (Computed | Watcher)[];
    
    // True if this signal is "live", in that it is watched by a Watcher,
    // or it is read by a Computed signal which is (recursively) live.
    function hasSinks(s: State | Computed): boolean;
    
    // True if this element is "reactive", in that it depends
    // on some other signal. A Computed where hasSources is false
    // will always return the same constant.
    function hasSources(s: Computed | Watcher): boolean;
    
    class Watcher {
      // When a (recursive) source of Watcher is written to, call this callback,
      // if it hasn't already been called since the last `watch` call.
      // No signals may be read or written during the notify.
      constructor(notify: (this: Watcher) => void);
      
      // Add these signals to the Watcher's set, and set the watcher to run its
      // notify callback next time any signal in the set (or one of its dependencies) changes.
      // Can be called with no arguments just to reset the "notified" state, so that
      // the notify callback will be invoked again.
      watch(...s: Signal[]): void;
      
      // Remove these signals from the watched set (e.g., for an effect which is disposed)
      unwatch(...s: Signal[]): void;
      
      // Returns the set of sources in the Watcher's set which are still dirty, or is a computed signal
      // with a source which is dirty or pending and hasn't yet been re-evaluated
      getPending(): Signal[];
    }
    
    // Hooks to observe being watched or no longer watched
    var watched: Symbol;
    var unwatched: Symbol;
  }
  
  interface Options<T> {
    // Custom comparison function between old and new value. Default: Object.is.
    // The signal is passed in as the this value for context.
    equals?: (this: Signal<T>, t: T, t2: T) => boolean;
    
    // Callback called when isWatched becomes true, if it was previously false
    [Signal.subtle.watched]?: (this: Signal<T>) => void;
    
    // Callback called whenever isWatched becomes false, if it was previously true
    [Signal.subtle.unwatched]?: (this: Signal<T>) => void;
  }
}

신호 알고리즘

  • 각 JavaScript에 노출된 API에 대해 구현하는 알고리즘을 설명함.
  • 이것은 초기 사양으로 생각할 수 있으며, 매우 열린 변경에 대해 가능한 한 세트의 의미를 못 박는 것을 목표로 함.

GN⁺의 의견

  • JavaScript Signals 표준 제안은 프레임워크 간의 상호 운용성을 향상시키고 개발자들이 반응형 프로그래밍을 더 쉽게 구현할 수 있게 하는 것을 목표로 함.
  • 이 제안은 기존의 여러 신호 라이브러리들의 핵심 기능을 표준화하려는 시도로, 개발자들에게 일관된 프로그래밍 모델을 제공할 수 있음.
  • 신호의 개념은 UI 개발뿐만 아니라 비-UI 컨텍스트에서도 유용하게 적용될 수 있으며, 특히 빌드 시스템에서 불필요한 재빌드를 피하는 데 도움이 될 수 있음.
  • 제안된 API는 프레임워크 개발자들에게 유용한 도구를 제공하며, 이를 통해 더 나은 성능과 메모리 관리를 달성할 수 있을 것으로 기대됨.
  • 그러나 이 기술이 널리 채택되기 위해서는 더 많은 프로토타이핑과 커뮤니티의 피드백이 필요하며, 실제 애플리케이션에 통합되어 그 효과가 입증되어야 함.
  • 현재 React, Vue, Svelte와 같은 프레임워크들은 이미 자체적인 반응형 시스템을 가지고 있으며, 이러한 프레임워크들과의 호환성이나 통합 전략도 중요한 고려 사항이 될 것임.
Hacker News 의견
  • Vanilla JS vs. Signals 예제

    • Vanilla JS 예제가 더 읽기 쉽고 작업하기 편하다고 느끼는 사람이 나 혼자인가?
      • 설정이 복잡하고 보일러플레이트가 많다고 생각됨.
      • 카운터 값이 변경되었을 때 필요하지 않은 연산과 렌더링이 발생할 수 있음.
      • UI의 다른 부분이 카운터 업데이트 시에만 렌더링하려면 상태 관리 방법을 변경해야 할 수도 있음.
      • UI의 다른 부분이 isEven이나 parity에만 의존하는 경우, 전체 접근 방식을 변경할 필요가 있을 수 있음.
  • Promises와 JavaScript의 변화

    • 처음에는 new Promise를 자주 써야 할까 걱정했지만, 실제로는 거의 사용하지 않았음.
    • 대신 .then을 많이 사용하게 되었고, 이는 다양한 서드파티 라이브러리와의 인터페이스를 단순화함.
    • Signal 제안이 반응형 UI 프레임워크에 유사한 효과를 가져다준다면 찬성함.
  • 언어의 일부로서의 Signals

    • Signals가 언어의 일부가 될 필요는 없으며, 라이브러리로 충분함.
    • 현재의 JS UI 라이브러리가 설계한 Signals가 언어의 일부가 될 정도로 좋다고 생각하는 것은 오만함.
    • 모든 유행을 언어 런타임에 추가하는 것은 단기적인 시각으로 보임.
  • 애플리케이션에서의 이벤트 사용

    • 애플리케이션 전반에 걸쳐 이벤트를 사용하여 신호를 보냄.
    • window.dispatchEventwindow.addEventListener를 통해 이벤트를 발생시키고 구독함.
  • DOM 상태 관리와 업데이트의 어려움

    • 수십 년 동안 사람들이 상태 관리와 DOM 업데이트를 어려워하는 이유를 이해하려고 함.
    • 간단한 DOM 함수를 복잡하게 만드는 것 같아 의아함.
  • Promises와 비동기 프로그래밍

    • Promises는 성공적인 사례이지만, async/await 없이는 표준화할 필요가 없었음.
    • 다양한 라이브러리 저자들이 이 제안에 대해 어떻게 생각하는지 궁금함.
  • S.js와 Signals

    • Signals를 좋아하며 UI 제작 시 다른 기본 요소보다 선호함.
    • 그러나 JavaScript 언어에 포함되어야 한다고는 생각하지 않음.
  • MobX와 유사한 Signals

    • MobX는 가장 좋아하는 JS 효과 시스템임.
    • MobX 버전의 코드 예제 제공.
  • 표준 라이브러리에 프레임워크 추가

    • 현재 선호하는 프레임워크를 표준 라이브러리에 추가하자는 것과 유사함.
  • Signal 제안에 대한 이해와 문제점

    • Signal 제안의 예제를 이해하는 데 어려움을 겪음.
    • effect 함수가 어떻게 parity 변경을 감지하는지, 어떤 신호 변경에도 이 람다를 호출하는지 등에 대한 질문.
    • Signal 아이디어는 타당하지만, 복잡한 애플리케이션에서 이벤트 추적이 어려워질 수 있음.