그럴듯한 개발 블로그
article thumbnail
반응형

완성본 코드

https://github.com/donghyun1998/vanilla_js_SPA_starter_kit/blob/main/src/utils/useState.js

 

바닐라 자바스크립트로 useState없이 온몸 비틀기로 사이트 만들다가 도저히 안되겠어서 useState 구현 시작했다.

전에 배운 클로저를 이용해서 useState가 호출되는 시점에 인스턴스를 만들고 getState, setState를 리턴하게 해서 구현했다. 근데 다 정상적으로 되지만 render함수가 갱신되지를 않았다.

function useState(oldState, render) {
  let state = [oldState];
  const getState = () => {
    return state[0];
  };
  const setState = (newState) => {
    // if (state[0] === newState) return;
    alert(state[0]); // 잘만 됨
    state[0] = newState;
    render();
  };
  return [getState, setState];
}
/**
 * @param {HTMLElement} $container
 */

export default function GameMode($container) {
  this.$container = $container;
  let [getGameMode, setGameMode] = useState(0, this.render);
  this.setState = () => {
    this.render();
  };

  this.render = () => {
    importCss("../../../assets/css/game-mode.css");
    this.$container.innerHTML = `
        <div class="game-mode-container">
          ${gameModeUnit(getGameMode())}
          ${gameModeUnit(getGameMode())}
          ${gameModeUnit(getGameMode())}
          <button id="button2" style="position: absolute; bottom: 10px; right: 10px;">state</button>
        </div>
	  `;
  };

  this.render();
  $container.addEventListener("click", (e) => {
    if (e.target.id === "button2") {
      setGameMode(getGameMode() + 1);
    }
  });
}

왜 그런가 하니 저 함수 인자로 받은 render()는 저 클로저 인스턴스 생성 당시의 render를 기억하고 있어서 state가 바뀌어봤자 이전 state가 적용된 render함수가 호출되서 바뀌질 않는것이다 ㅋㅋ..

그래서 걍 this를 전달해버렸다. 그리고 생각해보니 파라미터 자체도 받아서 사용하면 클로저 스코프 안에 들어가므로 굳이 다시 선언 해 줄 필요 없겠다.

/**
 * @description useState 훅 구현
 * @param state 상태
 * @param component 전달된 컴포넌트
 * @returns [getState, setState] 반환
 * @example 앞으로 모든 컴포넌트 렌더링 함수 다 render로 통일 해야 합니다. 안그러면 최신화가 안됩니다.
 */
export default function useState(state, component) {
  const getState = () => {
    return state;
  };
  const setState = (newState) => {
    state = newState;
    component.render(); // 그냥 render를 인자로 받으면 최신화가 안되버리는..!
  };
  return [getState, setState];
}

완성본 ㅋ.. 좀 구리긴 하다.

일단 이대로 써서 개발하다가 더 좋은 방법 있으면 변경 예정

 

+ 바로 바꿨다. render 함수만 호환된다는 점이 리팩토링하다 거슬려서 추가 인자로 render메서드 명을 받도록 했다.

/**
 * @description useState 훅 구현
 * @param state 상태
 * @param component 전달된 컴포넌트
 * @param {string}render 렌더링 함수 명
 * @returns [getState, setState] 반환
 */
export default function useState(state, component, render) {
  const getState = () => {
    return state;
  };
  const setState = (newState) => {
    state = newState;
    component[render](); // 그냥 render를 인자로 받으면 최신화가 안되버리는..!
  };
  return [getState, setState];
}

드럽지만 어쩔수 없다 ㅠㅠㅠㅠㅠ

 

++ 또 바뀌었다 ㅋㅋ

state 5개가 한번에 바뀌어야 할 상황이 생겼는데, 각각 state로 만들면 5번 렌더링이 된다.

react에서는 이를 피하기 위해 가상돔에다 반영하고 렌더링을 한꺼번에 하는데 이거 하다간 뇌 녹을거 같다..

그래서 그냥 오버로딩 해 줬다.

js는 오버로딩을 지원 안한다 ㅋㅋ 실환가

그래서 parameter typeof로 분기 줘서 유사오버로딩 해 줬다.

/**
 * @description useState 훅 구현
 * @param {array | number | string}stateInput 상태
 * @param {object}component 전달된 컴포넌트
 * @param {string}render 렌더링 함수 명
 * @returns [getState, setState] 반환
 * @description 배열이 인자로 올 때 동작이 다르니 유의해서 사용하세요
 */
export default function useState(stateInput, component, render) {
  if (typeof stateInput === "object") {
    let state = [...stateInput];
    const getState = () => {
      return state;
    };
    const setState = (newState) => {
      state = [...newState];
      component[render](); // 그냥 render를 인자로 받으면 최신화가 안되버리는..!
    };
    return [getState, setState];
  } else if (typeof stateInput === "number" || typeof stateInput === "string") {
      let state = stateInput;
      const getState = () => {
        return state;
      };
      const setState = (newState) => {
        state = newState;
        component[render](); // 그냥 render를 인자로 받으면 최신화가 안되버리는..!
      };
      return [getState, setState];
  }
}

극혐

 

+++ 또 바뀜

실제 useState와 달리 object도 받을 수 있다고 좋아했는데 

const setState = (newState) => {
      alert("setstate");
      state = [...newState];
      component[render](); // 그냥 render를 인자로 받으면 최신화가 안되버리는..!
    };

이 부분에서 배열로 싸버려서 안되더라 ㅋㅋ....

 

