간단한 useState 만들어보기(feat closure)

성준영·2022년 8월 28일
0

만들게 된 계기

원티드에서 주관한 프리온보딩 프론트엔드 챌린지에 참여를 했는데 멘토님께서 useState는 마법이 아니라며 closure와 연관지어 설명해 주셨다. 여기에 흥미를 느껴 closure를 공부해볼 겸 간단한 useState를 만들어보자고 생각하게 됐다.
(간단하다고 했지만, 생각보다 오래 걸렸다...)

closure함수의 특징

  • 외부 함수안에 지역 변수와 내부 함수가 있고, 외부 함수에서 내부 함수가 return될 때 내부 함수가 지역 변수를 참조 한다면 외부 함수가 return 되더라도 외부 함수의 지역 변수는 사라지지 않고 내부 함수에서 계속해서 참조할 수 있다.

MDN 예제

    function makeAdder(x) {
      var y = 1;
      return function(z) {
        y = 100;
        return x + y + z;
      };
    }

    var add5 = makeAdder(5);
    var add10 = makeAdder(10);
    //클로저에 x와 y의 환경이 저장됨

    console.log(add5(2));  // 107 (x:5 + y:100 + z:2)
    console.log(add10(2)); // 112 (x:10 + y:100 + z:2)
    //함수 실행 시 클로저에 저장된 x, y값에 접근하여 값을 계산

더 알아보기

링크

useState 만들기

npm create vite

위 명령어로 Vanilla Javascript템플릿을 만들고 리액트와 비슷한 폴더 구조를 구성했다.

트리 구조

📦custom-usestate-hook
 ┣ 📂src
 ┃ ┣ 📂components
 ┃ ┃ ┗ 📜counter.js
 ┃ ┣ 📂hooks
 ┃ ┃ ┗ 📜useState.js
 ┃ ┣ 📂router
 ┃ ┃ ┗ 📜render.js
 ┃ ┣ 📜main.js
 ┃ ┗ 📜style.css
 ┣ 📜.gitignore
 ┣ 📜favicon.svg
 ┣ 📜index.html
 ┣ 📜package.json
 ┣ 📜README.md
 ┗ 📜yarn.lock

우선 useState에 대해 생각해보면 state가 변경될 때 마다 리액트re-render를 한다. 그래서 우선 render함수를 만들었다.

render.js

import Counter from "../components/Counter";

export const render = () => {
  const app = document.querySelector("#app");
  app.innerHTML = `
  <div>
    <div>${Counter()}</div>
  </div>
`;
};

useState를 만들고 간단히 테스트 해보기 위해 Counter컴포넌트를 만들었다.

Counter.js

import useState from "../hooks/useState";

const Counter = () => {
  const [count, setCount] = useState(0);
  window.increment = () => setCount(count + 1);
  window.decrement = () => setCount(count - 1);
  window.reset = () => setCount(0);

  return `
    <div>
      <strong> count: ${count} </strong>
      <button> + </button>
      <button> 초기화 </button>
      <button> - </button>
    </div>
  `;
};
export default Counter;

useState의 경우 state변경 시 렌더링이 발생하기 때문에 아래와 같은 코드의 형태를 띄고 있을거라고 생각했다.

useState.js

before

import { render } from "../router/render";

const useState = (initialState) => {
  let state = initialState;

  const setState = (newState) => {
    state = newState;
    render(); // setState로 state변경시 렌더링 발생
  };
  return [state, setState];
};

export default useState;

그런데 위의 코드는 버튼을 아무리 클릭해도 값이 변화하지 않는다.(이것을 해결하는데 오래걸렸다..)

  • 이유
    setCount가 실행될 때마다 re-render가 발생하고 useState내부의 state는 결국 initialState로 초기화 된다.

이를 해결하기 위해 구글링을 했는데 useState의 경우 stateuseState내부가 아닌 외부에서 관리해야 한다고 한다.

after

import { render } from "../router/render";

let state = undefined;

const useState = (initialState) => {
  // state가 비어있을 경우에만 initialState로 초기화
  if (state === undefined) {
    state = initialState;
  }

  const setState = (newState) => {
    state = newState;
    render();
  };
  return [state, setState];
};

export default useState;

실행 화면

된다!

한계점

위의 코드는 잘 작동하는 것 처럼 보이지만 사실 큰 문제가 하나 남아있다. 컴포넌트를 여러개 만들고 useState를 여러번 호출 하게 되면 모든 컴포넌트는 모든 state를 공유하게 된다.

어찌보면 당연한 이야기다. stateuseState안에서 하나의 변수로 관리하기 때문이다.

이 현상을 해결하기 위해 stateArraykey값을 통해 관리한다고 한다. 이는 언젠가 해결하고 다시 포스팅하기로 했다.

링크

깃허브

테스트 링크

profile
기록해버리기

0개의 댓글