[번역] 상태 관리에서 Signals가 React를 대체 할 수 있을까요?

“Signal” 라는 용어가 빠르게 퍼지고 있습니다. 반응형 상태를 저장 할 때 SolidJS, Qwik, Preact 같은 최신 프레임 워크들은 signals를 사용하는것을 선호합니다.
이는 성능 이점과 제공되는 사용 편의성 때문입니다. signals는 상태를 정말 세밀하게 제어 할 수 있을 뿐만 아니라 개발자 경험 또한 향상시켜주기 위한 설계된 여러 기능을 포함하고 있습니다.

Signals 이 뭘까요?

signals은 Knockout.js 라는 프레임워크로 부터 영감을 받은것으로 사실 새로운 개념은 아닙니다.
Knockout.js의 아이디어는 간단합니다: View, ViewModels 그리고 Observables 이 세 개의 기본 컴포넌트가 있는데 ViewModel 은 UI의 데이터 구성요소 이며, ViewViewModel에 바인딩된 UI 요소 입니다. 그리고 Observable 은 javascript 객체의 한 유형으로, 자신이 변경될 때 구독자에게 알림을 보내는 역할을 합니다. 하나의 ViewModel 은 Observable들의 집합이며, 하나의 View 는 이중 하나를 구독하여 변경이 될 때마다 knockout.js는 뷰를 업데이트 하게 됩니다.
signals 작동 원리 또한 이것과 유사합니다. signal을 생성하면, getter와 setter 를 사용 할 수 있게 됩니다.
  • getter: value를 반환하는 함수
  • setter: value를 업데이트 하는 함수
useState 와 유사하지만, useState는 getter 와 달리 값을 직접 반환합니다.
notion image
즉, getter를 통해 값을 얻기 위해서는 다음과 같이 함수의 형태로 호출해줘야 합니다.
하지만 왜 getter를 사용하는것 일 까요?

Getter vs Value

signals는 value를 반환하는 대신 getter를 반환합니다. getter는 실제 value에 대한 참조를 유지하기 때문에 언제 사용하든 항상 최신 값을 얻어 올 수 있습니다.
예를 들어 아래에 react와 solidJS로 작성된 두 개의 코드가 있습니다.
notion image
예상 할 수 있듯이 react에서의 setInterval 함수는 항상 이전 값인 0을 가지고 있는 반면 solidJS는 항상 최신값이 포함하게 됩니다.
notion image

Signals는 반응성을 어떻게 구현할까요?

signals은 실제로 반응형이며 반응성은 상태에 대한 구독 생성을 통해 구현 됩니다.
signal은 변경에 반응하기 위해 어떤 부분이 변경되어야 하는지 알아야 합니다. signal은 상태 업데이트가 필요한 대상을 추적하기 위해 getter를 사용합니다.
getter는 호출된 모든 컨텍스트를 추적하여 signal의 상태에 변경 점이 발생하면 어떤 부분이 업데이트 되어야 되는지 알게됩니다.
결과적으로 상태가 변경 될 때 필요한 부분에 대해서만 업데이트를 진행합니다.
counter() 함수는 main 요소의 깊숙한 곳에 있으며, <p> 의 text node로 사용되기 때문에 코드가 실행 될 때의 컨텍스트, 즉 text node를 구독하게 됩니다.
Increment 버튼를 클릭하면 SolidJS는 전체 트리를 업데이트하는 것이 아닌 <p> 요소의 text node만을 업데이트 합니다.
notion image
컴포넌트가 리렌더링되는 것을 방지 할 뿐만 아니라, 요소 내부의 text node만을 업데이트하는 높은 수준의 정밀도를 자랑합니다.
이것을 통해 solidJS의 컴포넌트가 왜 react 처럼 상태 변화가 발생 할 때마다 리랜더링 되지 않고 한 번만 실행되는 이유를 설명 해줍니다.
 

Signals vs useState

