devTaehyeok

devTaehyeok

간단한 전역 상태 관리 훅 만들기

간단한 전역 상태 관리 훅 만들어보기.

참고 문서

배경 지식

  • Local state management
    • 한 컴포넌트 내 상태 관리
  • Global state management
    • 여러 컴포넌트 간 상태 공유

해당 예제는 count의 증가를 화면에 반영하지 않는다.

  • 리렌더링을 발생시키는 요소가 없기 때문이다.
import React from 'react';

// 전역 변수
let count = 0;

function Counter(props) {
  let incrementCount = e => {
    ++count;
    console.log(count);
  };

  return (
    <div>
      Count: {count}
      <br />
      <button onClick={incrementCount}>Click</button>
    </div>
  );
}

ReactDOM.render(<Counter />, document.querySelector('#root'));

가장 간단한 전역 변수 사용 방법

import React from 'react';

// use global variable to store global state
let count = 0;

function Counter(props) {
  const [, setState] = useState();

  let incrementCount = e => {
    ++count;
    console.log(count);
    // 해당 함수를 호출하기만 하면 강제로 리렌더링을 유발한다.
    setState({});
  };
  return (
    <div>
      Count: {count}
      <br />
      <button onClick={incrementCount}>Click</button>
    </div>
  );
}

ReactDOM.render(<Counter />, document.querySelector('#root'));

문제 : 만약 이렇게 된다면?

  • 하나만 리렌더링되어 싱크가 안맞을 것이다.
import React from 'react';

// 전역 변수
let count = 0;

function Counter1(props) {
  const [, setState] = useState();

  let incrementCount = e => {
    ++count;
    setState({});
  };

  return (
    <div>
      Count: {count}
      <br />
      <button onClick={incrementCount}>Click</button>
    </div>
  );
}

function Counter2(props) {
  const [, setState] = useState();

  let incrementCount = e => {
    ++count;
    setState({});
  };

  return (
    <div>
      Count: {count}
      <br />
      <button onClick={incrementCount}>Click</button>
    </div>
  );
}

function Counters(props) {
  return (
    <>
      <Counter1 />
      <Counter2 />
    </>
  );
}

ReactDOM.render(<Counters />, document.querySelector('#root'));

해결책 : 전역 변수가 모든 리렌더링 대상 컴포넌트에게 알리면 된다.

  • 이것이 옵저버 패턴이다.
    • subscriber가 subject다. 개략 옵저버vs펍섭
// 해당 함수는 객체 생성자 함수임
function GlobalState(initialValue) {
  this.value = initialValue; // 전역 변수.
  this.subscribers = []; // 구독자 리스트 : 리렌더링할 컴포넌트들.

  this.getValue = function () {
    // 전역 변수 값을 얻는 메소드
    return this.value;
  };

  this.setValue = function (newState) {
    // 전역 상태를 업데이트하는 메소드
    if (this.getValue() === newState) {
      // No new update
      return;
    }

    this.value = newState; // 전역 상태 업데이트
    this.subscribers.forEach(subscriber => {
      // 구독자들에게 변화를 알림.
      subscriber(this.value);
    });
  };

  this.subscribe = function (itemToSubscribe) {
    if (this.subscribers.indexOf(itemToSubscribe) > -1) {
      // 이미 구독중인 컴포넌트면 아무 일도 하지 않음
      return;
    }
    // 해당 컴포넌트를 구독함.
    this.subscribers.push(itemToSubscribe);
  };

  this.unsubscribe = function (itemToUnsubscribe) {
    // This is a function for unsubscribing from a global state
    this.subscribers = this.subscribers.filter(
      subscriber => subscriber !== itemToUnsubscribe,
    );
  };
}

정리

  1. subscribe, unsubscribe로 pub/sub 메커니즘 활용
  2. setValue로 전역변수 변경. 전체 업데이트
  3. getValue로 전역변수 얻어옴.
  4. subscriber인 컴포넌트는 setValue 이벤트를 발행함. 그러면 observer GlobalState가 rerender 이벤트를 퍼블리시함.
import { useState, useEffect } from 'react';

function useGlobalState(globalState) {
  const [, setState] = useState();
  const state = globalState.getValue();

  function reRender(newState) {
    // 리렌더링을 강제로 유발함
    setState({});
  }

  useEffect(() => {
    // 변화 시 리렌더를 강제 유발하기 위함.
    globalState.subscribe(reRender);

    return () => {
      // 언마운트 시 구독 해제
      globalState.unsubscribe(reRender);
    };
  });

  function setState(newState) {
    // 전역 상태를 업데이트하면 퍼블리셔인 GlobalState가 업데이트를 publish함.
    globalState.setValue(newState);
  }
  return [State, setState];
}

Observer Vs PubSub

  • 추가로 해볼만한 것들
    • globalState를 클로저로 집어넣기.
    • reRender부분에 로직을 넣어 redux connect 구현. -...