(번역) 자바스크립트 Signal의 진화

Jung Han
10 min readMar 22, 2023

원문: https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob

최근 프런트엔드 생태계에서 ‘Signal’이라는 용어에 대한 논의가 활발하게 이뤄지고 있습니다. Preact부터 Angular에 이르기까지 모든 곳에서 이 용어가 이야기되고 있는 것 같습니다.

하지만 Signal은 전혀 새로운 것이 아닙니다. 1960년대 후반의 연구로 거슬러 올라간다는 점을 고려하면 사실 꽤 오래전부터 존재했습니다. 최초의 전자 스프레드시트와 하드웨어 기술 언어(Verilog나 VHDL같이)를 가능하게 한 기반에는 동일한 모델링이 존재합니다.

자바스크립트에서도 선언적 자바스크립트 프레임워크의 태동기부터 사용되어 왔습니다. 시간이 지남에 따라 다양한 이름으로 불리며 수년 동안 인기를 끌다가 사라지기도 했습니다. 그렇지만 지금이 Signal의 ‘왜’와 ‘어떻게’를 좀 더 자세히 알아볼 수 있는 좋은 시기입니다.

면책 조항: 저는 SolidJS의 작성자입니다. 이 글은 제가 영향을 받은 관점에서의 진화를 반영합니다. 이 글에서 다루지는 않지만 Elm Signals, Ember의 연산 프로퍼티(computed property), 그리고 미티어는 모두 샤라웃 할 만큼 가치가 있습니다.

Signal이 무엇인지, 어떻게 작동하는지 잘 모르겠나요? 세분화된 반응형에 대한 소개를 확인해보세요. (링크, 번역)

태초에는…

때때로 여러 당사자가 거의 같은 시기에 비슷한 솔루션에 도달하는 놀라운 일이 벌어지곤 합니다. 선언적 자바스크립트 프레임워크의 시작을 알리는 세 가지 기술은 모두 3개월 이내에 출시되었습니다. Knockout.js(2010년 7월), Backbone.js(2010년 10월), Angular.js(2010년 10월)가 이에 해당합니다.

Angular.js의 더티 체킹(Dirty Checking), Backbone의 모델 기반 리렌더링, Knockout의 세분화된 업데이트는 약간씩은 달랐지만 궁극적으로 오늘날 우리가 상태를 관리하고 DOM을 업데이트하는 방법의 기반이 되었습니다.

이 글의 주제와 관련해 Knockout.js가 특히 중요한 이유는 이 세분화된 업데이트가 우리가 Signal이라 부르는 것을 기반으로 만들어졌기 때문입니다. 처음에는 observable(상태)와 computed(부수 효과) 두 가지 개념을 도입했지만, 향후 몇 년에 걸쳐 pureComputed(파생 상태) 개념을 프런트엔드 언어에 도입합니다.

const count = ko.observable(0);

const doubleCount = ko.pureComputed(() => count() * 2);
// doubleCount가 업데이트 될 때마다 로그를 남깁니다
ko.computed(() => console.log(doubleCount()));

와일드 웨스트

이 시기 패턴은 서버에서 MVC를 개발하면서 배운 패턴과 지난 몇 년간 jQuery를 사용하면서 배운 패턴이 혼합되어 있었습니다. 특히 공통적인 패턴 중 하나는 데이터 바인딩으로 약간의 방식 차이는 있지만 Angular.js와 Knockout.js에서 모두 공유했습니다.

데이터 바인딩은 뷰 트리의 특정 부분에 상태 조각을 연결해야 한다는 아이디어입니다. 데이터 바인딩이 할 수 있는 가장 강력한 일 중 하나는 양방향으로 만드는 것이었습니다. 이를 통해 상태가 DOM을 업데이트하도록 할 수 있으며, DOM 이벤트가 상태를 자동으로 업데이트 하는 작업을 선언적인 방식으로 쉽게 구현할 수 있었습니다.