useState는 상태 변수와 setter 함수를 반환합니다. 상태 값이 직접 반환되기 때문에 react는 JSX 내부 또는 다른 컴포넌트에서 상태가 사용된 위치를 추적 할 수 없습니다. 따라서 상태 변화에 반응하는 방법은 컴포넌트 트리를 리렌더링 하는것 뿐입니다.
이로 인해 react는 solidJS와 같은 프레임워크 보다 더 많은 계산을 수행하게 됩니다. 이제 이 둘을 비교하며 어떤 것이 더 나은 성능을 보이는지 확인해보겠습니다.

Component Level Performance

하나는 react useState 다른 하나는 solidJS와 signals를 사용한 counter 예제를 통해 각 각 리렌더링이 얼마나 발생하는지 비교해보겠습니다. (console 창을 확인해주세요!!)
React Counter:
SolidJS Counter:
react 컴포넌트의 경우 상태에 변화가 생길 때마다 리렌더링이 발생하는 반면 solidJS 컴포넌트의 경우 한 번만 랜더링 되는 것을 확인 할 수 있습니다. solidJS의 업데이트는 필요한 위치에 대해서만 발생하게 됩니다.
성능 측면에서는 signal의 명백한 승리입니다.

Props Drilling

signal은 구독을 통해 상태 변화에 응답하기 때문에 필요한 부분에 대해서만 업데이트 하고 하위 요소들은 렌더링하지 않으면서 다른 컴포넌트들에 깊이 전달 할 수 있습니다.
react에서 상태를 컴포넌트 깊이 전달할 경우 상태 변화시 모든 하위 컴포넌트들이 리렌더링 하게 됩니다.
동일하게 counter 예제를 가지고 react와 solidJS가 어떻게 다르게 동작하는지 알아보겠습니다.
React Counter:
SolidJS Counter:
solidJS 경우 각 컴포넌트를 한 번만 렌더링 합니다. 추가 업데이트는 최종 하위 컴포넌트의 관련 text node에만 적용되지만 react의 경우 상태에 변화가 생기면 상태 값을 사용하고 있지 않은 하위 컴포넌트 일지라도 모든 하위 컴포넌트들이 리렌더링 됩니다. 애플리케이션 규모가 큰 경우 성능에 영향을 줄 수 있습니다. 물론 memo 함수 활용을 통해 렌더링을 줄일수 있습니다. 하지만 props drilling 는 여전히 리렌더링을 발생시킵니다.

Signals와 useRef의 작동방식은 동일 할 까요?

useState 가 많은 리렌더링을 필요로 하는것은 사실입니다. 그리고 useRef 가 리렌더링 간에 상태를 유지한다는 점에서 useState 와 유사하게 동작한다고 주장할 수 있습니다. 그렇다면 useRef 는 signal의 대안으로 사용될 수 있을까요?
답은 아니요 입니다. useRef 가 상태를 유지하지만 값이 변경되어도 리렌더링을 발생시키지 않습니다. 때문에 useRef반응형이 이 아닙니다.
signals의 경우 useStateuseRef 그리고 memoization의 조합으로 생각할 수 있습니다. 상태 처럼 반응형 이고 refs 처럼 리렌더링을 발생시키지 않으며 memoization 처럼 필요한 곳에만 업데이트를 적용합니다.

Signals의 장점

Performance (성능)

signals는 구독 기반으로 작동함으로 관련 부분에 대해서만 업데이트를 진행합니다. 이를 fine-grained reactivity (세밀한 반응성) 라고 하는데 변경이 필요한 가장 작은(또는 세분화된) 요소에 대해서만 업데이트를 진행한다는 것입니다.

Decoupled (디커플링)

signals를 컴포넌트 내부에서만 생성하고 사용할 필요는 없습니다. 별도로 분리된 파일에 signals를 생성하고 export 하여 다른 파일에 import 하여 사용 할 수 도 있습니다.
notion image
상태를 컴포넌트에서 분리하여 관리하고자 할 때 유용한 기능이지만 다른곳에 있을 수 있는 모든 signals를 추적해야 되기 때문에 문제가 발생 할 수도 있습니다.

