개발 일기

구독-발행(Pub-Sub) 패턴을 중심으로 본 Zustand의 동작방식

Kody_with_the_K 2025. 3. 5. 00:52

안녕하세요, 프론트엔드 개발자 코디(Kody)입니다.

이번 글에서는 Pub-Sub 패턴을 중심으로 Zustand가 상태를 관리하는 방식을 분석하고, 이를 활용하여 효율적인 상태 관리 방법을 정리해보려 합니다.

 

1. 구독-발행(Pub-Sub) 패턴이란?

상태 관리 라이브러리를 사용하다 보면, 특정 값이 변경될 때 해당 값을 사용하는 여러 컴포넌트가 자동으로 업데이트되는 경우가 많습니다. 그런데, 이런 자동 업데이트는 어떻게 이루어지는 걸까요?

 

이러한 동작 방식의 핵심 개념 중 하나가 바로 구독-발행(Pub-Sub, Publish-Subscribe) 패턴입니다.

 

Pub-Sub 패턴의 개념

Pub-Sub(Publish-Subscribe) 패턴은 이벤트 기반 아키텍처에서 자주 사용되는 설계 패턴입니다.

Publisher(발행자): 특정 이벤트를 발생시키는 역할을 합니다. 상태를 변경하거나 특정 작업을 실행하면, 이 정보를 발행(Publish) 합니다.

Subscriber(구독자): 특정 이벤트를 구독(Subscribe)하고 있다가, 해당 이벤트가 발생하면 미리 등록한 로직을 실행합니다.

 

즉, Publisher는 Subscriber의 존재를 모르고, Subscriber는 Publisher의 존재를 알지 않아도 되는 구조입니다. 대신, 중간에 이벤트 시스템이 있어 상태 변경이 발생하면 이를 자동으로 감지하고 필요한 구독자에게 알리는 방식으로 동작합니다.

 

Pub-Sub 패턴을 활용하면 무엇이 좋을까?

Pub-Sub 패턴이 사용되는 가장 큰 이유는 모듈 간의 결합도를 낮출 수 있기 때문입니다.

 

만약 상태가 변경될 때마다 직접 관련된 모든 컴포넌트를 일일이 업데이트해야 한다면, 코드가 복잡해지고 유지보수가 어려워질 것입니다. 하지만 Pub-Sub 패턴을 활용하면 상태를 변경하는 코드(Publisher)와 이를 감지하는 코드(Subscriber)가 분리되기 때문에, 구조적으로 더 유연한 상태 관리가 가능합니다.

 

1️⃣ 결합도를 낮춘다

결합도(Coupling)란, 모듈 간의 의존성이 얼마나 높은지를 나타내는 개념입니다. 결합도가 높을수록 특정 코드가 변경될 때 연쇄적으로 많은 부분을 수정해야 하고, 반대로 결합도가 낮으면 코드 변경이 다른 부분에 영향을 덜 미치므로 유지보수가 쉬워집니다.

Pub-Sub 패턴을 활용하면 Publisher(발행자)와 Subscriber(구독자)가 직접 연결되지 않기 때문에 결합도를 낮출 수 있습니다.

 

❌ 결합도가 높은 코드

아래는 Pub-Sub 패턴을 사용하지 않고, 직접 함수를 호출하는 방식으로 상태를 업데이트하는 예제입니다.

class Store {
  constructor() {
    this.state = { count: 0 };
  }

  increase() {
    this.state.count += 1;
    renderCounter(this.state.count); // 직접 업데이트
    renderLogger(this.state.count); // 직접 업데이트
  }
}

const store = new Store();

function renderCounter(count) {
  console.log(`Counter: ${count}`);
}

function renderLogger(count) {
  console.log(`Log: Count updated to ${count}`);
}

// 버튼 클릭 시 상태 업데이트
document.getElementById("increaseBtn").addEventListener("click", () => {
  store.increase();
});

 

이 코드의 문제점

1. increase() 함수가 renderCounter()renderLogger()를 직접 호출하고 있음 → 결합도가 높음

2. 새로운 구독자를 추가하려면 increase() 함수 내에서 직접 renderXXX() 함수를 추가해야 함

