본문 바로가기
개인 학습

전역 상태 관리 파헤치기

by qjatjs123123 2025. 4. 6.

상태 관리는 왜 필요한가?

상태

  • 어떠한 의미를 지닌 값
  • 애플리케이션의 시나리오에 따라 지속적으로 변경될 수 있는 값

상태 종류

  • UI
  • URL
  • Form
  • 서버에서 가져온 값 

 

 

MVC 패턴

 

  • 양방향 데이터 바인딩의 모습
  • 애플리케이션이 비대해지고 상태도 많아짐에 따라 복잡해짐
  • 3번째 View에서 어떤 Model을 사용했는지 추적하기 어려움

 

Flux 패턴

  • 단방향 흐름으로 만든 것이 Flux 패턴
  • action → 액션 타입과 데이터를 정의해 디스패처로 보낸다.
  • dispatcher → 액션을 스토어에 보낸다. 콜백 함수 형태다. 애플리케이션 내에서 단 하나만 존재한다.
  • store → 실제 값과 상태를 변경할 수 있는 메서드를 가지고 있다.
  • view → 컴포넌트 부분, 액션을 호출해서 상태를 업데이트 한다.

 

 

useState와 useReducer

  • useState와 useReducer는 훅을 사용할 때마다 컴포넌트별로 초기화된다.
  • 지역 상태로서 해당 컴포넌트 내에서만 유효하다는 한계가 있다.
  • 여러 컴포넌트에서 상태를 공유하려면 props로 제공해야 한다.
  • props drilling 문제가 있다.

 

 

지역 상태의 한계를 벗어나보자. useState의 상태를 바깥으로 분리하기

  • useState는 리액트가 만든 클로저 내부에서 관리되어 지역 상태로 생성되기 때문에 해당 컴포넌트에서만 사용할 수 있다.
  • 만약 리액트 클로저가 아닌 자바스크립트 실행 문맥에서 관리된다면 어떨까?

 

export const createStore = (initialState) => {
  // 1. 초기 상태 설정 (함수형 초기값도 허용)
  let state = typeof initialState !== 'function' ? initialState : initialState();

  // 2. 구독 콜백 저장할 Set
  const callbacks = new Set();

  // 3. 현재 상태 반환
  const get = () => state;

  // 4. 상태 변경 + 구독자 호출
  const set = (newState) => {
    state = typeof newState === 'function' ? newState(state) : newState;
    callbacks.forEach((callback) => callback());
    return state;
  };

  // 5. 상태 변경 구독 (구독 해지 함수 반환)
  const subscribe = (callback) => {
    callbacks.add(callback);
    return () => {
      callbacks.delete(callback);
    };
  };

  // 6. store 객체 반환
  return {
    get,
    set,
    subscribe,
  };
};

 

 

  • 초기 상태 설정  →  useState와 마찬가지로 초기값을 게으른 초기화를 위한 함수를 받을 수 있도록 한다.
  • 구독 콜백 함수 저장할 변수  →  state 를 구독하는 컴포넌트의 렌더링 함수를 참조하기 위해 사용하는 변수이다.
  • get  →  클로저를 사용하여 은닉된 state를 접근할 수 있게 한다.
  • set  →  함수면 함수형 업데이트를 위해 현재 state를 파라미터로 넣어준다. state를 업데이트 한다. 그리고 자신에게 등록된 모든 callback을 실행하게 된다. 그 결과 구독한 컴포넌트가 리렌더링을 실행할 수 있게 한다.
  • subscribe  →  자기 자신을 렌더링하는 코드를 callbacks 변수에 저장한다.

 

 

전체 코드

import React, { useEffect, useState } from 'react';

export const createStore = (initialState) => {
  // 1. 초기 상태 설정 (함수형 초기값도 허용)
  let state = typeof initialState !== 'function' ? initialState : initialState();

  // 2. 구독 콜백 저장할 Set
  const callbacks = new Set();

  // 3. 현재 상태 반환
  const get = () => state;

  // 4. 상태 변경 + 구독자 호출
  const set = (newState) => {
    state = typeof newState === 'function' ? newState(state) : newState;
    callbacks.forEach((callback) => callback());
    return state;
  };

  // 5. 상태 변경 구독 (구독 해지 함수 반환)
  const subscribe = (callback) => {
    callbacks.add(callback);
    return () => {
      callbacks.delete(callback);
    };
  };

  // 6. store 객체 반환
  return {
    get,
    set,
    subscribe,
  };
};

