Ray Book
상태 관리의 발전

수렴, 결국 같은 곳을 향하고 있다

Signals, fine-grained reactivity, 그리고 프레임워크들이 수렴하는 지점, 상태 관리의 미래를 정리합니다.

state-managementsignalsreactivityfrontend

각자의 길, 같은 방향

4편에 걸쳐 상태 관리의 발전을 추적했습니다. 각 프레임워크는 서로 다른 출발점에서 시작했습니다.

  • Vue , 반응성 시스템 위의 Vuex에서, mutation을 버린 Pinia로
  • Angular , Zone.js의 전체 검사에서, Signals의 세밀한 추적으로
  • React , Context의 전체 리렌더링에서, Zustand/Jotai의 선택적 구독으로

경로는 달랐지만, 방향은 동일합니다. 더 적은 코드로, 더 정밀하게, 변경된 곳만 업데이트합니다.

세밀한 반응성이라는 수렴점

현재 프론트엔드 프레임워크들이 수렴하고 있는 기술적 지점은 세밀한 반응성 (fine-grained reactivity) 입니다. 상태의 최소 단위를 추적하여, 그 값이 변경되면 정확히 영향받는 부분만 업데이트하는 방식입니다.

각 프레임워크에서 이 개념이 어떻게 표현되는지 비교해봅시다.

// Angular Signals
const count = signal(0);
const doubled = computed(() => count() * 2);
// Vue ref/reactive
const count = ref(0);
const doubled = computed(() => count.value * 2);
// Solid Signals
const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2);
// Svelte 5 Runes
let count = $state(0);
const doubled = $derived(count * 2);

문법은 다르지만 구조가 동일합니다. 반응성 값을 선언하고, 파생 값을 정의하면, 의존성이 자동으로 추적됩니다. 원본 값이 바뀌면 파생 값이 재계산되고, 그 값을 사용하는 UI만 업데이트됩니다.

왜 Signals로 수렴하는가

Signals가 여러 프레임워크에서 동시에 채택되는 것은 우연이 아닙니다. 이전 세대의 접근들이 가진 공통 한계를 해결하기 때문입니다.

Virtual DOM의 한계. React의 Virtual DOM은 "전체를 다시 렌더링하고 diff를 계산한다"는 접근입니다. 개념적으로 단순하지만, 변경되지 않은 부분까지 vDOM을 생성하고 비교하는 비용이 있습니다. Signals는 변경된 값을 직접 추적하므로 diff 자체가 불필요합니다.

Zone.js의 한계. Angular의 Zone.js는 "무언가 바뀌었을 수 있다"만 알려줄 뿐, "무엇이 바뀌었는지"는 알지 못합니다. Signals는 의존성 그래프로 정확히 추적합니다.

전체 리렌더링의 한계. Context나 setState가 트리거하는 리렌더링은 범위가 넓습니다. Signals는 값 단위로 구독하므로 최소한의 업데이트만 발생합니다.

상태 관리 라이브러리의 미래

Signals가 프레임워크 수준에서 반응성을 해결하면, 별도의 상태 관리 라이브러리가 필요한 영역은 좁아집니다.

프레임워크 내장 반응성이 커버하는 영역

과거에 별도 라이브러리가 필요했던 것현재 프레임워크가 제공하는 것
컴포넌트 간 상태 공유Vue의 composable, Angular의 Signal Service
파생 상태computed / $derived / createMemo
변경 감지 최적화Signals가 자동으로 처리
상태 변경 추적DevTools 통합

여전히 별도 도구가 필요한 영역

  • 서버 상태 관리 , TanStack Query, SWR 같은 라이브러리가 담당하는 캐싱, 재검증, 낙관적 업데이트는 프레임워크 반응성만으로 해결되지 않습니다
  • 대규모 클라이언트 상태 , 오프라인 지원, 상태 지속성, 복잡한 undo/redo가 필요한 앱에서는 여전히 전용 스토어가 유용합니다
  • React 외부 접근 , React 생태계에서 Zustand처럼 "React 밖에서도 상태에 접근"해야 하는 경우는 프레임워크 내장 메커니즘만으로는 어렵습니다

보일러플레이트는 왜 줄어드는가

1편에서 다뤘던 Redux의 카운터를 다시 봅시다.

// Redux (2015), 카운터 하나에 필요한 코드
const INCREMENT = 'INCREMENT';
function increment() { return { type: INCREMENT }; }
function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case INCREMENT: return { ...state, count: state.count + 1 };
    default: return state;
  }
}
const mapStateToProps = state => ({ count: state.count });
export default connect(mapStateToProps, { increment })(Counter);

같은 기능을 2026년에 각 프레임워크로 구현하면 이렇습니다.

// Zustand (2024)
const useCounter = create((set) => ({
  count: 0,
  increment: () => set((s) => ({ count: s.count + 1 }))
}));
// Vue + Pinia (2024)
const useCounter = defineStore('counter', () => {
  const count = ref(0);
  const increment = () => count.value++;
  return { count, increment };
});
// Angular Signals (2024)
@Injectable({ providedIn: 'root' })
export class CounterService {
  count = signal(0);
  increment = () => this.count.update(v => v + 1);
}
// Svelte 5 (2024)
let count = $state(0);
function increment() { count++; }

보일러플레이트가 줄어든 이유는 명확합니다. 상태 변경을 추적하는 책임이 개발자에서 프레임워크로 넘어갔기 때문입니다. Redux에서 개발자가 Action, Reducer, connect를 작성한 이유는 "상태가 어떻게 변경되었는지"를 명시적으로 기록하기 위해서였습니다. Signals 기반 시스템에서는 프레임워크가 의존성을 자동으로 추적하므로, 그 보일러플레이트가 불필요해진 것입니다.

하나의 질문

이 시리즈에서 다룬 모든 라이브러리와 패턴은 결국 하나의 질문을 다듬어온 과정입니다.

"상태가 바뀌었을 때, 최소한의 비용으로 정확히 영향받는 부분만 업데이트하려면 어떻게 해야 하는가?"

jQuery 시대에는 이 질문 자체가 없었습니다. Backbone에서 처음 상태와 뷰를 분리했고, Flux/Redux에서 변경을 추적 가능하게 만들었고, 각 프레임워크가 자신만의 방식으로 최적화해왔습니다. 그리고 지금, Signals라는 공통 답에 수렴하고 있습니다.

도구는 계속 바뀌겠지만, 이 질문은 프론트엔드 개발이 존재하는 한 사라지지 않을 것입니다. 중요한 것은 특정 라이브러리의 API를 외우는 것이 아니라, 각 도구가 이 질문에 어떤 답을 내놓았는지를 이해하는 것입니다.