useState 만들어보기

김기한·2022년 6월 1일
0

javascript

목록 보기
3/3

처음 react hook을 사용했을 때부터, useState, useEffect와 같은 hook들이 내부적으로 어떻게 동작하는 지 매우 궁금했다. 사용하는 입장에서는 그저 마법처럼 느껴진다는 말이 맞는 것 같다.

사실, 이번 말고도 이전에 hook을 바닐라 js로 만드려다 실패한 적이 있다. 그래서 이번에는 가볍게 hook을 이해하고 동작하도록 만들어보려고 한다.

맨바닥부터 혼자 설계하여 이끌어내면 가장 좋겠지만, useState 만들기의 목적은 react hook의 내부 원리 및 동작을 간략하게나마 이해하는 것이 목적이기 때문에, 여러 reference를 참고하면서 진행한다.

useState의 사용

  • hook은 Function Component 내부에서 상태 관리를 위해 사용된다.
  • 첫 번째 반환 값은 상태 값이고, 두 번째 반환 값은 해당 상태를 변경시키고 컴포넌트를 리렌더링하는 함수이다.
  • 파라미터는 컴포넌트가 마운트 되는 시점 (맨 처음 렌더링 되는 시점)에 초기 상태 값을 전달한다.
const App = () => {
  const [posts, setPosts] = useState([]);

  const submitPost = (post) => () => {
    setPosts([...posts, post]);
  };

  return `<div class="app">
    ${InputForm({ submitPost })}
    ${Todolist({ posts })}
  </div>`;
};

짚고갈 점

  • 컴포넌트 렌더링 시 App 함수가 호출된다.
  • App함수 호출에 따라서 내부에 존재하는 useState도 무조건 한 번 호출한다.

생각나는대로 구현해보기

  • 상태와 상태를 변경시킬 수 있는 setState 함수를 만들어 실제 useState 처럼 튜플로 반환한다.
  • useState 스코프 내부에 상태를 잘 감췄다고 생각했다.
const useState = (initState) => {
	let state = initState;
	const setState = (newState) => {
		state = newState;
		render();	
	}
	return [state, setState];
}

⇒ 하지만, useState는 컴포넌트가 리렌더링 될때마다 계속해서 호출된다.

⇒ 따라서, 이전 상태를 기억하지 못하게 되고 초기 상태인지 구분할 수 없게 된다.

⇒ 당연히 상태는 계속 initState만 리턴한다.

state를 useState 밖으로

  • 항상 initState로 초기화 되는 문제는 해결된다.
  • 하지만, react를 사용하면서 useState를 단 한 번만 사용하는 경우는 없다.
  • 아래는 useState로 상태 1개만을 관리할 수 있다.
let _state;
const useState = (initState) => {
	if(_state === undefined) {
		_state = initState;
	}
	const state = _state;
	const setState = (newState) => {
		_state = newState;
		render();
	}
	return [state, setState];
}

state를 배열로 ..?

  • 여러 reference를 참조하면서 제일 이해가 가지 않았던 부분이다.
  • useState를 한 곳에서 가지런히 실행하는 것도 아닌데 왜 ! 배열을 사용하지? 하고 몹시 고민했다.
  • state가 저장된 공간을 기억하기 위해서라면 객체를 사용하여 key 값으로 참조하는 것이 편할텐데 왜 굳이 배열을 사용하는 걸까
  • 여러 번의 고민 중 내가 내린 답은 아래와 같다.
  • 첫 렌더링이든 리렌더링이든 항상 최상위 root 부터 렌더링을 시작한다고 하면, 전체에서 useState가 호출되는 컴포넌트의 순서는 동일하다.
  • 따라서, 따로 key 값을 설정해야하는 object를 사용할바엔, 호출되는 순서를 key로써 사용하여 이전 상태를 기억하는 것이다.
let cursor = 0;
const states = [];

const useState = (initState) => {
  let localCursor = cursor;
  if (cursor === states.length) {
    states.push(initState);
  }

  const state = states[cursor];
  const setState = (newState) => {
    states[localCursor] = newState;
    render();
  };

  cursor += 1;

  return [state, setState];
};

const render = () => {
  const $root = document.getElementById("root");
  cursor = 0;
  $root.innerHTML = App();
};

모듈로 분리

  • render 함수가 컴포넌트와 root element에 의존하고 있다.
  • 최상위 컴포넌트와 target element를 파라미터로 전달 받도록 설계하자.
  • render 함수는 이제 호출 시 두 가지 파라미터가 필요하기 때문에, _render 함수를 만들어 실제 리렌더링 시에는 _render 함수가 호출되도록 한다.
let cursor = 0;
let _render;
const states = [];

export const useState = (initState) => {
  let localCursor = cursor;

  if (cursor === states.length) {
    states.push(initState);
  }

  const state = states[cursor];
  const setState = (newState) => {
    states[localCursor] = newState;
    _render();
  };

  cursor += 1;

  return [state, setState];
};

const render = (GiactElement, $target) => {
  _render = () => {
    cursor = 0;
    $target.innerHTML = GiactElement();
  };
  _render();
};

const Giact = { render };

export default Giact;

사용

  • index.js에서 아래와 같이 이용가능하다.
import Giact from "../core/Giact.js";
import App from "./App.js";

const $root = document.getElementById("root");

Giact.render(App, $root);

다음은 jsx를 이용하여 virtual DOM 까지 도입해보고자 한다.

reference

Vanilla Javascript로 React UseState Hook 만들기
React hooks: not magic, just arrays

profile
vgihan's velog

0개의 댓글