토막지식시리즈/React 토막지식
전역 상태 라이브러리를 왜 사용할까?
GrapeMilk
2025. 2. 4. 23:54
간단한 전역 객체를 만들다가 들었던 생각을 공유해 봅니다.
javascript에서 모듈안에 생성된 객체/배열은 다른 모듈에서 import할 때 새 인스턴스가 생기지 않는다.
예를들어 module.js라는 공유되는 모듈이 있고
module.js
export const arr = [];
A.js 모듈에서 arr를 import한 뒤 숫자 1을 push 하는 코드가 있다.
import { arr } from "./module";
arr.push(1);
B.js 모듈에서 arr를 콘솔에 찍어보면 push된 값이 반영된 걸 확인할 수 있다.
// B.js
import { arr } from "./module";
console.log("B.js에서 arr:", arr); // [1] (A.js에서 변경한 값이 반영됨)
참조의 공유
모듈에서 선언한 객체/배열을 export하면 참조를 공유하기 때문에 import 한 모듈사이에서 값을 공유할 수 있게 된다.
(단, let을 통해 값을 "재할당"하면 안됨)
이렇게 모듈 시스템을 통해 간단하게 전역에서 공유하는 객체를 생성할 수 있는데 왜 우리는 전역 상태 라이브러리를 사용할까?
React 상태 관리와 모듈 시스템
여러가지 이유가 있지만 일단 모듈 시스템만으로는 React 상태 관리를 할 수 없다.
모듈로 import한 값이 변경된다고 React 라이프 사이클을 타지 않는다 따라서 상태 변경을 감지하려면 useState나 useEffect와 같이 사용해야한다. 전역 상태 라이브러리는 이런 작업을 내부적으로 구현해서 사용하기 편리하게 제공한다.
Zustand의 경우 내부적으로 구독(subscribe) 시스템을 사용하여 상태 변경을 감지하고 React 컴포넌트를 자동으로 업데이트한다.
아래는 Zustand의 상태 관리 코드의 단순화 버전이라고 하는데(by GPT) 정확한 구현은 시간될 때 뜯어봐야 할 것 같다.
import { useSyncExternalStore } from "react";
// Zustand 내부에서 실행되는 상태 관리 로직
const createStore = (createState) => {
let state = createState((newState) => {
state = { ...state, ...newState };
listeners.forEach((listener) => listener(state));
});
let listeners = new Set();
return {
getState: () => state,
setState: (newState) => {
state = { ...state, ...newState };
listeners.forEach((listener) => listener(state));
},
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
};
// Zustand의 상태 변경 감지 로직
const useStore = (selector) => {
const store = createStore((set) => ({ count: 0, increase: () => set({ count: state.count + 1 }) }));
return useSyncExternalStore(
store.subscribe,
() => selector(store.getState()),
() => selector(store.getState())
);
};