본문 바로가기

Frontend/React

[React] React Hook 동작 원리(useState,useEffect)

728x90
렉시컬 스코프, 클로저에 대한 공부를 하고
hook의 동작 원리에 대해 궁금해져 공부해 본 것에 대한 기록이다.

개요

지난 JS 기록에서 렉시컬 스코프와 클로저에 대해 다뤘다.

 

[JS] 클로저를 이용한 정보 은닉 구현(클로저, 렉시컬 스코프 개념)

일급 객체를 공부하다 보니 렉시컬 스코프, 클로저에 대한 궁금증이 생겨 이에 대해 공부한 기록이다. Lexical Scope 렉시컬 스코프는 말 그대로 어휘의 범위이다. 변수가 사용 가능한 범위를 알기

choi-records.tistory.com

위의 기록에서 나아가 React Hook의 동작 원리를 이해해보려고 한다.

 

먼저 useState, useEffect 순서로 원리를 이해하고,

이를 바탕으로 다른 Hook들이 어떻게 작동하는지에 대한 원리를 이해할 것이다.

useState

useState는 리액트의 대표적인 가장 중요한 훅이라고 할 수 있다.

 

[React] useState로 button과 input 관리하기

State state는 리액트에서 데이터 값을 저장하는 객체이다. state가 아닌 변수에 저장하고 데이터를 변화시키면 update 작업이 일어나지 않아 변수의 변화가 렌더링 되지 않는다. 함수형 컴포넌트에서

choi-records.tistory.com

상태값과 상태를 수정할 수 있는 함수를 배열로 반환한다.

 

실제 useState와 유사한 기능을 하는 useState함수를 만들어보자.

  function useState(initialValue) {
    var _val = initialValue;
    function setState(newValue) {
      _val = newValue;
    }
    return [_val, setState];
  }
  
  var [v, setV] = useState(0);
  
  console.log("old", v);
  setV(1);
  console.log("new", v);

useState 함수는 initialValue를 초기값으로 갖는 변수

해당 변수를 수정하는 setState를 반환한다.

기존 useState와 유사한 모습이다.

 

하지만 콘솔에는 아래와 같이 출력된다.

위에서 v 변수는 초기 useState 호출에서 참조한 값이다.

따라서 setState에 의해 값이 변한다고 해도 참조한 값이 변하지 않는다.

 

이를 해결하기 위해서는 다른 클로저 안으로 클로저를 이동시켜야 한다.

모듈 패턴을 이용하여 useState를 가지는 MyReact를 만들어보자.

const MyReact = (function () {
  let _val;
  
  return {
      render(Component) {
      const Comp = Component();
      Comp.render();
      return Comp;
    },
    
    useState(initialValue) {
      _val = _val || initialValue;
      
      function setState(newValue) {
        _val = newValue;
      }
      
      return [_val, setState];
    }
  };
})();

MyReact는 변수 _val을 가지며

render와 useState 메서드를 반환하고, useState는 다시 내부에서 _val을

MyReact의 _val이나 전달받은 initialValue로 설정한다.

 

덕분에 외부에서 _val을 호출하게 되면

그때마다 _val은 MyReact의 _val을 참조할 것이다.

그렇게 되면 setState의 변경값을 반영할 수 있다. 직접 보자.

  function Counter() {
    const [count, setCount] = MyReact.useState(0);
    return {
      click: () => setCount(count + 1),
      render: () => console.log(count)
    };
  }
  let App;
  App = MyReact.render(Counter);
  App.click();
  App = MyReact.render(Counter);

생성한 MyReact 모듈의 useState를 통해 Counter 함수에서 setState 실행과 state 출력 함수를 반환한다.

위처럼 이제 변한 값을 참조하는 것을 볼 수 있다.

 

하지만 위의 useState는 싱글톤 형태로 하나의 상태만을 반영한다.