3. 상태 변경 로직과 UI 업데이트 로직이 분리되지 않아서 유지보수가 어려움

 

✅ 결합도를 낮춘 Pub-Sub 패턴 적용

이제 Pub-Sub 패턴을 적용하여 increase() 함수가 어떤 특정 UI 업데이트 함수도 직접 호출하지 않도록 개선해 보겠습니다.

class Store {
  constructor() {
    this.state = { count: 0 };
    this.subscribers = new Set(); // 구독자 리스트
  }

  subscribe(callback) {
    this.subscribers.add(callback);
  }

  unsubscribe(callback) {
    this.subscribers.delete(callback);
  }

  increase() {
    this.state.count += 1;
    this.notify(); // 상태 변경을 알림
  }

  notify() {
    this.subscribers.forEach((callback) => callback(this.state.count));
  }
}

const store = new Store();

// ✅ 구독자 등록 (Counter UI)
store.subscribe((count) => {
  console.log(`Counter: ${count}`);
});

// ✅ 구독자 등록 (Logger)
store.subscribe((count) => {
  console.log(`Log: Count updated to ${count}`);
});

// 버튼 클릭 시 상태 업데이트
document.getElementById("increaseBtn").addEventListener("click", () => {
  store.increase();
});

Pub-Sub 패턴을 적용한 코드의 장점

increase() 함수는 상태만 변경하고, 직접 특정 UI를 업데이트하지 않음

UI가 새롭게 추가되더라도 store.subscribe(newCallback)을 호출하기만 하면 됨

Publisher(상태 변경 로직)와 Subscriber(UI 업데이트 로직)가 완전히 분리되어 유지보수가 쉬움

 

이제 increase() 함수가 UI 업데이트를 직접 신경 쓰지 않고도 상태 변경을 알리기만 하면 됩니다. 즉, 어떤 UI가 상태를 구독하고 있는지 알 필요가 없어지면서 결합도가 낮아집니다.

 

2️⃣ 확장성이 뛰어나다

Pub-Sub 패턴을 활용하면 새로운 기능을 추가할 때 기존 로직을 수정하지 않고도 쉽게 확장할 수 있습니다.

예를 들어, 상태가 변경될 때마다 추가적인 알림을 표시하고 싶다고 가정해 보겠습니다.

 

❌ 기존 방식(결합도가 높은 코드)에서는?

class Store {
  increase() {
    this.state.count += 1;
    renderCounter(this.state.count);
    renderLogger(this.state.count);
    showToast(`Count changed to ${this.state.count}`); // 새로운 기능 추가
  }
}

 

문제점

새로운 기능(showToast())이 추가될 때마다 increase() 내부 코드를 직접 수정해야 함

코드가 점점 복잡해지고 유지보수가 어려워짐

 

✅ Pub-Sub 패턴을 적용하면?

// ✅ 새로운 구독자 추가
store.subscribe((count) => {
  showToast(`Count changed to ${count}`);
});

 

기존 코드 수정 없이 새로운 기능을 추가할 수 있음!

기존 increase() 로직을 전혀 수정하지 않고 새로운 기능을 추가 가능

확장성이 뛰어나며, 새로운 기능이 필요할 때마다 subscribe()만 호출하면 됨

 

3️⃣ 여러 구독자가 동시에 상태 변경을 감지할 수 있음

Pub-Sub 패턴을 사용하면 한 번의 상태 변경으로 여러 개의 구독자가 동시에 업데이트될 수 있습니다.

예를 들어, 상태가 변경될 때

UI를 업데이트하고,

로그를 출력하고,

알림을 보내는 등의 작업을 동시에 실행할 수 있습니다.

store.subscribe((count) => {
  console.log(`Counter UI 업데이트: ${count}`);
});

store.subscribe((count) => {
  console.log(`Logger: Count 값 변경됨 -> ${count}`);
});

store.subscribe((count) => {
  alert(`Count가 변경되었습니다: ${count}`);
});

 

한 번의 상태 변경(Publish)으로 여러 개의 Subscriber가 동시에 반응!

increase() 함수가 실행되면 모든 구독자가 자동으로 실행됨

추가적인 변경 없이 여러 구독자가 상태 변경을 감지할 수 있음

 

