리액트와 클로저 2편

창건·2022년 7월 16일
1

리액트

목록 보기
4/5

useEffect 구현하기

이전에 useState를 구현해보았습니다.

이번에는 useEffect를 추가해 보겠습니다.

기존에 useState를 구현했던 MyReact에 useEffect 메소드를 추가하고,

Counter 함수에는 useEffect를 실험하기 위한 noop이라는 메소드를 추가했습니다.

const MyReact = (function () {
  let _val, _deps; // _deps는 의존성 배열을 저장하고 있습니다.

  return {
    render(Component) {
      const C = new Component();
      C.render();
      return C;
    },
    useState(initVal) {
      _val = _val || initVal;
      function setState(newVal) {
        _val = newVal;
      }
      return [_val, setState];
    },
    useEffect(callback, depArray) {
      const oldDeps = deps;
      let hasChanged = true;
      if (oldDeps) {
      	hasChanged = depArray.some((dep, i) => !Object.is(dep, _deps[i]));
      }
      if (hasChanged) cb();
      _deps = depArray;
    },
  };
})();

function Counter() {
  const [count, setCount] = MyReact.useState(0);
  MyReact.useEffect(() => {
    console.log('effect: ', count);
  }, [count]);

  return {
    render() {
      console.log('render: ', count);
    },
    click() {
      setCount(count + 1);
    },
    noop() {
      setCount(count);
    },
  };
}

let App;
App = MyReact.render(Counter); // effect: 0 render:  0

App.click();
// 새로운 count 값을 받기 위해 컴포넌트 함수를 다시 호출합니다.
App = MyReact.render(Counter); // effect: 1 render: 1 

App.noop();
App = MyReact.render(Counter); // render: 1
  • useEffect는 callback 함수와, depArray라는 의존성 배열을 매개변수로 갖고 있습니다.

    Counter 컴포넌트를 새로 렌더링할 때마다 useEffect가 호출됩니다.

  • useEffect는 내부에서 전달된 의존성 배열이 있는지 없는지 확인합니다.

    만약 없다면, callback함수가 무조건 호출됩니다.

  • 전달된 의존성 배열이 있다면 기존에 갖고 있던 의존성 배열(_deps)와 비교합니다.

    의존성 배열의 값에 변화가 있다면 callback함수가 호출됩니다.

  • 마지막으로, _deps에 변화된 의존성 배열을 반영합니다.

싱글톤 모듈을 개선하기

MyReact 모듈은 한가지 치명적인 문제가 있습니다. 싱글톤 형태이기 때문에, 여러가지 훅을 사용할 수 없다는 것입니다.

useState는 _val을, 그리고 useEffect는 deps라는 각각 하나의 변수만을 참조하고 있습니다.

이렇게 되면 여러개의 useState나 useEffect를 사용할 수 없습니다.

이를 개선한 코드는 다음과 같습니다.

훅이 참조하는 값들을 배열에 담겠습니다.

const MyReact = (function () {
  let hooks = [];		// hook이 참조하는 값들을 저장하는 배열
  let idx = 0;	// 위 배열을 순회하기 위한 인덱스

  return {
    render(Component) {
      const C = Component();
      C.render();
      idx = 0; // 다음 렌더링을 위한 리셋, 이 부분을 잘 기억해야 합니다!
      return C;
    },
    useState(initVal) {
      hooks[idx] = hooks[idx] || initVal;
      const _idx = idx;
      // 약간 의문이 드는 코드
      // 아래에 설명이 이어집니다!
      function setState(newValue) {
        hooks[_idx] = newValue;
      }
      return [hooks[idx++], setState];
    },
    useEffect(callback, depArray) {
      const oldDeps = hooks[idx];
      let hasChanged = true;
      if (oldDeps) {
      	hasChanged = depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));
      }
      if (hasChanged) cb();
      hooks[idx] = depArray;
      idx++;
    },
  };
})();

function Counter() {
  const [count, setCount] = MyReact.useState(0);
  const [message, setMessage] = MyReact.useState('hi');
  MyReact.useEffect(() => {
    console.log('effect: ', count, message);
  }, [count, message]);

  return {
    render() {
      console.log('render: ', count, message);
    },
    newMessage(text) {
      setMessage(text);
    },
    click() {
      setCount(count + 1);
    },
    noop() {
      setCount(count);
    },
  };
}

let App;
App = MyReact.render(Counter);
// effect: 0 hi
// render: 0 hi
App.click();
App = MyReact.render(Counter);
// effect: 1 hi
// render: 1 hi
App.newMessage('bye');
App = MyReact.render(Counter);
// effect: 1 bye
// render: 1 bye
  • 작성하다보면 useState에서 이상하다고 느낀 부분이 있을 수 있습니다.

    useState(initVal) {
          hooks[idx] = hooks[idx] || initVal;
          const _idx = idx;
          function setState(newVal) {
            hooks[_idx] = newVal;
          }
          return [hooks[idx++], setState];
    }

    굳이 setStateHookIndex에 currentHook을 할당하여 새로운 변수를 만들어 쓰고 있습니다.

    그냥 currentHook을 쓰면 되지 않을까요? 아래처럼요

    useState(initVal) {
          hooks[idx] = hooks[idx] || initialValue;
          function setState(newVal) {
            hooks[idx] = newVal;
          }
          return [hooks[idx++], setState];
    }

    setState()가 호출될 때는 컴포넌트가 렌더링이 끝난 후 입니다.

    따라서 idx이 다시 0으로 리셋이 되어버린 것이죠.

    때문에 변수를 하나 더 선언하고(_idx) 값을 묶어놓아야 합니다.

Tip: 훅은 Top-Level에 작성한다.

Top-Level에 작성한다는 것은 함수형 컴포넌트의 맨 윗줄에 작성하라는 뜻이 아닙니다!

조건문, 반복문 등에 중첩되지 않게 작성하라는 뜻입니다.

만약 조건문안에 훅을 작성하여, 훅이 호출될 때가 있고 호출되지 않을 때도 있다면, 훅이 참조하는 배열의 인덱스에 혼란이 올 수 밖에 없겠죠.

profile
피곤한만큼 성장할 수 있으면

0개의 댓글