[ Vanilla JS ] Observer 패턴으로 상태관리 하기 ( feat. state )

bepyan·2022년 2월 6일
7
post-thumbnail

Observer Pattern.

한 줄로 요약 하자면, 상태의 변경을 관찰하겠다.
상태가 변경되면 미리 정의한 일련의 동작을 실행시키는 원리이다.



전체 코드.

/core/State.js

// 여기서 state 변수는 json 객체이어야 한다.
export const State = (state) => {
  
  // 구독자는 SET 자료구조에 담는다.
  state._subscribers = new Set();
  // 구독하는 메소드이다. SET에 추후에 실행시킬 메소드를 추가한다.
  state.subscribe = (render) => state._subscribers.add(render);

  // state의 모든 key에 대해서 작업을 수행한다.
  Object.keys(state).forEach((key) => {
    let _value = state[key];

    // C++ 연산자 오버로딩과 비슷하다. `=`연산의 동작을 재정의한다.
    Object.defineProperty(state, key, {
      get() {
        return _value;
      },

      set(value) {
        // 상태가 실재로 변동되지않으면 로직을 수행하지 않는다.
        if (_value === value) return;
        if (JSON.stringify(_value) === JSON.stringify(value)) return;

        _value = value;
        // 구독한 메소드를 모두 실행시킨다.
        state._subscribers.forEach((fn) => fn());
      },
    });
  });
  
  return state;
};

/core/index.js

export * from "./State";


패턴의 흐름.

  1. 상태를 정의한다.

    export const homeState = State({ list: [] });
  2. 구독를 추가한다.

    _subscribers은 Set을 활용하여 구독자를 관리한다.

    상태변경시 실행시킬 작업을 등록한다.

    const render = () => {
       root.innerHTML = `
          ${homeState.list.map((name) => `<p>${name}</p>`).join("")}
       `;
     };
    
     homeState.subscribe(render);
  3. 상태를 변경한다.

    homeState.list = [...homeState.list, e.target.name.value];

    defineProperty를 사용하여 = 로 state에 새로운 값을 주입하면 set 메소드가 실행된다.



간단한 활용 예시.

/pages/home/_state.js

import { State } from "../../core";

export const homeState = State({ list: [] });

이렇게 전역 상태로 사용해도 되고, 컴포넌트 내부에서 선언해서 props로 넘겨줘도 좋을 것 같다.

/pages/home/HomeForm.js

import { homeState } from "./_state";

export const HomeForm = () => {
  const root = document.createElement("form");
  root.innerHTML = `
    <h1>Form</h1>
    <input id="name"/>
  `;

  root.addEventListener("submit", (e) => {
    e.preventDefault();

    homeState.list = [...homeState.list, e.target.name.value];
    e.target.name.value = "";
  });

  return root;
};

/pages/home/HomeList.js

import { homeState } from "./_state";

export const HomeList = () => {
  const root = document.createElement("div");

  const render = () => {
    root.innerHTML = `
        ${homeState.list.map((name) => `<p>${name}</p>`).join("")}
    `;
  };

  homeState.subscribe(render);
  render();

  return root;
};

/pages/home/index.js

export const HomePage = () => {
  const app = document.querySelector("#app");

  app.appendChild(HomeForm());
  app.appendChild(HomeList());
};

/pages/index.js

import "../styles/index.scss";
import { HomePage } from "./home";

(() => {
  HomePage();
})();


코어 코드.

export const State = (state) => {
  state._subscribers = new Set();
  state.subscribe = (render) => state._subscribers.add(render);

  Object.keys(state).forEach((key) => {
    let _value = state[key];

    Object.defineProperty(state, key, {
      get() {
        return _value;
      },

      set(value) {
        if (_value === value) return;
        if (JSON.stringify(_value) === JSON.stringify(value)) return;

        _value = value;
        state._subscribers.forEach((fn) => fn());
      },
    });
  });

  return state;
};


참고.

profile
쿠키 공장 이전 중 🚛 쿠키 나누는 것을 좋아해요.

0개의 댓글