2. Pub-Sub 패턴의 동작 방식

Pub-Sub 패턴을 쉽게 이해하기 위해, 일반적인 이벤트 시스템을 생각해 보면 좋습니다.

예를 들어, 뉴스레터 서비스를 떠올려 보겠습니다.

 

Publisher(발행자) → 뉴스레터 발송 시스템

Subscriber(구독자) → 뉴스레터를 구독한 사용자

 

1. 사용자가 뉴스레터를 구독하면, 구독자 리스트(Subscriber List)에 추가됩니다.

2. 새로운 뉴스레터가 발행(Publish)되면, 구독자 리스트에 있는 모든 사람에게 메일이 발송됩니다.

3. 만약 구독을 취소하면, 이후 뉴스레터를 받지 않습니다.

 

이처럼 특정 이벤트(뉴스레터 발행)가 발생하면, 이를 구독하고 있는 모든 대상(Subscriber)에게 자동으로 전달되는 구조가 바로 Pub-Sub 패턴입니다.

 

이제 이 개념을 JavaScript 코드로 표현해 보겠습니다.

 

 

3.1 Object.defineProperty를 이용해서 Zustand 직접 만들어보기

Object.defineProperty는 객체의 속성을 더 정밀하게 제어할 수 있는 메서드입니다.

이걸 활용하면 특정 속성에 Getter(읽기)Setter(쓰기) 를 설정해서, 상태가 변경될 때 자동으로 특정 동작(예: UI 업데이트)을 수행할 수 있습니다.

보통 객체의 속성을 정의할 때는 이렇게 하죠:

const obj = {
  count: 0,
};
console.log(obj.count); // 0
obj.count = 5; // 값 변경 가능

그런데 Object.defineProperty를 사용하면, 속성의 읽기 전용(변경 불가능) 속성을 만들거나, 속성이 변경될 때 특정 동작을 수행하도록 설정할 수 있어요.

const obj = {};
Object.defineProperty(obj, "count", {
  value: 0, // 초기 값
  writable: false, // 값 변경 불가능 (읽기 전용)
});
console.log(obj.count); // 0
obj.count = 10; // Error! (writable: false 이므로 변경 불가)

우리는 이걸 활용해서 Zustand처럼 상태가 변경될 때 자동으로 구독자들에게 알리는 시스템을 만들어볼 거예요.

 

Zustand 스타일 상태 관리 시스템 만들기

이제 Object.defineProperty를 활용하여 상태를 관리하는 시스템을 직접 구현해봅시다.

아래 기능을 하나씩 만들어볼게요.

 

1️⃣ 상태 저장소와 구독자 리스트 만들기

우리는 state라는 객체를 만들고, 상태가 변경될 때 이를 감지하여 UI를 업데이트할 구독자(콜백 함수)를 저장할 listeners 리스트를 생성할 거예요.

let state = { count: 0 }; // 기본 상태
const listeners = new Set(); // 구독자(콜백 함수) 저장

• state는 현재 애플리케이션의 상태를 저장하는 객체입니다.

• listeners는 상태 변경을 감지하고 실행될 함수(구독자)를 저장하는 Set입니다. (Set을 쓰면 중복 등록이 방지됨)

 

2️⃣ getState() 함수 구현

현재 상태를 반환하는 함수입니다.

function getState() {
  return state;
}

 

console.log(getState()); // { count: 0 }

이제 getState()를 호출하면 현재 상태를 읽을 수 있어요.

 

3️⃣ setState() 함수 구현 (상태 변경 + 구독자 알림)

이제 상태를 변경하는 setState() 함수를 만들어볼 거예요.

state 값을 변경하고, 등록된 구독자(콜백 함수)들에게 변경된 상태를 알려주는 역할을 합니다.

function setState(newState) {
  state = { ...state, ...newState }; // 기존 상태를 새로운 값과 병합
  listeners.forEach((listener) => listener(state)); // 구독자들에게 변경 알림
}

📌 어떻게 동작할까요?

  1. 새로운 상태(newState)를 기존 상태(state)와 병합합니다.
  2. listeners에 등록된 모든 함수(구독자)를 실행하여 변경된 상태를 전달합니다.