const store = createStore({count : 0})

export const useStore = (store) => {
  const [state, setState] = useState(() => store.get())

  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      setState(store.get());
    })

    return unsubscribe;
  }, [store])

  return [state, store.set]
}



export default function App() {
  return (
    <div>
      <Counter1 />
      <Counter2 />
    </div>

  )
}

function Counter1() {
  const [state, setState] = useStore(store);

  function handleClick() {
    setState((prev) => ({ count: prev.count + 1 }))
  }

  return (
    <>
      <h3>Counter1: ${state.count}</h3>
      <button onClick={handleClick}>+</button>
    </>
  )
}

function Counter2() {
  const [state, setState] = useStore(store);

  function handleClick() {
    setState((prev) => ({ count: prev.count + 1 }))
  }

  return (
    <>
      <h3>Counter2: ${state.count}</h3>
      <button onClick={handleClick}>+</button>
    </>
  )
}

 

 

 

Recoil과 비교하기

const = createStore({ count: 0 }) // 구현

export const enableState = atom({
  key: "enableState",
  default: true
}) // recoil 코드

 

먼저 전역 상태 변수를 정의한다.

 

recoil에서는 atom으로 전역 상태를 저장하고 공유하는 변수를 정의한다.

 

createStore과 atom은 비슷하다고 볼 수 있다.

 

 

const store = createStore({count : 0})

export const useStore = (store) => {
  const [state, setState] = useState(() => store.get())

  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      setState(store.get());
    })

    return unsubscribe;
  }, [store])

  return [state, store.set]
} 

const [state, setState] = useStore(store); // 내 코드

-------------------------------------------------------------------

import { enableState } from "./recoil/enableState";
const [enable, setEnable] = useRecoilState(enableState); // Recoil 코드

 

useStore 훅과 useRecoilState훅은 비슷하다고 볼 수 있다.

 

useRecoilState 인자로 Atom 객체를 받는데 useStore도 마찬가지로 store 객체를 인자로 받는다.

 

그리고 store 객체를 변경시킬 store.set과, state를 반환한다.

 

즉 setState로 state를 변경하면 해당 컴포넌트는 setState함수로 인해 재렌더링 된다. 그리고 구독한 재렌더링 코드들이 실행된다.

 

 

 

 state가 객체일 때

만약 store의 구조가 원시값이 아니라 객체인 경우 객체 일부값만 변경해도 리랜더링이 발생한다.

 

객체의 원하는 값만 변했을 때 리렌더링 되도록 하자

 

export const useStoreSelector = (store, selector) => {
  const [state, setState] = useState(() => selector(store.get()))

  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      const value = selector(store.get());
      setState(store.get());
    })

    return unsubscribe;
  }, [store])

  return [state, store.set()]
}


const counter = useStoreSelector(store, useCallback(state => state.count, []))

 

먼저 게으른 초기화를 통해 state 변수를 초기화한다.

 

즉, store.get()을 호출해 클로저에 저장된 변수에 접근하고, 그 결과 객체의 count 값을 추출하여 state에 저장한다.

 

useState는 값이 변겨오디지 않으면 리렌더링을 수행하지 않으므로 store의 값이 변경왰다 하더라도 selector(store.get())이 변경되지 않으면 리렌더링이 일어나지 않는다.

 

여기서 핵심은 selector함수를 useCallback으로 감싸두어야 한다. 그렇지 않으면 컴포넌트가 리렌더링될 때마다 함수가 계속 재생성되어 store의 subscribe를 반복적으로 수행한다.

 

 

 

'개인 학습' 카테고리의 다른 글

자료구조 원리 파헤치기  (0) 2025.04.14
React의 Suspense 동작 원리  (0) 2025.04.09
서버 사이드 렌더링  (0) 2025.04.04
Debounce과 Throttle  (0) 2025.04.04
리액트 코드 분석하기  (0) 2025.04.02