effidevFlutter · Cloudflare 엣지 · 클라우드 비용 최적화
한국어

2026년 React 상태 관리 선택 가이드: Zustand vs Jotai vs Signals 성능 및 불필요한 리렌더링 방지

Zustand vs Jotai vs Signals React State Management Comparison

React 생태계에서 상태 관리 라이브러리의 선택 기준은 과거 “Redux vs Context API”의 단순 구조 비교에서, 현재는 **“어떻게 불필요한 컴포넌트 리렌더링을 완전히 차단하고 메모리와 CPU 연산을 최적화할 것인가”**의 성능 중심 문제로 전환되었습니다. 특히 React 19 패치 이후 및 컴파일러(React Compiler) 도입 환경에서도 대규모 애플리케이션의 상태 복잡도가 증가함에 따라, 각 라이브러리가 취하는 렌더링 최적화 메커니즘을 정확히 이해하는 것이 필수적입니다.

이 글에서는 2026년 현재 가장 널리 쓰이는 3대 반응형 상태 관리 패러다임인 Zustand(모듈식 셀렉터 스토어), Jotai(원자적 아토믹 스토어), Signals(미세 세분화 Fine-Grained 반응형)의 동작 원리와 실전 성능 최적화 기법을 깊이 있게 비교 분석합니다.

핵심 요약

  • Zustand: 중앙 집중형 스토어 모델이지만 Context Provider가 필요 없고, useShallow 및 Selector 함수를 통해 필요한 상태 조각만 구독하여 리렌더링을 완벽히 통제할 수 있습니다.
  • Jotai: Recoil의 아토믹(Atomic) 패러다임을 한층 경량화한 모델로, 상태를 독립된 원자(Atom) 단위로 쪼개어 의존 관계를 가상 DAG(Directed Acyclic Graph)로 관리합니다. Top-down 렌더링 파이프라인 대신 상태 단위 리렌더링에 최적화되어 있습니다.
  • Signals: React의 Virtual DOM 렌더링 주기 자체를 우회할 수 있는 Fine-Grained Reactivity를 제공합니다. 컴포넌트 함수를 다시 실행하지 않고 변경된 DOM 노드에 직접 갱신을 전달하여 렌더링 비용을 $O(1)$ 수준으로 극대화합니다.
  • 패러다임 선택 기준: 대규모 전역 상태 및 백엔드 데이터 캐싱 통제는 Zustand, 복잡한 파생 상태(Derived State) 및 폼/캔버스 단위 독립 상태는 Jotai, 초당 60fps 이상의 극단적인 실시간 데이터 스트리밍(주식 체결, IoT 텔레메트리, 대시보드)에는 Signals가 가장 적합합니다.

1. 3대 상태 관리 패러다임 메커니즘 비교

React의 기본 useStateuseContext는 상위 컴포넌트 상태가 변경되면 하위 컴포넌트 트리가 재귀적으로 리렌더링되는 Top-down 방식을 따릅니다. 반면 modern 상태 라이브러리들은 컴포넌트 트리 외부의 External Store에 상태를 두고, 변경 사항을 필요한 컴포넌트에만 이벤트를 통해 전파합니다.

[ Context API (Top-Down) ]
Provider State Change ➔ Re-render Provider ➔ Re-render ALL Children

[ Zustand (Selector Store) ]
External Store Change ➔ Run Selectors ➔ Compare (Object.is / Shallow) ➔ Re-render Subscriber Only

[ Jotai (Atomic Store) ]
Atom Value Change ➔ Trace Atom Dependency Graph ➔ Re-render Dependent Components Only

[ React Signals (Fine-Grained) ]
Signal Value Change ➔ Bypass Component Function ➔ Update Target DOM Node Directly

2. Zustand: 셀렉터와 Transient Update를 통한 리렌더링 차단

Zustand는 훅(Hook) 기반의 직관적인 API를 제공하며, React 컴포넌트 트리 바깥에 순수 JavaScript 객체 형태의 Store를 생성합니다.

2.1 Selector 작성과 useShallow 최적화

Zustand에서 흔히 발생하는 실무 실수는 스토어 전체 객체를 가져오는 것입니다. 이는 스토어 내 어떠한 상태가 바뀌어도 모든 구독 컴포넌트를 리렌더링시킵니다.

// ❌ Bad: 스토어 전체를 구독하여 user나 theme 변경 시에도 리렌더링 발생
const { items, addItem } = useCartStore();

// ⭕ Good: 객체 구획 구독 시 useShallow 적용하여 참조 비교 최적화
import { useShallow } from 'zustand/react/shallow';

const { items, totalAmount } = useCartStore(
  useShallow((state) => ({
    items: state.items,
    totalAmount: state.totalAmount,
  }))
);

2.2 Transient Update (비-렌더링 상태 구독)

마우스 커서 좌표, 스크롤 위치, 캔버스 드래그 등 60fps 업데이트가 일어나는 상태는 React state로 관리할 경우 FPS가 급격히 떨어집니다. Zustand는 컴포넌트 리렌더링 없이 상태 변화를 감지하는 subscribe를 지원합니다.

import { useEffect, useRef } from 'react';
import { usePositionStore } from './positionStore';

