반응성의 진화: UI 업데이트가 스스로를 관리하는 방법

발행: (2025년 12월 8일 오전 10:54 GMT+9)
10 min read
원문: Dev.to

Source: Dev.to

간략한 역사

2010년, Knockout.js가 프론트엔드 세계에 ObservableComputed 개념을 도입했습니다.

처음으로 브라우저는 데이터를 먼저 말하게 하고—UI가 자동으로 따라오게 하는 실용적인 방법을 갖게 되었습니다.

그 순간부터 모든 주요 프레임워크 뒤에 있던 핵심 논쟁은 다음과 같았습니다:

프레임워크가 데이터를 적극적으로 “체크”해야 할까, 아니면 데이터가 프레임워크에 능동적으로 “알려”줘야 할까?

돌이켜보면 Knockout은 인기도 경쟁에서 승리하지 못했지만, 결국 Angular, React, Vue라는 큰 3대 프레임워크를 형성한 개념적 씨앗을 심었습니다. 자동 반응성(reactivity)이라는 아이디어가 모든 것을 바꾸었습니다.

이 글은 UI 반응성(Functional Reactive Programming과 관련은 있지만 동일하지 않음)에 초점을 맞추고, 그것이 Angular, React, Vue, 그리고 궁극적으로 현대 Signals 움직임에 어떻게 영향을 미쳤는지 살펴봅니다.

반응성이 실제 의미하는 바

반응성은 UI 업데이트 방식을 다음과 같이 바꿉니다:

❌ “데이터가 바뀔 때 DOM을 수동으로 변경한다”

✅ “UI가 어떻게 보여야 하는지를 기술한다—시스템이 나머지를 처리한다.”

핵심 원칙

선언형
UI가 어떻게 보여야 하는지를 지정합니다. 시스템이 화면에 업데이트가 도달하는 방식을 결정합니다.

의존성 추적
프로그램이 처음으로 값을 읽을 때, 시스템은 “누가 무엇에 의존하는가”를 조용히 기록합니다.

변경 전파
데이터가 바뀌면 시스템은 무효화 신호를 모든 의존자에게 보내고, 필요한 부분만 업데이트합니다.

왜 중요한가?

  • 정신적 부하 감소: “X를 업데이트했는지 기억하고 있나?” 하는 고민이 사라집니다.
  • 성능: 실제로 바뀐 부분만 업데이트—전체 페이지를 다시 그릴 필요가 없습니다.
  • 데이터 흐름 명확화: 디버깅이 쉬워지고, 동작이 예측 가능해집니다.

두 가지 핵심 전략: 누가 먼저 말하는가?

전략전형적인 구현키워드
Pull (프레임워크가 데이터를 요청)loops, diffingdirty‑checking, VDOM diff
Push (데이터가 프레임워크에 알림)watchers, signalsobservable, effect

대부분의 최신 프레임워크는 실제로 하이브리드입니다: 데이터가 무효화를 푸시 → 프레임워크가 적절한 시점에 계산이나 diff를 풀(pull)합니다.

네 가지 반응성 모델

Pull ↔ Push 스펙트럼에 주요 접근 방식을 배치하면, 반응성이 어떻게 진화했는지 명확한 타임라인을 얻을 수 있습니다.

pull push timeline

요약 표

모델업데이트 흐름Push/Pull 위치세분화 정도대표 프레임워크
Dirty‑checking$digest가 모든 $watch를 스캔 → 동기식 업데이트Pure Pull표현식 단위; 워처가 많아질수록 성능 저하AngularJS 1.x
Virtual DOM diffsetState가 dirty 플래그를 푸시 → 배치 재렌더 → VDOM diff → DOM 패치Hybrid컴포넌트 서브트리; 간단한 정신 모델이지만 과도하게 렌더링될 수 있음React, Preact, Vue 2
Watcher / Observable Graphsetter가 푸시 → 워처가 자신의 서브트리만 재계산Push‑leaninggetter 기반 의존성 추적; 더 세밀한 granularityVue 2 Watchers, MobX
Fine‑grained Signalssetter가 푸시 → 값이 lazy하게 재계산을 pull → 직접 DOM 업데이트Hybrid (runtime) 또는 compile‑time에 거의 Pure Pushprop 수준 또는 DOM‑node 수준 정밀도; VDOM 없음Solid.js, Angular Signals, Svelte 5 Runes