setState({ count: 5 });
// 상태 변경: { count: 5 } (구독된 모든 함수가 실행됨)

 

4️⃣ subscribe() 함수 구현 (구독 기능 추가)

이제 상태 변경을 감지하고 특정 동작을 수행할 수 있도록 구독 기능을 추가할 차례예요.

function subscribe(listener) {
  listeners.add(listener); // 구독자 추가

  // 구독 해제 기능 반환 (언제든지 구독을 해제할 수 있음)
  return () => listeners.delete(listener);
}

📌 어떻게 동작할까요?

  1. subscribe(listener)를 호출하면 상태가 변경될 때 실행할 콜백 함수를 listeners에 추가합니다.
  2. unsubscribe()를 호출하면 특정 구독을 해제할 수 있습니다.
const unsubscribe = subscribe((newState) => {
  console.log("상태 변경됨:", newState);
});

setState({ count: 10 });
// 콘솔 출력: "상태 변경됨: { count: 10 }"

// 구독 해제
unsubscribe();

setState({ count: 20 });
// 이제 콘솔에 아무것도 출력되지 않음 (구독 해제됨)

 

5️⃣ 상태를 감지하도록 Object.defineProperty 적용하기

이제 Object.defineProperty를 활용해서 상태 변경을 감지하는 시스템을 만들어볼 거예요.

상태 객체 state 내부의 count 속성이 변경될 때마다, 자동으로 listeners를 실행하도록 설정하겠습니다.

const store = (() => {
  let state = { count: 0 }; // 초기 상태
  const listeners = new Set();

  const observableState = {};

  Object.keys(state).forEach((key) => {
    Object.defineProperty(observableState, key, {
      get() {
        return state[key]; // 현재 값 반환
      },
      set(value) {
        state[key] = value; // 새로운 값 설정
        listeners.forEach((listener) => listener({ ...state })); // 구독자 실행
      },
    });
  });

  function getState() {
    return observableState;
  }

  function setState(newState) {
    Object.keys(newState).forEach((key) => {
      if (observableState[key] !== undefined) {
        observableState[key] = newState[key]; // Setter 실행됨 (자동으로 구독자 실행)
      }
    });
  }

  function subscribe(listener) {
    listeners.add(listener);
    return () => listeners.delete(listener);
  }

  return { getState, setState, subscribe };
})();

 

이제 실제로 Zustand처럼 동작하는지 확인해볼까요?

// 상태 변경 감지 (구독)
const unsubscribe = store.subscribe((newState) => {
  console.log("상태 변경:", newState);
});

// 초기 상태 확인
console.log("초기 상태:", store.getState()); // { count: 0 }

// 상태 변경 테스트
store.setState({ count: 1 }); // "상태 변경: { count: 1 }"
store.setState({ count: 5 }); // "상태 변경: { count: 5 }"

// 구독 해제 후 상태 변경 (출력 안 됨)
unsubscribe();
store.setState({ count: 10 });

 

✅ Object.defineProperty를 활용하여 상태가 변경될 때마다 자동으로 구독자들에게 알리는 시스템을 만들었어요!

 

 

이 방법 외에도 Proxy를 활용해서 구현하는 방법도 한번 살펴보겠습니다.

Object.defineProperty vs Proxy 비교

속성 감지 미리 정의된 속성만 감지 가능 동적으로 속성을 추가/삭제해도 감지 가능
성능 속성이 많아질수록 성능 저하 성능이 더 좋음
코드 복잡성 비교적 복잡함 코드가 더 간결함
호환성 IE9 이상 지원 IE 미지원 (최신 브라우저만 가능)

📌 Object.defineProperty는 속성을 미리 정의해야 하지만, IE9 이상에서 동작하므로 브라우저 지원이 넓습니다.

📌 반면 Proxy는 모든 속성 추가/삭제를 감지할 수 있어 더욱 직관적이지만, IE를 지원하지 않습니다.

 

• Object.defineProperty를 활용하면 상태 변경 감지 시스템을 직접 구현할 수 있다.

• 하지만 모든 속성을 미리 정의해야 한다는 제한점이 있다.

• Proxy를 사용하면 보다 간결하고 강력한 상태 감지 시스템을 만들 수 있다.

 

 

