useState를 바닐라JS로 구현하기

     

    이전 글에서는 React의 공식 문서를 참조하여 useState의 개념을 정리했습니다. 하지만 공부하는 과정에서 useState가 어떻게 동작하는지 내부적으로 구성되어 있는지 궁금했습니다.

    그래서 JavaScript로 useState를 직접 구현해보면서 동작 원리를 이해하는 것을 목표로 했습니다.

     

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="root"></div>
    
      <script>
          let state = undefined;
          function useState(initialState) {
            state = state ? state : initialState;
    
          function setState(newState) {
            // 상태값 업데이트
            state = newState;
            // // 상태가 업데이트 되었으므로 컴포넌트 렌더링을 해야함.
            render();
          }
    
          return [state, setState];
        }
    
        function Counter() {
          const [count, setCount] = useState(0);
    
          // onclick 속성에서 함수를 호출할 때 해당 함수를 찾을 수 없음
          // function increment() {
            // setCount(count + 1);
          // }
    
          // 전역 스코프에 정의
           window.increment = () => {
              setCount(count + 1);
            };
    
          return `
            <div>${count}</div>
            <button onclick='increment()'>Increment</button>
          `;
        }
    
        function render() {
          const root = document.getElementById('root');
          root.innerHTML = Counter();
        }
    
        render();
      </script>
    </body>
    </html>
    1. HTML 구조 설정
      • <div id="root"></div>는 나중에 컴포넌트가 렌더링될 위치를 나타낸다.
    2. useState 함수
      • state 관리
        • state는 외부에서 관리를 한다.
        • 내부에서 관리하게 되면 useState 실행할때마다 state를 초기값으로 변경되기 때문에!
          • state는 state값이 있으면 state 값이고 없다면 초기값으로 저장된다.
      • setState함수는 새로운 상태값을 받아 state 변수를 업데이트 하고, render 함수를 호출하여 컴포넌트를 다시 렌더링한다.
    3. Counter 컴포넌트
      • 버튼을 누르면 카운터 기능을 가진 컴포넌트
      • useState(0)을 호출해 초기값 0을 가진 count 상태와 setCount함수를 받는다.
      • increment 함수는 setCount를 호출해 count 상태를 1씩 증가시킨다.
        • setCount 함수는 새로운 상태값을 받아 상태를 업데이트하고, render함수를 호출하여 컴포넌트를 다시 렌더링 한다.
    4. render 함수
      • 컴포넌트를 렌더링 하는 함수
      • root요소를 찾아 Counter 컴포넌트를 HTML로 변환하고 root요소에 넣는다.

    setState에 함수형 업데이트를 넣으면 어떻게 될까? setState가 함수인지 아닌지 확인하고, 함수이면 반환된 값으로 업데이트 되게 코드를 수정했다. 

     

    아래와 같이 setCount를 세 번 호출하면 count 상태값이 즉시 업데이트가 되지 않기 때문에 count값이 한 번만 업데이트된다.

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
      </head>
      <body>
        <div id="root"></div>
    
        <script>
          let state = undefined;
          function useState(initialState) {
            state = state ? state : initialState;
    
            function setState(newState) {
              state = typeof newState === 'function' ? newState(state) : newState;
              render();
            }
            return [state, setState];
          }
    
          function Counter() {
            const [count, setCount] = useState(0);
    
            window.increment = () => {
              setCount(count + 1);
              setCount(count + 1);
              setCount(count + 1);
            };
    
            return `
            <div>${count}</div>
            <button onclick='increment()'>Increment</button>
            `;
          }
    
          function render() {
            const root = document.getElementById('root');
            root.innerHTML = Counter();
          }
    
          render();
        </script>
      </body>
    </html>
    state = typeof newState === 'function' ? newState(state) : newState

    newState가 함수이면, 함수를 호출하여 반환된 값을 상태로 업데이트하고, 함수가 아니라면 그대로 새로운 상태값으로 설정한다.

     

    window.increment = () => {
      setCount(count + 1);
      setCount(count + 1);
      setCount(count + 1);
    };
    • 현재 count (초기값인 0이다.) 상태값을 세 번 모두 사용하므로, count값은 실제로 한 번만 증가한다.

     

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
      </head>
      <body>
        <div id="root"></div>
    
        <script>
          let state = undefined;
          function useState(initialState) {
            state = state ? state : initialState;
    
            function setState(newState) {
              state = typeof newState === 'function' ? newState(state) : newState;
              render();
            }
            return [state, setState];
          }
    
          function Counter() {
            const [count, setCount] = useState(0);
    
            window.increment = () => {
              setCount(prevCount => prevCount + 1);
              setCount(prevCount => prevCount + 1);
              setCount(prevCount => prevCount + 1);
            };
    
            return `
            <div>${count}</div>
            <button onclick='increment()'>Increment</button>
            `;
          }
    
          function render() {
            const root = document.getElementById('root');
            root.innerHTML = Counter();
          }
    
          render();
        </script>
      </body>
    </html>

    - 위의 코드는 setCount 함수를 세 번 호출하지만, 이전 값을 매개 변수로 받아 업데이트된 값을 반환하는 콜백 함수를 사용한다.  따라서 각 호출마다 count의 값이 1씩 증가하게 된다.

     

    여러 컴포넌트에서 useState를 사용한다면? state 관리는 어떻게 할까?

    • 각 컴포넌트의 상태를 states 배열로 관리함으로, 각 컴포넌트는 독립적으로 상태를 관리하게 한다.
    • 각 컴포넌트별로 고유한 currentStateKey를 가진다. => 인덱스 번호
    •  states 배열에 고유한 currentStateKey의 value에 state를 저장하고 setState를 호출하면 state를 업데이트한 값을 저장한다.

     

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
      </head>
      <body>
        <div id="root"></div>
    
        <script>
          // 각 컴포넌트별 고유한 key값
          let currentStateKey = 0;
          // states 배열
          const states = []
    
          function useState(initialState) {
            const key = currentStateKey
            // 초기값
            if (states.length === currentStateKey) {
              states.push(initialState)
            }
            const state = states[key]
    
            function setState(newState) {
              // 각 key에 value값인 state값을 업데이트함.
              states[key] = typeof newState === 'function' ? newState(state) : newState;
    
              render();
            }
    
            currentStateKey += 1
            return [state, setState];
          }
    
          function Counter() {
            const [count, setCount] = useState(0);
    
            window.increment = () => {
              setCount(prevCount => prevCount + 1);
              console.log(states)
            };
            
            return `
            <div>${count}</div>
            <button onclick='increment()'>Increment</button>
            `;
          }
          
          function ChangeClick() {
            const [isclick, setIsClick] = useState(true);
            
            window.clickhandler = () => {
              setIsClick(!isclick)
              console.log(states)
            }
            return `
            <div>${isclick}</div>
            <button onclick='clickhandler()'>click me!</button>
            `;
          }
    
          function render() {
            const root = document.getElementById('root');
            root.innerHTML = `<div>${Counter()}${ChangeClick()}</div>`;
            // 랜더링 될때마다 현재 key값을 초기화 시켜줘야 함.
            currentStateKey = 0;
          }
    
          render();
        </script>
      </body>
    </html>

     

     

    단순 호기심으로 시작했던 setState 구현해보기! 처음부터 혼자 구현했으면 더 좋았을지도 모르겠다는 생각이 듭니다. 하지만 render 함수를 구현하고, state에 초기값을 할당하고 setState를 호출하여 업데이트된 값을 state에 재할당하는 정도만 가능했습니다. 여러 참고 자료를 읽고 다른 사람들의 코드를 이해하며 작성하는 것만으로도 setState 작동 원리를 이해할 수 있었고, 이는 제게 큰 성취감을 주는 좋은 경험이었습니다.

     


    참고 자료

     

    https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Make-useSate-hook/#_2-bottom-up-%E1%84%87%E1%85%AE%E1%86%AB%E1%84%89%E1%85%A5%E1%86%A8

    개발자 황준일님의 코드를 보고 학습했습니다.

    https://stackoverflow.com/questions/64744252/how-to-replicate-usestate-with-vanilla-js

    https://codesandbox.io/s/ff4rx?file=/src/index.js 

     

    댓글