GN⁺: 자바스크립트에 시그널 추가 제안
(github.com/proposal-signals)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에만 의존하는 경우, 전체 접근 방식을 변경할 필요가 있을 수 있음.
- Vanilla JS 예제가 더 읽기 쉽고 작업하기 편하다고 느끼는 사람이 나 혼자인가?
-
Promises와 JavaScript의 변화
- 처음에는
new Promise
를 자주 써야 할까 걱정했지만, 실제로는 거의 사용하지 않았음. - 대신
.then
을 많이 사용하게 되었고, 이는 다양한 서드파티 라이브러리와의 인터페이스를 단순화함. - Signal 제안이 반응형 UI 프레임워크에 유사한 효과를 가져다준다면 찬성함.
- 처음에는
-
언어의 일부로서의 Signals
- Signals가 언어의 일부가 될 필요는 없으며, 라이브러리로 충분함.
- 현재의 JS UI 라이브러리가 설계한 Signals가 언어의 일부가 될 정도로 좋다고 생각하는 것은 오만함.
- 모든 유행을 언어 런타임에 추가하는 것은 단기적인 시각으로 보임.
-
애플리케이션에서의 이벤트 사용
- 애플리케이션 전반에 걸쳐 이벤트를 사용하여 신호를 보냄.
-
window.dispatchEvent
와window.addEventListener
를 통해 이벤트를 발생시키고 구독함.
-
DOM 상태 관리와 업데이트의 어려움
- 수십 년 동안 사람들이 상태 관리와 DOM 업데이트를 어려워하는 이유를 이해하려고 함.
- 간단한 DOM 함수를 복잡하게 만드는 것 같아 의아함.
-
Promises와 비동기 프로그래밍
- Promises는 성공적인 사례이지만, async/await 없이는 표준화할 필요가 없었음.
- 다양한 라이브러리 저자들이 이 제안에 대해 어떻게 생각하는지 궁금함.
-
S.js와 Signals
- Signals를 좋아하며 UI 제작 시 다른 기본 요소보다 선호함.
- 그러나 JavaScript 언어에 포함되어야 한다고는 생각하지 않음.
-
MobX와 유사한 Signals
- MobX는 가장 좋아하는 JS 효과 시스템임.
- MobX 버전의 코드 예제 제공.
-
표준 라이브러리에 프레임워크 추가
- 현재 선호하는 프레임워크를 표준 라이브러리에 추가하자는 것과 유사함.
-
Signal 제안에 대한 이해와 문제점
- Signal 제안의 예제를 이해하는 데 어려움을 겪음.
-
effect
함수가 어떻게 parity 변경을 감지하는지, 어떤 신호 변경에도 이 람다를 호출하는지 등에 대한 질문. - Signal 아이디어는 타당하지만, 복잡한 애플리케이션에서 이벤트 추적이 어려워질 수 있음.