모델 상세

Dirty‑Checking

Dirty checking flow

순수 Pull 모델. 프레임워크가 모든 감시된 표현식을 반복적으로 스캔해 변화 여부를 확인합니다. 예측 가능하지만 비용이 많이 듭니다—성능은 워처 수에 비례합니다.

Virtual DOM Diff

vdom flow

React는 배칭, 무효화 플래그, 그리고 diff 단계를 추가합니다. 푸시 단계에서 컴포넌트를 dirty 로 표시하고, 풀 단계에서 diff를 통해 새로운 UI를 계산합니다. 뛰어난 개발자 경험(DX)을 제공하지만 때때로 불필요한 작업을 수행합니다.

Watcher / Observable Graph

observer flow

의존성은 getter를 통해 설정됩니다. 변경된 값에 직접 의존하는 워처만 다시 실행되므로 불필요한 재계산이 크게 줄어듭니다.

Fine‑Grained Signals

signal flow

큰 컴포넌트 트리를 다루는 대신, Signals는 가능한 가장 작은 반응 단위에서 동작합니다:

  • 값(value)
  • 메모(memo)
  • 심지어 원시 DOM 노드

런타임 시그널(Solid, Angular Signals)은 push 무효화 + lazy pull 재계산을 사용합니다.
컴파일 타임 시그널(Svelte 5)은 대부분의 작업을 컴파일러로 옮겨, 거의 순수 Push 모델에 가깝게 만듭니다.

주요 비교

1. Push vs Pull — 누가 작업을 시작하는가?

  • Dirty‑checking: 순수 Pull
  • Virtual DOM: push (dirty) → pull (diff)
  • Watcher / Proxy / Signals: push 무효화 → pull 평가(lazy)
  • Compile‑time Signals: 컴파일 시 의존성이 고정되어 순수 Push에 가깝다

2. 의존성 정밀도 — 시스템이 얼마나 정확한가?

  • Virtual DOM: “어떤 컴포넌트 서브트리가 업데이트될 수 있는가” 정도만 알음.
  • Runtime Signals / Proxies: “어떤 메모나 DOM 노드가 정확히 바뀌었는가”를 알음.
  • Compile‑time Signals: 최종 DOM 조작을 직접 내보내며 가장 높은 정밀도를 제공.

3. 스케줄링 — 업데이트가 실제로 언제 실행되는가?

  • React / Solid: 마이크로태스크를 통한 배칭; 여러 쓰기를 하나의 틱으로 축소
  • Vue 3: 작업 큐(job queue)
  • Dirty‑checking: 동기 루프; 비용이 선형적으로 증가

4. 정신 모델 — 코딩 느낌은 어떠한가?

  • Signals / MobX: “값만 바꾸면 된다”는 느낌—시스템이 전파를 담당
  • Virtual DOM: “render ≠ paint” 선언형 워크플로우를 수용
  • Compile‑time Signals: 순수 JavaScript에 가깝고, 컴파일러와 IDE가 예측 가능성을 유지

결론

반응성 역사는 본질적으로 PushPull 사이의 균형을 맞추는 과정이었습니다.

  • 우리는 순수 Pull 세계인 dirty‑checking에서 시작했습니다.
  • 그 다음 Virtual DOM 같은 하이브리드 모델이 등장했습니다.
  • 이후 의존성 그래프 기반 시스템이 세밀한 Push + Pull 하이브리드를 만들었습니다.

런타임 시그널은 push 무효화 + lazy pull 평가를 주로 사용하고, 컴파일 타임 시그널은 작업을 빌드 단계로 옮겨 순수 Push 반응성에 가깝게 만듭니다.

이제 각 모델이 다음 측면에서 어떻게 다른지 명확히 이해했을 것입니다:

  • 누가 업데이트를 시작하는가?
  • 효과가 얼마나 멀리까지 전파되는가?

하지만 아직 답이 없는 중요한 질문이 하나 남았습니다:

Even in a fine‑gr

Back to Blog

관련 글

더 보기 »