즉, 두 개 이상의 상태를 관리할 수 없다.

  function Counter() {
    const [count, setCount] = MyReact.useState(0);
    const [testCount, setTestCount] = MyReact.useState(0);
    return {
      click: () => {
        setCount(count + 1);
        setTestCount(testCount + 2);
      },
      render: () => {
        console.log("count", count);
        console.log("testCount", testCount);
      }
    };
  }
  let App;
  App = MyReact.render(Counter);
  App.click();
  App = MyReact.render(Counter);

코드를 위와 같이 수정했다.

testCount가 count와 별개로 증가할 것 같지만 그렇지 않다.

위와 같이 count와 testCount는 같은 값을 갖는다.

같은 변수를 참조하니 당연한 결과다.

 

이 버그를 해결하기 위해서는 상태값을 변수가 아닌 배열로 관리해야 한다.

const MyReact = (function () {
  let hooks = [];
  let currentHook = 0;
  return {
    render(Component) {
      const Comp = Component();
      Comp.render();
      currentHook = 0;
      return Comp;
    },
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue;
      const setStateHookIndex = currentHook;
      const setState = (newState) => (hooks[setStateHookIndex] = newState);
      return [hooks[currentHook++], setState];
    }
  };
})();

MyReact를 위와 같이 수정해준다.

결국 hook들은 배열에 담긴다. 서로 다른 hook이라는 것을 기억하기 위해서 말이다.

 

그리고 렌더링 될 때마다 currentHook을 0으로 만들어

본인의 값 즉, 자리를 찾아가는 형식이다.

 

이렇게 hook을 배열로 구분해 주면

여러 개의 상태를 관리할 수 있다.

 

위의 코드에서 setStateHookIndex가 의미하는 것은 무엇일까?

currentHook을 그대로 사용하게 되면 선언된 당시의 currentHook인 0의 값을 참조하게 된다.

setStateHookIndex를 없애게 되면 setCount, setTestCount 모두 인덱스가 0인 count의 값을 변경시킨다.

따라서 setTestCount에 의해 계속해서 0의 값을 갖는 testCount에서 2를 증가시킨 2가 계속해서 출력된다.


useEffect

이번엔 useEffect를 구현해 보자.

useEffect는 deps 배열을 받아 배열 내부 상태값이 변할 때마다 콜백 함수를 실행시킨다.

배열 내부의 상태값을 hook 배열에 저장해야 한다.

    useEffect(callback, depArray) {
      const hasNoDeps = !depArray
      const deps = hooks[currentHook] // type: array | undefined
      const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
      if (hasNoDeps || hasChangedDeps) {
        callback()
        hooks[currentHook] = depArray
      }
      currentHook++
    },

만약 deps가 없을 경우에는 매번 실행하는 것이므로 위와 같이 작성하면 된다.

 

이제 Hook은 상태 관련 동작 및 부수 작용을 캡슐화하는 방법이라는 말을 이해할 수 있게 됐다.

Hook은 배열로 상태를 관리하며 상태에 관한 부수 작용을 해준다.


참고

 

[React hooks] 리액트 훅의 원리 : 단지 배열일 뿐

리액트 훅이 배열임임을 알아봅니다. 원문 : https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e React hooks: not magic, just arrays Untangling the rules around the proposal using diagrams medium.com 저는 새로운 h

itchallenger.tistory.com

 

[번역] 심층 분석: React Hook은 실제로 어떻게 동작할까?

React Hook에 대해 이해하려면 JavsScript 클로저에 대해 잘 알아야합니다. React의 작은 복제본을 만들어보며 클로저와 hook의 동작 방식을 알아봅니다.

hewonjeong.github.io

마무리

아직 클로저, 렉시컬 스코프에 대한 이해가 완전하지 않아

100프로 이해했다고는 할 수 없을 것 같다.

 

계속 복습하고 훈련하며 완전히 이해할 수 있을 때까지 공부해야겠다.

 

잘못된 정보에 대한 피드백은 환영입니다.

감사합니다.

728x90