3.2 Proxy를 이용해서 Zustand 직접 만들어보기

Proxy는 Object.defineProperty보다 더 강력하고 직관적인 방법으로 객체의 변화를 감지할 수 있는 기능을 제공합니다.

특히, Proxy는 새로운 속성이 동적으로 추가되거나 삭제되는 것도 감지할 수 있기 때문에 전역 상태 관리 시스템을 만들 때 매우 유용합니다.

 

💡 Proxy란?

Proxy는 JavaScript의 내장 객체로, 객체의 동작을 가로채서(Customize) 새로운 기능을 추가할 수 있도록 해주는 기능입니다.

이걸 활용하면 객체의 값이 변경될 때 특정 작업을 자동으로 실행할 수 있어요.

 

✅ Proxy 기본 사용법

const person = {
  name: "Alice",
  age: 25,
};

const personProxy = new Proxy(person, {
  get(target, prop) {
    console.log(`읽기: ${prop}`);
    return target[prop];
  },
  set(target, prop, value) {
    console.log(`변경: ${prop} = ${value}`);
    target[prop] = value;
    return true;
  },
});

console.log(personProxy.name); // "읽기: name", "Alice"
personProxy.age = 30;          // "변경: age = 30"
console.log(personProxy.age);  // "읽기: age", "30"

Proxy를 사용하면 객체의 속성을 읽거나 변경할 때마다 자동으로 특정 동작을 수행할 수 있어요.

이제 Proxy를 활용해서 Zustand처럼 동작하는 상태 관리 시스템을 만들어볼게요!

 

🛠 Proxy를 활용한 Zustand 스타일 상태 관리 구현하기

우리는 아래 기능을 갖춘 상태 관리 시스템을 만들 것입니다.

전역 상태(state) : Proxy 객체로 감싸서 자동으로 상태 변경을 감지

getState() : 현재 상태를 반환하는 함수

setState() : 상태를 변경하고, 모든 구독자에게 알림을 보내는 함수

subscribe() : 상태 변경을 감지할 함수를 등록하는 기능

 

1️⃣ Proxy 기반 상태 저장소 만들기

먼저, listeners(구독자 목록)를 만들고, Proxy를 활용하여 상태를 감지하는 저장소(store)를 만들어봅시다.

const listeners = new Set(); // 구독자(콜백 함수) 저장소

const state = {
  count: 0, // 초기 상태
};

const store = new Proxy(state, {
  get(target, prop) {
    console.log(`🔍 get: ${prop} -> ${target[prop]}`); // 읽기 로깅
    return target[prop];
  },
  set(target, prop, value) {
    console.log(`✏️ set: ${prop} = ${value}`); // 변경 로깅

    target[prop] = value; // 값 변경

    // 상태가 변경되었으므로 모든 구독자 호출
    listeners.forEach((listener) => listener({ ...target }));

    return true; // set 함수는 반드시 true를 반환해야 함
  },
});

// 구독 기능 추가
function subscribe(listener) {
  listeners.add(listener);
  return () => listeners.delete(listener);
}

 

2️⃣ Proxy 방식 상태 변경 테스트

이제 Proxy를 활용한 상태 관리가 정상적으로 동작하는지 확인해볼게요.

// 상태 변경 감지 (구독)
const unsubscribe = subscribe((newState) => {
  console.log("🚀 Proxy 상태 변경:", newState);
});

// 초기 상태 확인
console.log(store.count); // "🔍 get: count -> 0"

// 상태 변경 테스트
store.count = 1; // "✏️ set: count = 1", "🚀 Proxy 상태 변경: { count: 1 }"
store.count = 5; // "✏️ set: count = 5", "🚀 Proxy 상태 변경: { count: 5 }"

// 구독 해제 후 상태 변경 (출력 안 됨)
unsubscribe();
store.count = 10; // "✏️ set: count = 10" (구독 해제됨)

이제 store.count 값을 변경하면 자동으로 구독된 함수가 실행되는 걸 확인할 수 있어요!

 

 

3️⃣ Proxy 방식으로 Zustand 스타일 상태 관리 시스템 완성하기

Zustand처럼 getState(), setState(), subscribe()를 갖춘 완벽한 상태 관리 시스템을 만들어보겠습니다.