분기로 object인지 array인지 검사해서 타입에 맞게 구조분해할당 하는 코드 추가했다.

/**
 * @description useState 훅 구현
 * @param {array | number | string | boolean | Object}stateInput 상태
 * @param {object}component 전달된 컴포넌트
 * @param {string}render 렌더링 함수 명
 * @returns [getState, setState] 반환
 * @description 배열이 인자로 올 때 동작이 다르니 유의해서 사용하세요
 */

export default function useState(stateInput, component, render) {
  if (Array.isArray(stateInput)) {
    // 배열일 경우
    let state = [...stateInput];
    const getState = () => {
      return state;
    };
    const setState = (newState) => {
      state = [...newState];
      component[render](); // render 메서드 호출
    };
    return [getState, setState];
  } else if (typeof stateInput === "object") {
    // 객체일 경우 (단, null과 배열 제외)
    let state = { ...stateInput };
    const getState = () => {
      return state;
    };
    const setState = (newState) => {
      state = { ...newState };
      component[render](); // render 메서드 호출
    };
    return [getState, setState];
  } else {
    // 기본형(primitive) 데이터 타입일 경우
    let state = stateInput;
    const getState = () => {
      return state;
    };
    const setState = (newState) => {
      state = newState;
      component[render](); // render 메서드 호출
    };
    return [getState, setState];
  }
}

 

++++ 또 또 바뀜

setState에서 state가 달라질 경우에만 렌더링 되게 바꿨다.

근데 array, object의 경우 멤버가 같아도 참조가 다르기 때문에 비교 함수를 따로 만들어야 했다.

대충 두가지 방법이 떠올랐다.

1. json으로 바꿔서 보기

2. 재귀하향식으로 비교함수 만들기

 

1번의 장점: 코드 매우짧음 JSON.stringify 해버리면 됨.

1번의 단점: boolean같이 의도하지 않게 string화 되거나 {a:1, b:2} {b:2, a:1} 이런 거 구분 못함(그럴 일 없겠지만 혹시 이 경우에 버그잡기 매우 힘들 듯)

2번의 장점: 정밀한 비교 가능 

이정도면 state로 넣을만한 데이터는 괜찮겠지..

2번의 단점: js에서 콜스택 터진다고 거품무는 재귀 사용, 코드 생각보다 엄청 길어짐

 

안전하게 2번으로 가자..

function deepEqual(a, b) {
  // 두 인자의 타입이 다르면 바로 false 반환
  if (typeof a !== typeof b) return false;
  // 기본 타입이거나 함수인 경우, 단순 비교
  if (a === b || typeof a === "function") return true;
  // 둘 중 하나라도 null이면, 둘 다 null인지 확인 (위에서 === 비교를 했으므로 여기는 둘 다 null일 수 없음)
  if (a === null || b === null) return false;
  // 배열인 경우, 길이와 각 요소 비교
  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) return false;
    for (let i = 0; i < a.length; i++) {
      if (!deepEqual(a[i], b[i])) return false;
    }
    return true;
  }
  // 객체인 경우, 키의 개수와 각 키의 값을 비교
  if (isObject(a) && isObject(b)) {
    const keysA = Object.keys(a);
    const keysB = Object.keys(b);
    if (keysA.length !== keysB.length) return false;
    for (const key of keysA) {
      if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false;
    }
    return true;
  }
    // 타입이 매칭되지 않는 경우
  return false;
}

function isObject(obj) {
  return obj !== null && typeof obj === "object";
}

/**
 * @description useState 훅 구현
 * @param {array | number | string | boolean | Object}stateInput 상태
 * @param {object}component 전달된 컴포넌트
 * @param {string}render 렌더링 함수 명
 * @returns [getState, setState] 반환
 * @description 배열이 인자로 올 때 동작이 다르니 유의해서 사용하세요
 */

export default function useState(stateInput, component, render) {
  if (Array.isArray(stateInput)) {
    // 배열일 경우
    let state = [...stateInput];
    const getState = () => {
      return state;
    };
    const setState = (newState) => {
      if (deepEqual(state, newState)) return; // 이전 상태와 새로운 상태가 같으면 렌더링 하지 않음
      state = [...newState];
      component[render](); // render 메서드 호출
    };
    return [getState, setState];
  } else if (typeof stateInput === "object") {
    // 객체일 경우 (단, null과 배열 제외)
    let state = { ...stateInput };
    const getState = () => {
      return state;
    };
    const setState = (newState) => {
      if (deepEqual(state, newState)) return; // 이전 상태와 새로운 상태가 같으면 렌더링 하지 않음
      state = { ...newState };
      component[render](); // render 메서드 호출
    };
    return [getState, setState];
  } else {
    // 기본형(primitive) 데이터 타입일 경우
    let state = stateInput;
    const getState = () => {
      return state;
    };
    const setState = (newState) => {
      console.log(state, newState);
      if (deepEqual(state, newState)) return; // 이전 상태와 새로운 상태가 같으면 렌더링 하지 않음
      state = newState;
      component[render](); // render 메서드 호출
    };
    return [getState, setState];
  }
}

 

반응형

'<frontend> > vanilla-js-SPA-starter-kit' 카테고리의 다른 글

useReducer 구현  (2) 2024.02.12
useEffect 구현  (0) 2024.02.08
바닐라 js SPA 스타터킷  (1) 2024.02.08
profile

그럴듯한 개발 블로그

@donghyk2

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!