React Hooks 동작 방법

TwentyFiveSeven·2021년 2월 4일
0
post-custom-banner

React Hooks란 ?

리액트 Hooks는 16.8.0에서 새로 도입된 기능입니다. 기존에는 함수형 컴포넌트에서 상태를 관리하기 위해서는 클래스 컴포넌트 다시 작성해야 했습니다.
하지만 Hooks가 나오면서 함수형 컴포넌트에서도 상태를 가질 수 있게 되었습니다.

React Hooks가 왜 필요할까?

공식문서에서는 다음과 같이 설명합니다. 자세한 설명은 공식문서를 확인해주세요.

React Hooks는 어떻게 동작할까 ?

Hooks의 종류에는 (useState, useEffect, useCallback, ...) 등 많은 종류가 있습니다.
하지만 Hooks를 사용해보면 Hooks마다의 의미사용법만 익히면 쉽게 사용할 수 있다는 것을 알 수 있습니다.

앞서 얘기한 것 처럼 React Hooks는 의미와 사용법만 알고 있다면, 내부적으로 어떻게 구성되어있고, 돌아가는지 몰라도 쉽게 사용할 수 있는 캡슐화의 장점을 갖고 있다고 생각이 듭니다.

그러면 React Hooks가 어떻게 돌아가는지 모르고 계속 사용해도 되는 것일까?

엄밀히 말하자면 React Hooks가 어떻게 돌아가고 어떻게 구성되어있는지 모른다면, 사용하다가 원치않게 문제점들을 마주치게 될 것 입니다.

사용자는 의미와 사용법 밖에 모르는데 이러한 문제를 만나게 되면 패닉에 빠지게 될 것이고, 만약 찾는다고 해도 문제점을 피하는 '감'을 익힌 것이기 때문에 또 다시 문제를 마주했을 때 똑같은 어려움을 겪을 것입니다.

이제 React Hooks가 어떻게 동작하는지 알아보자.

우선 Hooks는 Closure를 기반으로 구현되어있습니다.
때문에 아직 Closure를 이해하지 못했다면 이곳에서 선행학습을 하시길 바랍니다.

우선 Hooks의 가장 기본적인 useState를 보이는 그대로 구현해보겠습니다.

function useState(initialValue) {
  var value = initialValue;
  function state() {
    return value;
  }
  function setState(newVal) {
    value = newVal;
  }
  return [state, setState]
}

위 코드의 state, setState 함수는 각각이 value 값의 getter, setter 함수입니다.
두함수는 클로저로서 렉시컬 스코프의 value 변수에 접근할 수 있습니다.

하지만 이 useState는 실제 useState와 차이점이 있습니다.
바로 실제 useState에서는 state 값이 함수가 아니기 때문에 함수를 호출하지 않아도 값을 알려준다는 것 입니다.

그럼 state값으로 value값을 넣어줘보자

function useState(initialValue) {
  var value = initialValue;
  function setState(newVal) {
    value = newVal;
  }
  return [value, setState]
}

const [state, setState] = useState(0);

이제 state 값을 함수 호출없이 사용할 수 있습니다 !!

하지만 또 다른 문제가 기다리고 있습니다...
바로 아무리 setState함수로 state값을 바꿔도 클로저가 아니기 때문에 처음 useState를 호출할 때 넣어줬던 initialValue 인자 값이 계속 그대로 나옵니다.

이 문제점은 모듈 패턴으로 해결할 수 있습니다.

const React = (function(){
  let value;
  return {
    render(Component){
      const component = Component();
      component.render();
      return component;
    },
    useState(initialValue){
      value = value || initialValue;
      const setState = function(newValue){
      	value = newValue;
      }
      return [value, setState];
    }
  }
})()

const [state, setState] = React.useState(1);

이처럼 모듈 패턴을 사용하여 value를 렉시컬 스코프의 변수로 선언해두면 getter 함수 없이도 setter 함수를 통해 변화하는 state값을 가져올 수 있습니다.

내친김에 useEffect도 함께 봐볼까요 ?

useEffect Hook은 첫번째 인자로 Callback 함수, 두번째 인자로 값의 리스트를 받아옵니다. 이를 바탕으로 위의 모듈패턴을 활용하여 useEffect를 구현해보겠습니다.