Synchronous (동기화)

signal 상태 업데이트는 동기적으로 처리됩니다. 이는 업데이트 즉시 사용 할 수 있음을 의미합니다. 반면 react는 상태 업데이트를 비동기적으로 일괄 처리합니다.

No Need for a Virtual DOM (가상돔이 필요 없음)

signals는 구독 패턴에 따라 작동하며 변경사항을 실제 DOM내에 필요 위치에 직접 적용합니다. VirtualDOM과 모든 종류의 diffing 알고리즘은 필요하지 않습니다.
결과적으로 오버헤드를 줄이고 성능이 향상 시키고, 더 적은 javascript를 브라우저에 제공합니다.
따라서 solidJS, Qwik 같은 모던 프레임워크에서는 VirtualDOM 사용하지 않고 signals을 활용하여 반응형 상태를 처리합니다.

Signals의 단점

Initial Learning Curve (초기 러닝 커브)

signals는 구독 패턴 및 getter 사용으로 초기 러닝커브를 유발 할 수 있습니다. 하지만 대부분의 대부분의 모던 프레임워크는 개발자 경험을 개선하고 러닝커브를 줄이기 위해 사용하기 쉽고 선언적인 API를 제공합니다.

Subscription Are Not Disposable (구독을 해제 할 수 없음)

반응형 업데이트를 하기 위해 생성된 구독은 해제 되지 않습니다. 예를 들어 컴포넌트 외부에서 생성한 signal의 경우 컴포넌트가 언마운트 된 이후에도 메모리에 유지 됩니다.
그러나 solidJS, Qwik 같은 프레임워크는 이러한 사례를 처리하고 성능을 최적화하는 역할을 수행합니다.

Signals로 전환하는것을 고려해야 할 까요?

성능으로 보면 signals은 최고입니다. 세밀화된 반응형, 불필요한 리렌더링도 없음, 향상된 성능, VirtualDOM을 사용하지 않음으로 인한 오버헤드 감소, 브라우저에 더 적은 양의 javascript 제공 등을 제공합니다.
반면 react는 메모이제이션, 사이드이펙트 등 측면에서 더 많은 유연성을 제공합니다. 아마 들어보셨겠지만 react는 컴포넌트와 값를 지능적으로 메모하고 콜백을 적용하여 성능을 향상 시킬수 있는 새로운 컴파일러를 도입하고 있습니다. 간단히 말하자면 signal의 단점을 피할 수 있도록 fine-grained reactivity (세밀한 반응성)이 제공될 것입니다.
이 둘 사이에는 상호 교환관계가 있으며 각각의 자체 사용사례에 잘 맞추어져 있습니다.
개인적으로는 signals가 상태보다 우수하다고 생각하고 있으며, 새로운 그리고 일부 오래된 프레임워크들(Angular, Preact)이 signal를 채택함으로써 앞으로 상태 관리의 대표적인 선택지가 될 수 있다고 생각합니다.

결론

  • 많은 신규 그리고 기존 프레임워크에서 채택하고 있는 signals는 현재 뜨거운 주제 입니다. 이유는 구독 기반 패턴으로 제공되고 있는 세밀한 반응성 때문입니다.
  • VirtualDOM이 필요하지 않아 상태 업데이트 중 diffing이 발생하지 않아 오버가 적고 브라우저에 전송되는 javascript고 적습니다.
  • 상태보다 더 우수한 성능을 보여주는 이유는 불필요한 리렌더링을 수행하지 않기 때문입니다. (변경사항을 DOM에 직접 적용합니다)
  • signals은 상태보다 훨씬 우수하며 미래의 상태관리의 선택지 중 하나가 될 수 있습니다.
 

© 2024 dan.dev.log, All right reserved.

Built with NextJS