const store = (() => {
  let state = { count: 0 }; // 초기 상태
  const listeners = new Set(); // 구독자 리스트

  // Proxy를 활용하여 상태 감지
  const proxyState = new Proxy(state, {
    get(target, prop) {
      return target[prop];
    },
    set(target, prop, value) {
      target[prop] = value;
      listeners.forEach((listener) => listener({ ...target })); // 구독자 실행
      return true;
    },
  });

  function getState() {
    return proxyState;
  }

  function setState(newState) {
    Object.keys(newState).forEach((key) => {
      proxyState[key] = newState[key]; // Proxy의 set 트리거됨
    });
  }

  function subscribe(listener) {
    listeners.add(listener);
    return () => listeners.delete(listener);
  }

  return { getState, setState, subscribe };
})();

이제 완전히 Zustand처럼 동작하는 상태 관리 시스템이 완성되었습니다! 🚀

 

🛠 Proxy 기반 Zustand 스타일 상태 관리 테스트

// 상태 변경 감지 (구독)
const unsubscribe = store.subscribe((newState) => {
  console.log("✅ Proxy 상태 변경:", newState);
});

// 현재 상태 조회
console.log(store.getState()); // { count: 0 }

// 상태 변경 테스트
store.setState({ count: 3 });  // "✅ Proxy 상태 변경: { count: 3 }"
store.setState({ count: 10 }); // "✅ Proxy 상태 변경: { count: 10 }"

// 구독 해제 후 상태 변경 (출력 안 됨)
unsubscribe();
store.setState({ count: 20 });

✅ Proxy 덕분에 상태 변경을 자동으로 감지하고, 모든 구독자에게 알릴 수 있습니다.

✅ Object.defineProperty보다 코드가 간결하고 속성 추가/삭제가 자유롭습니다.

 

 

4. Vanilla JavaScript로 구현한 Zustand, React에서 활용해보기

지금까지 Vanilla JavaScript로 Zustand 스타일의 전역 상태 관리 시스템을 구현했어요.

이제 이걸 React에서 직접 활용해볼 차례입니다!

React의 useState, useReducer 없이 전역 상태를 관리하는 방법을 자세히 다뤄볼게요.

 

1) React에서 사용할 Zustand 스타일 Store 만들기

우선, 우리가 만든 Zustand 스타일 상태 관리 시스템을 정리해서 store.js 파일로 만들어요.

이 파일을 React 컴포넌트에서 불러와서 전역 상태 관리에 활용할 겁니다.

 

📁 프로젝트 구조

/my-app
 ├── /src
 │   ├── /components
 │   │   ├── Counter.js
 │   │   ├── Button.js
 │   ├── store.js  👈 여기에서 Zustand 스타일 Store 정의
 │   ├── App.js
 │   ├── index.js
 ├── package.json
 ├── public/index.html
 ├── ...

 

1.1) store.js 만들기 (Proxy 방식 Zustand 구현)

먼저 store.js에서 Zustand처럼 상태를 관리하는 코드를 작성해볼게요.

 

📄 src/store.js

const store = (() => {
  let state = { count: 0 }; // ✅ 전역 상태
  const listeners = new Set(); // ✅ 상태 변경을 구독하는 함수 목록

  // Proxy를 사용하여 상태 변경을 감지
  const proxyState = new Proxy(state, {
    get(target, prop) {
      return target[prop]; // 상태 반환
    },
    set(target, prop, value) {
      target[prop] = value; // 새로운 값 저장
      listeners.forEach((listener) => listener({ ...target })); // 구독자 실행
      return true;
    },
  });

  function getState() {
    return proxyState; // 현재 상태 반환
  }

  function setState(newState) {
    Object.keys(newState).forEach((key) => {
      proxyState[key] = newState[key]; // 상태 업데이트 (Proxy가 감지)
    });
  }

  function subscribe(listener) {
    listeners.add(listener);
    return () => listeners.delete(listener);
  }

  return { getState, setState, subscribe };
})();

export default store;

이 코드에서 하는 일

• state 객체를 Proxy로 감싸 속성이 변경될 때 자동으로 감지