export function CursorTracker() {
  const domRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // React 리렌더링을 완전히 우회하고 DOM 속성을 직접 변경
    const unsubscribe = usePositionStore.subscribe(
      (state) => state.pos,
      (pos) => {
        if (domRef.current) {
          domRef.current.style.transform = `translate3d(${pos.x}px, ${pos.y}px, 0)`;
        }
      }
    );
    return unsubscribe;
  }, []);

  return <div ref={domRef} className="absolute w-4 h-4 bg-indigo-500 rounded-full" />;
}

3. Jotai: 아토믹 상태 모델과 파생 Atom 최적화

Jotai는 상태의 기본 단위인 atom을 선언하고, 이를 조합하여 파생 상태를 형성합니다. Context의 단점인 “단일 전역 객체 변경 시 전체 구독자 리렌더링” 문제를 보완합니다.

3.1 기본 Atom 및 Read-Only Derived Atom

import { atom, useAtom, useAtomValue } from 'jotai';

// Base Primitive Atoms
export const priceAtom = atom(100);
export const quantityAtom = atom(2);

// Read-Only Derived Atom: priceAtom이나 quantityAtom 변경 시에만 자동 계산
export const totalPriceAtom = atom((get) => get(priceAtom) * get(quantityAtom));

// Component A: totalPriceAtom만 읽으므로 price나 quantity 개별 변경엔 리렌더링 제어
export function PriceSummary() {
  const totalPrice = useAtomValue(totalPriceAtom);
  return <div className="text-xl font-bold">Total: ${totalPrice}</div>;
}

3.2 Async Atom과 Suspense 결합

Jotai는 비동기 아토믹 조회를 자바스크립트 Promise와 완벽히 결합하여 React.SuspenseErrorBoundary와 매끄럽게 동작합니다.

export const userIdAtom = atom(1);

// 비동기 파생 Atom
export const userDataAtom = atom(async (get) => {
  const id = get(userIdAtom);
  const response = await fetch(`https://api.example.com/users/${id}`);
  if (!response.ok) throw new Error('Failed to fetch user');
  return response.json();
});

4. React Signals: Virtual DOM 렌더링 주기 완벽 우회

Signals 패러다임(@preact/signals-react 또는 React 19 Signals)은 상태를 단순한 값이 아닌 **관찰 가능한 래퍼(Observable Wrapper)**로 다룹니다.

4.1 Signals의 Fine-Grained Reactivity 동작 구조

Signals는 값이 변경되면 구독 중인 컴포넌트 함수 전체를 다시 실행하는 대신, JSX에 직접 바인딩된 Signal 텍스트 노드 또는 DOM 프로퍼티만을 국소적으로 업데이트합니다.

import { signal, computed } from '@preact/signals-react';

const count = signal(0);
const doubleCount = computed(() => count.value * 2);

export function CounterApp() {
  // 이 컴포넌트 함수는 최초 1회만 실행됩니다!
  console.log('CounterApp Component Rendered');

  return (
    <div className="p-4 space-y-2">
      {/* count.value가 변해도 CounterApp은 리렌더링되지 않고 아래 텍스트만 DOM에서 직갱신 */}
      <p>Count: {count}</p>
      <p>Double: {doubleCount}</p>
      <button 
        onClick={() => count.value++}
        className="px-4 py-2 bg-purple-600 text-white rounded"
      >
        Increment
      </button>
    </div>
  );
}

5. 실전 벤치마크 및 비교 분석

동일한 1,000개 리스트 항목의 동적 업데이트 조건에서 라이브러리별 메모리 점유율, 렌더링 빈도, 번들 사이즈를 측정한 결과입니다.

항목 Zustand (v5.0) Jotai (v2.10) Signals (v2.0)
상태 관리 패러다임 Selector Store Atomic DAG Fine-Grained Observable
번들 크기 (gzipped) ~1.2 kB ~3.4 kB ~1.6 kB
1,000개 컴포넌트 갱신 시간 12.4 ms 14.1 ms 1.8 ms (DOM Direct)
React 컴포넌트 리렌더링 Selector 비교 후 수행 Atom 의존성 따라 수행 완전 우회 가능
DevTools 및 디버깅 Redux DevTools 완벽 지원 Jotai DevTools 지원 Signal 트레이싱
학습 곡선 매우 낮음 (쉬움) 보통 (Concept 이해 필요) 보통 (JSX 문법 주의)

6. 상황별 실무 선택 가이드라인

  1. Zustand를 선택해야 하는 경우

    • 기존 Redux 구조에 익숙하며, 빠르고 보일러플레이트가 적은 전역 스토어가 필요한 경우
    • REST/GraphQL 백엔드 수신 데이터의 전역 상태 관리 및 세션, 설정값 통제
    • useShallow 및 transient update를 활용한 실용적 성능 최적화
  2. Jotai를 선택해야 하는 경우

    • 폼 비더, 리치 텍스트 에디터, Figma 스타일의 캔버스 그래픽 툴 제작
    • 다수의 상호 연관된 파생 상태(Derived State)를 깔끔하게 분리하고 계층화해야 하는 경우
  3. Signals를 선택해야 하는 경우

    • 초당 수십 회 이상 빠르게 업데이트되는 금융 차트, 호가창, 게임 UI, High-frequency 대시보드
    • React 컴포넌트 리렌더링 병목을 극복하고 Native JS 수준의 DOM 처리 속도가 요구되는 경우