하지만 이 기능을 남용하는 것은 결국 자기 발등을 찍는 결과를 초래했습니다. 이를 잘 알지 못했던 우리는 이런 식으로 앱을 구축했습니다. Angular에서는 어떤 변경 사항이 있는지 알지 못하면 전체 트리를 더티 체크하고 상향 전파로 인해 여러 번 변경이 발생할 수 있었습니다. Knockout에서는 트리를 오르락내리락하는 사이클이 일반적이었기 때문에 변경 사항을 추적하기 어려웠습니다.

React가 솔루션과 함께 등장했을 때 (개인적으로 Jing Chen의 강연이 그 해결책을 확고히 했다고 생각합니다.) 우리는 뛰어들 준비가 되어있었습니다.

글리치 프리(Glitch Free)

이후 리액트는 많은 사람들에게 채택되었습니다 일부 사람들은 여전히 반응형 모델을 선호했고, 리액트는 상태 관리에 대한 강제가 없었기 때문에 매우 쉽게 두 가지를 혼합할 수 있었습니다.

Mobservable가 바로 그 솔루션이었습니다. 하지만 리액트로 작업하는 것 이상의 새로운 것을 가져왔습니다. 일관성과 결함 없는(glitch-free) 전파를 강조했습니다. 즉, 주어진 변경사항에 대해 시스템의 각 부분이 적절한 순서로 한 번만 동기적으로 실행된다는 것을 의미했습니다.

이전 코드들에서 볼 수 있었던 일반적인 푸시 기반 반응형을 푸시-풀 하이브리드 시스템으로 변경해 이를 수행했습니다. 변경 알림은 푸시되지만 파생된 상태의 실행은 읽은 위치로 지연되었습니다.

Mobservable의 독창적인 접근 방식에 대해 더 자세히 알아보려면 여기를 클릭하세요. Michel Westrate의 An in Depth Explanation of Mobservable by Michel Westrate.

이 디테일은 리액트가 어쨌든 변경 사항을 읽고 있는 컴포넌트를 다시 렌더링 한다는 사실에 의해 크게 가려졌지만, 이런 시스템을 디버깅할 수 있게 하고 일관되게 만드는 기념비적인 한 걸음이었습니다. 이후 몇 년 동안 알고리즘이 더욱 정교해짐에 따라 우리는 풀 기반 시맨틱을 더 많이 사용하는 추세를 보게 될 것입니다.

누수 옵저버 정복하기

세분화된 반응형은 4인방(Gang of four) 관찰자 패턴의 변형입니다. 동기화를 위한 강력한 패턴이지만 고전적인 문제도 존재합니다. Signal은 구독자에 대한 강력한 참조를 유지하므로 수명이 긴 Signal은 수동으로 폐기하지 않는 한 모든 구독을 유지합니다.

이 장부는 특히 중첩이 포함된 경우 사용량이 많을 때 엄청나게 복잡해집니다. 중첩은 UI 뷰를 작성할 때 같이 분기 로직이나 트리를 다룰 때 흔히 발생합니다.

잘 알려지지 않은 라이브러리인 S.js(2013)가 해답을 제시할 수 있습니다. S는 대부분의 다른 솔루션과 독립적으로 개발되었으며, 모든 상태 변경이 클록 주기에 따라 작동하는 디지털회로를 보다 더 직접적으로 모델링했습니다. S는 이를 상태 원시(state primitive) Signal이라 불렀습니다. 이 이름을 처음 사용한 것은 아니지만, 오늘날 우리가 사용하는 용어는 여기서 유래되었습니다.

더 중요한 것은 반응형 소유권이라는 개념을 도입했다는 점입니다. 소유자는 모든 자식 반응형 스코프를 수집하고, 소유자가 직접 처분하거나 재실행할 경우 처분을 관리합니다. 반응형 그래프는 루트 소유자로 래핑 하면서 시작되고 각 노드는 그 자손의 소유자 역할을 하게 됩니다. 이 소유자 패턴은 처분에 유용할 뿐만 아니라 공급자/소비자 컨텍스트를 반응형 그래프에 구축하는 메커니즘으로도 유용합니다.

스케줄링

Vue(2014) 또한 오늘날을 만드는데 큰 기여를 했습니다. 일관성을 위한 최적화의 발전으로 MobX와 보조를 맞추고 있을 뿐 아니라, Vue는 처음부터 세분화된 반응형을 핵심으로 삼았습니다.

