코반주반

react hooks 직접 구현해보기

어떤 기술을 맨날 사용하다보면, 스스로가 잘 알고 있다고 착각을 하기 쉽다.

늘 경계하고 '정말 잘 알고 사용하고 있는가? 누구에게 설명할 수 있을 정도로 알고 있는가?' 에 대한 질문을 스스로에게 던지는게 중요한 것 같다.

스스로가 잘 알고 있다고 생각하는 부분은 공부를 소홀히 하게 되기 마련이니까...

 

오늘은 react hooks 를 간단하게 직접 만들어봤다.

 

단일 state 및 effect deps를 관리하는 singleton 방식의 react, hooks 배열을 통해 조금 더 실제 react hooks와 비슷하게 구현한 방식으로 만들었다.

 

첫 번째 예제는 hook의 작동방식과 closer의 연관성에 대해 이해하기에 좋은 것 같고, 두 번째 예제는 'hooks는 마법이 아니라 배열일 뿐' 이라는 말을 이해하기에 좋은 것 같다.

 

myReactSingleton.js

const React = (function () {
  let _state;
  let _deps;

  return {
    // 기본적인 렌더함수 생성
    render(component) {
      // functional component 호출
      const Component = component();
      // 렌더함수 호출
      Component.render();
      // 컴포넌트 반환
      return Component;
    },
    // useState Hook 생성
    useState(initial) {
      // 기존 state 없을 경우 initial
      _state = _state || initial;
      // 값 업데이트는 값을 직접 넣던지, prev update 함수를 넣던지.
      function setState(updatedValueOrCb) {
        if (typeof updatedValueOrCb === "function") {
          _state = updatedValueOrCb(_state);
        } else {
          _state = updatedValueOrCb;
        }
      }
      return [_state, setState];
    },
    // useEffect는 콜백, deps Array를 받는다.
    useEffect(cb, deps) {
      // 의존성 존재여부 확인
      const noDeps = !deps;
      // 업데이트 필요여부
      const isDepsChange = _deps ? !deps.every((d, i) => d === _deps[i]) : true;
      // 실행 후 deps 상태 갱신
      if (noDeps || isDepsChange) {
        cb();
        _deps = deps;
      }
    },
  };
})();

function Home() {
  const [clickCount, setClickCount] = React.useState(0);

  const handleDoubleClick = () => {
    setClickCount((prev) => prev + 2);
  };
  const handleClick = () => {
    setClickCount(clickCount + 1);
  };
  const handleNoClick = () => {
    setClickCount(clickCount);
  };

  React.useEffect(() => {
    console.log("useEffect 클릭 횟수 --> ", clickCount);
  }, [clickCount]);

  //! 싱글톤 방식이라 같은 deps를 받는 2번째 useEffect는 실행되지 않음. 이미 위의 effect 에서 deps가 업데이트 되었기 때문
  React.useEffect(() => {
    console.log("실행되지 않는 useEffect --> ", clickCount);
  }, [clickCount]);

  const render = () => {
    console.log(`render 클릭 횟수 --> ${clickCount}`);
    return `HOME`;
  };
  return { render, handleNoClick, handleClick, handleDoubleClick };
}

let App;

// 컴포넌트 마운트
App = React.render(Home);
//useEffect 클릭 횟수 -->  0
//render 클릭 횟수 --> 0


// setState 값을 직접 넣는 방식
App.handleClick();
App = React.render(Home);
//useEffect 클릭 횟수 -->  1
//render 클릭 횟수 --> 1


// setState 업데이트 방식
App.handleDoubleClick();
App = React.render(Home);
//useEffect 클릭 횟수 -->  3
//render 클릭 횟수 --> 3


// effect callback 실행되지 않음
App.handleNoClick();
App = React.render(Home);
//render 클릭 횟수 --> 3

 

myReactHooksArray.js

