본문 바로가기

React

useState 동작실험 및 구현

class 타입인  React.Component의 this.setState 와는 다르게,

동작원리를 초심자 입장에서는 직관적으로 유추하기가 힘들다. 

일단 아래의 코드를 살펴보자. (공식문서의 내용)

import React, { useState } from 'react';

function Example() {
  // 새로운 state 변수를 선언하고, count라 부르겠습니다.
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

공식문서를 살펴보면 useState(0)가 각각 count, setCount 에 변수와 함수를 할당한다고 하는데,

useState가 어떻게 자신을 호출한 컴포넌트(함수)마다 알맞는 변수를 밷어내는지에 대한 내용은..

React는 Hook 호출을 컴포넌트와 어떻게 연관시키는가?

React는 현재 렌더링 컴포넌트를 추적합니다. Rules of Hook 덕분에 Hook은 React 컴포넌트 (또는 React 컴포넌트에서만 호출되는 커스텀 Hook)에서만 호출된다는 것을 알고 있습니다.
각 컴포넌트와 관련된 “메모리 셀”의 내부 목록이 있습니다. 이것은 단지 데이터를 넣을 수 있는 JavaScript 객체입니다. useState()와 같은 Hook을 호출하면 현재 셀을 읽거나 첫 번째 렌더링 중에 초기화한 다음 포인터를 다음 셀로 이동합니다. 이것이 여러 useState() 호출이 각각 독립적인 로컬 state를 얻는 방법입니다.

메모리 셀을 만들어서 관리를 한다고 하는데, 어떻게 메모리셀을 만드는지는 저 내용만으로는 알 수 없다.

소스를 뜯어보면 알 수 있겠지만... 코드량이 상당하다.

 

일단은 문서를 다 살펴보기전에 추측만으로 알아보려한다. 

리엑트돔.렌더 로부터 호출이 시작되면, 두번째 파라미터인 dom 객체 기반으로 루트를 만들고 그 밑으로 호출된 함수 기반으로 노드를 만들어서 연관을 시키는게 간단한 방법이 될거 같다.

그게 사실이면 함수를 가리키는 변수와 호출할 함수가 일치 하지 않을 때 새롭게 state 값을 할당할 것이다.

실험1

document.getElementById('bt').addEventListener('click', () => {
  function Example() {
    const [count, setCount] = React.useState(0);
    return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
    );
  }
  ReactDOM.render(
    React.createElement(Example),
    document.getElementById('root')
  );
});

결과: 예상대로 버튼(bt)을 클릭 할 때마다 Example함수가 새로이 정의 되므로 state 값이 초기화 되는것을 알 수 있다.

그러면, 직접 정의한 함수에서 함수로 호출 하면 어떨까?

실험2

function Example() {
    const [say, setSay] = React.useState("hi");
    return React.createElement("h3",null,say)
}

function Example2() {
    const [say, setSay] = React.useState("안녕");
    return React.createElement("h1",null,say)
}

function Junction(){
    const [count, setCount] = React.useState(true);
    if(count){
        setTimeout(() => {
            setCount(false)
        }, 2000);
    }else{
        return Example2();
    }
    return Example();
}

el = React.createElement(Junction);
dom = document.getElementById('root');
ReactDOM.render(el,dom);

결과: 예상대로 작동하지 않는다.

"hi" 가 출력된 뒤 2초 뒤에  "안녕"을 기대했지만, 그대로 "hi" 가 출력된다.

Example2 가 호출은 되었지만, useState가 여전히 Example에서 사용한 값을 가져온다.

그리고 없는것보다 못하다는 생각으로 Example2에서 useState를 지우면 빨간오류를 확인 할 수 있다.

즉 처음에 호출했던 횟수와 나중에 호출한 횟수가 안맞으면 심각해진다??

hook에  대한 신뢰가 조금 무너지긴 하지만, 

공식홈의 글을 살펴보면 스테이트는 최상위에서만 쓰라고 한다.

https://ko.reactjs.org/docs/hooks-rules.html#explanation

 

정리를 하자면,

리엑트 돔.렌더에서 호출한 함수는 따로 식별을 하지만, 그 하위로 호출된 함수는 호출 순서에 의해 useState의 반환 값이 결정된다. 

 

그러한 사실을 바탕으로

hook의 기본기능만 간단하게 구현해보았다.

const Hook = {
    render: function (fn, dom) {
        const newObj = { fn: fn, values: [], needInit: true, needUpdate: false };
        dom.hook = dom.hook || newObj;
        clearInterval(dom.hook.clear);
        if (dom.hook.fn != fn) dom.hook = newObj;

        dom.hook.cur = 0;
        dom.hook.effects = [];
        Hook.current = dom;
        const child = fn();
        Hook.current = null;
        dom.hook.needInit = false;
        dom.innerHTML = '';
        dom.appendChild(child);

        setTimeout(() => {
            dom.hook.effects.forEach(element => element());
        }, 1);
        dom.hook.clear = setInterval(() => {
            if (dom.hook.needUpdate) {
                dom.hook.needUpdate = false;
                Hook.render(fn, dom);
            }
        }, 20);
    },
    useState: function useState(val) {
        const dom = Hook.current;
        if (!dom || !dom.hook) {
            throw new Error("Can't find hookObj. Use render.");
        }
        let newVal = null;
        const hook = dom.hook;
        if (hook.needInit) {
            newVal = { value: val };
            newVal.setvalue = function (val) {
                newVal.value = val;
                hook.needUpdate = true;
            }
            hook.values.push(newVal);
        } else {
            if (hook.cur >= hook.values.length) {
                throw new Error("FATAL ERROR: No more useState.");
            }
            newVal = hook.values[hook.cur];
        }
        hook.cur++;
        return Object.values(newVal);
    },
    useEffect: function (callback) {
        if (typeof callback != 'function') {
            throw new Error("is not function");
        }
        const dom = Hook.current;
        if (!dom || !dom.hook) {
            throw new Error("Can't find hookObj. Use render().");
        }
        dom.hook.effects.push(callback);
    }
}

 

테스트 소스.

function Example() {
    const [value, setValue] = Hook.useState(0);
    const [value2, setValue2] = Hook.useState(0);
    const [value3, setValue3] = Hook.useState(0);

    Hook.useEffect(() => {
        setTimeout(() => setValue(value + 1),50);
        setTimeout(() => setValue2(value2 + 1),75);
        setTimeout(() => setValue3(value3 + 1),100);
    })

    ele = document.createElement("h1");
    ele.innerText = `[${value}, ${value2}, ${value3}]`;

    return ele;
}

Hook.render(Example, document.getElementById('root'));
Hook.render(Example, document.getElementById('root2'));

 

동작 확인.

0
0

끝!

 

https://codepen.io/gitbeginer/pen/oNeaBYo

 

'React' 카테고리의 다른 글

JSX TO JS 변환 구현  (0) 2021.11.24
React virutal-DOM 구조 및 구현해보기.  (0) 2021.11.13