const React = (function(){
  let value,list;
  return {
    render(Component){
      const component = Component();
      component.render();
      return component;
    },
    useState(initialValue){
      value = value || initialValue;
      const setState = function(newValue){
      	value = newValue;
      }
      return [value, setState];
    },
    useEffect(callback,newList){
      const checkAlways = !newList //newList값이 특정되지않았으면 매번 Callback 실행
      const checkList = list ? !list.every((now,idx)=> now === newList[idx]): true;
      if(checkAlways || checkList){
        callback();
        list = newList;
      }
    }
  }
})()

const [state, setState] = React.useState(1);

이제 useEffect까지 완성되었다 !!

하지만 테스트하다보면 문제점을 찾을 수 있다.
바로 useState, useEffect를 각각 2개 이상씩 사용하면 앞서 선언한 초기값들은 무시되고, 최종 호출 값을 따라가는 문제점이 발생한다.

값이 덮어씌워지는 이 문제점은 어떻게 해결할까?

이 문제점의 해결방식은 마법이 아니고 어렵지않다.
바로 상태값들을 저장하기 위해 배열을 사용하는 것이다.
아래 변경된 코드를 봐보자.

const React = (function(){
  const hooks = [];
  let currentHook = 0;
  return {
    render(Component){
      const component = Component();
      component.render();
      return component;
    },
    useState(initialValue){
      hooks[currentHook] = hooks[currentHook] || initialValue;
      const setState = function(newValue){
      	hooks[currentHook] = newValue;
      }
      return [hooks[currentHook++], setState];
    },
    useEffect(callback,newList){
      const checkAlways = !newList //newList값이 특정되지않았으면 매번 Callback 실행
      const list = hooks[currentHook];
      const checkList = list ? !list.every((now,idx)=> now === newList[idx]): true;
      if(checkAlways || checkList){
        callback();
        hooks[currentHook] = newList;
      }
      currentHook++;
    }
  }
})()

const [state, setState] = React.useState(1);

정말 간단하다.
Hooks라는 배열안에 currentHook이라는 index값이 가리키는 순서대로 사용된 state, array 값들을 기억하고 호출될 때 마다 내가 기억하고 있는 순서대로 비교하는 것이다.

Hooks 배열을 통해 Hook을 관리하기 위해서는 2가지의 규칙을 만족해야 합니다.

  1. “최상위에서만 Hook을 호출해야 합니다”
  2. “오직 React 함수 내에서 Hook을 호출해야 합니다.”

간단히 설명하자면,

첫번째 규칙은

export const First = ({flag}) =>{
  const [val, setVal] = useState(false);
  if(flag){
    useEffect(()=>{console.log(val,val),[]);
  }
  
  useEffect(()=>{console.log(val)},[val]);
}
  • 만약 hooks를 선언한 순서가 변경됐을 때 (if문을 사용해서 특정시기에만 적용되는 useState가 존재할 때) hooks 배열은 기존에 저장해뒀던 순서대로 비교하기 때문에 서로 다른 2번째 useEffect와 3번째 useEffect가 비교되어 문제를 발생시킬 수 있다는 것을 뜻합니다.

두번째 규칙은

  • Hooks를 리액트 함수 컴포넌트에서 호출해야한다.
  • Hooks를 Custom Hooks에서 호출해야 한다.

즉 상태관련 로직에 의존하는 부분을 명확하게 구분하여 사용하라는 것입니다.

마치며

이번에는 React Hooks가 내부적으로 어떻게 동작하는지 useState, useEffect를 예시로 확인해봤습니다.
다른 Hooks들도 이와 유사하게 동작할 것이고 여기서 중요한 것이 클로저모듈 패턴을 이해하는 것이라고 생각합니다.
두가지를 잘 이해하고 본다면 충분히 쉽게 이해가 가능한 구조로 동작하고 있다고 생각이 듭니다.

참고

https://hewonjeong.github.io/deep-dive-how-do-react-hooks-really-work-ko/

profile
부지런한 웹개발자🌙
post-custom-banner

0개의 댓글