const React = (function () {
  // let _state;  let _deps;
  // 2개 이상의 컴포넌트에서 훅을 호출하기 위해 배열로 변환
  let _hooks = [];
  // 훅 호출을 위한 인덱스 관리
  let _currentHookIndex = 0;

  return {
    // 기본적인 렌더함수 생성
    render(component) {
      // component effect 실행
      const Component = component();
      // 렌더함수 호출
      Component.render();
      // 훅 인덱싱 초기화
      _currentHookIndex = 0;

      // 컴포넌트 반환
      return Component;
    },
    // useState Hook 생성
    useState(initial) {
      // hooks 배열에서 기존 state를 찾아온다.
      _hooks[_currentHookIndex] = _hooks[_currentHookIndex] || initial;
      // hook index 더해주기 전에 클로저로 접근할 수 있게 빼준다
      const prevHookIndex = _currentHookIndex;
      // 미리 빼준 index를 사용해서 값을 업데이트한다.
      const setState = (updatedValueOrCb) => {
        if (typeof updatedValueOrCb === "function") {
          _hooks[prevHookIndex] = updatedValueOrCb(_hooks[prevHookIndex]);
        } else {
          _hooks[prevHookIndex] = updatedValueOrCb;
        }
      };
      // 다음 훅을 위해 인덱스를 올려준다.
      _currentHookIndex++;
      // state와 setState 반환.
      return [_hooks[prevHookIndex], setState];
    },
    useEffect(cb, deps) {
      // 의존성 존재여부 확인
      const noDeps = !deps;
      // 업데이트 필요여부
      const effectHookDeps = _hooks[_currentHookIndex];
      const isDepsChange = effectHookDeps
        ? !deps.every((d, i) => d === effectHookDeps[i])
        : true;
      // 실행 후 deps 상태 갱신
      if (noDeps || isDepsChange) {
        cb();
        _hooks[_currentHookIndex] = deps;
      }
      // 다음 훅을 위해 인덱스를 올려준다.
      _currentHookIndex++;
    },
  };
})();

function Home() {
  const [clickCount, setClickCount] = React.useState(0);

  const handleDoubleClick = () => {
    setClickCount((prev) => prev + 2);
  };
  const handleClick = () => {
    setClickCount(clickCount + 1);
  };
  const handleNoClick = () => {
    setClickCount(clickCount);
  };

  React.useEffect(() => {
    console.log("useEffect 클릭 횟수 --> ", clickCount);
  }, [clickCount]);

  // ? 이제 동일 deps를 가진 useEffect 역시 실행이 가능하다.
  React.useEffect(() => {
    console.log("useEffect 클릭 횟수 --> ", clickCount);
  }, [clickCount]);

  const render = () => {
    console.log(`render 클릭 횟수 --> ${clickCount}`);
    return `HOME`;
  };
  return { render, handleNoClick, handleClick, handleDoubleClick };
}

let App;

// 컴포넌트 마운트
App = React.render(Home);
//useEffect 클릭 횟수 -->  0
//useEffect 클릭 횟수 -->  0
//render 클릭 횟수 --> 0

// setState 값을 직접 넣는 방식
App.handleClick();
App = React.render(Home);
//useEffect 클릭 횟수 -->  1
//useEffect 클릭 횟수 -->  1
//render 클릭 횟수 --> 1

// setState 업데이트 방식
App.handleDoubleClick();
App = React.render(Home);
//useEffect 클릭 횟수 -->  3
//useEffect 클릭 횟수 -->  3
//render 클릭 횟수 --> 3

// effect callback 실행되지 않음
App.handleNoClick();
App = React.render(Home);
//render 클릭 횟수 --> 3

 

 

참고 포스팅 :

 

Deep dive: How do React hooks really work? | Netlify

In this article, we reintroduce closures by building a tiny clone of React Hooks. This will serve two purposes - to demonstrate the effective use of closures in building our React Hooks clone itself, and also to implement a naive mental model of how Hooks

www.netlify.com