Vue는 리액트와 가상 DOM 사용을 공유함에도 반응형이 매우 중요하게(first-class) 된 것은 프레임워크와 함께 먼저 Options API를 구동하기 위한 내부 메커니즘으로 사용되다가, 지난 몇 년 동안 컴포지션 API(2020)에서 가장 중요한 역할을 담당하게 되었다는 것을 의미합니다.

Vue는 작업 완료 시기를 스케줄링해 푸시/풀 메커니즘을 한 단계 발전시켰습니다. 기본적으로 Vue에서는 모든 변경 사항이 수집되지만, 다음 마이크로태스크에서 이펙트 큐가 실행될 때까지 처리되지 않습니다.

그러나 이 스케줄링은 keep-alive(계산 비용 없이 화면 밖 그래프를 보존하는 기능) 및 Suspense와 같은 작업에도 사용할 수 있습니다. Concurrent 렌더링 같은 작업도 이 접근 방식을 통해 가능하며, 풀 기반 접근 방식과 푸시 기반 접근 방식의 장점을 모두 활용할 수 있음을 보여줍니다.

컴파일하기

2019년 Svelte 3은 우리가 컴파일러로 얼마나 많은 일을 할 수 있는지 모두에게 보여줬습니다. 사실 Svelte는 반응형을 완전히 제거합니다. 트레이드 오프는 존재하지만 컴파일러가 어떻게 인체공학적(ergonomic) 단점을 보완할 수 있는지 보여줬다는 점에서 매우 흥미로웠습니다. 그리고 이것은 앞으로 계속해서 트렌드가 될 것입니다.

상태, 파생 상태, 그리고 효과(effect)를 다루는 반응형의 언어는 사용자 인터페이스와 같이 동기화된 시스템을 설명하는데 필요한 모든 것을 제공할 뿐만 아니라 분석도 가능합니다. 우리는 무엇이 어디에서 변경되었는지 정확히 알 수 있게 됩니다. 추적 가능성의 잠재력은 엄청납니다.

컴파일 할 때 자바스크립트 양을 줄일 수 있다는 점을 안다면 더 적은 양의 자바스크립트를 제공할 수 있습니다. 코드 로딩에서 더 자유로워질 수 있습니다. 이것이 QwikMarko의 재개 가능성(resumability)입니다.

미래를 향한 Signal

이 기술이 얼마나 오래되었는지를 감안할 때, 탐구해야 할 것이 훨씬 더 많다는 것은 놀라운 일입니다. 하지만 이는 특정 솔루션이 아니라 솔루션을 모델링하는 방법이기 때문이라 생각합니다. 이 기술이 제공하는 것은 부수 효과와 무관하게 상태 동기화를 설명하는 언어입니다.

Vue, Solid, Preact, Qwik 및 Angular에서 채택한 것은 어쩌면 놀라운 일이 아닐 수도 있습니다. 우리는 Leptos와 Sycamore가 DOM에서 WASM이 느릴 필요가 없음을 보여주면서 Rust에 도입이 되는 것을 보았습니다. 심지어 React에서 내부적으로 사용하도록 고려 중입니다.

리액트용 가상 DOM은 항상 구현 세부 사항일 뿐이었기 때문에 어쩌면 당연한 결과일지도 모릅니다.

Signal과 반응형 언어는 모든 것이 수렴되는 곳인 것 같습니다. 하지만, 자바스크립트가 처음 등장했을 때는 그렇게 명확하지 않았습니다. 아마도 자바스크립트가 이를 위한 최적의 언어가 아니었기 때문일 겁니다. 저는 요즘 프런트엔드 프레임워크 설계에서 느끼는 많은 고통이 언어 문제라고 말하고 싶습니다.

이 모든 것이 어디로 귀결되든 지금까지 꽤 긴 여정이었습니다. 그리고 많은 분이 Signal에 관심을 두시는 만큼, 다음 단계로 나아갈 수 있을지 기대가 됩니다.

🚀 한국어로 된 프런트엔드 아티클을 빠르게 받아보고 싶다면 Korean FE Article(https://kofearticle.substack.com/)을 구독해주세요.

--

--