• setState()를 사용하면 상태가 변경되며 자동으로 구독자(listener)에게 알림

• subscribe()를 통해 React 컴포넌트에서 상태 변경을 감지하고 UI 업데이트

이제 store.js를 React에서 활용해볼게요!

 

2) React에서 Zustand 스타일 상태 관리 사용하기

이제 React에서 이 store.js를 사용해서 전역 상태를 관리할 수 있도록 코드를 작성해볼게요.

2.1) Counter.js 컴포넌트 만들기

Counter 컴포넌트는 store.getState()로 상태를 읽고, store.subscribe()를 이용해 상태가 변경될 때 UI를 업데이트합니다.

 

📄 src/components/Counter.js

import React, { useState, useEffect } from "react";
import store from "../store"; // ✅ Zustand 스타일 Store 가져오기

function Counter() {
  const [count, setCount] = useState(store.getState().count); // ✅ 초기 상태 가져오기

  useEffect(() => {
    // ✅ 상태 변경 감지 (구독)
    const unsubscribe = store.subscribe((newState) => {
      setCount(newState.count); // 상태 변경 시 UI 업데이트
    });

    return () => unsubscribe(); // ✅ 컴포넌트 언마운트 시 구독 해제
  }, []);

  return (
    <h1>Count: {count}</h1>
  );
}

export default Counter;

이 코드에서 하는 일

  1. store.getState()로 현재 상태를 가져와 count 값 설정
  2. store.subscribe()를 사용해 상태 변경을 감지하고 UI 업데이트
  3. useEffect()를 이용해 컴포넌트가 언마운트될 때 구독 해제

 

2.2) Button.js 컴포넌트 만들기

버튼을 누를 때 store.setState()를 사용해서 전역 상태를 업데이트할 거예요.

 

📄 src/components/Button.js

import React from "react";
import store from "../store"; // ✅ Zustand 스타일 Store 가져오기

function Button() {
  return (
    <button onClick={() => store.setState({ count: store.getState().count + 1 })}>
      +
    </button>
  );
}

export default Button;

이 코드에서 하는 일

  1. onClick 이벤트에서 store.setState()를 사용해 count 값을 1 증가
  2. 상태가 변경되면 Counter.js의 useEffect()가 실행되어 UI가 자동 업데이트

 

2.3) App.js에서 상태 관리 테스트하기

이제 Counter.js와 Button.js를 App.js에서 사용해봅시다.

 

📄 src/App.js

import React from "react";
import Counter from "./components/Counter";
import Button from "./components/Button";

function App() {
  return (
    <div>
      <Counter />
      <Button />
    </div>
  );
}

export default App;

 

 

3. 여러 개의 상태 추가해보기

이제 count뿐만 아니라, 새로운 상태(text 값)를 추가해볼게요.

 

📄 src/store.js (여러 개의 상태 추가)

const store = (() => {
  let state = { count: 0, text: "Hello" }; // ✅ 여러 개의 상태
  const listeners = new Set();

  const proxyState = new Proxy(state, {
    get(target, prop) {
      return target[prop];
    },
    set(target, prop, value) {
      target[prop] = value;
      listeners.forEach((listener) => listener({ ...target }));
      return true;
    },
  });

  function getState() {
    return proxyState;
  }

  function setState(newState) {
    Object.keys(newState).forEach((key) => {
      proxyState[key] = newState[key];
    });
  }

  function subscribe(listener) {
    listeners.add(listener);
    return () => listeners.delete(listener);
  }

  return { getState, setState, subscribe };
})();

export default store;

 

📄 src/components/Text.js (새로운 상태 사용)

import React, { useState, useEffect } from "react";
import store from "../store";

function Text() {
  const [text, setText] = useState(store.getState().text);

  useEffect(() => {
    const unsubscribe = store.subscribe((newState) => {
      setText(newState.text);
    });

    return () => unsubscribe();
  }, []);

  return (
    <h2>{text}</h2>
  );
}

export default Text;

 

📄 src/components/TextButton.js (새로운 상태 변경)

import React from "react";
import store from "../store";

function TextButton() {
  return (
    <button onClick={() => store.setState({ text: "Hello, Zustand!" })}>
      Change Text
    </button>
  );
}

export default TextButton;