React는 어떻게 상태를 저장할까?

LUCAS·2023년 8월 5일
0

서론

리액트의 함수형 컴포넌트는 Hook을 통해 다양한 라이프사이클 메소드를 활용할 수 있습니다.

그렇다면 Hook은 어떻게 동작할까요?

Hook의 동작 방식

기본적으로 Hook은 클로저를 통해 동작합니다.

클로저는 JS의 기본 개념입니다. 그럼에도 불구하고 많은 개발자에게 혼란을 주는 것으로 유명한데요.
You Don't Know JS의 Kyle Simpson은 클로저를 다음과 같이 정의합니다.

클로저는 함수가 어휘 범휘 밖에서 실행되는 경우에도 함수가 어휘 범위를 기억하고 액세스할 수 있는 경우입니다.

그렇다면 이제 클로저를 통해 React의 useState를 구현해봅시다.

useState 구현

일반적으로 useState는 어떻게 쓰일까요?
그 특징을 함께 살펴봅시다.

const [foo, setFoo] = useState(0);
// useState는 배열을 반환하며 배열의 길이는 2입니다.
// 첫 번째 인덱스는 상태의 값을 나타내며,
// 두 번째 인덱스는 상태를 변경하는 업데이트 함수로 이루어져 있습니다.

이러한 인터페이스를 프로토타입으로 개발하면 다음과 같습니다.

const React = (function() {
  let _val;
  
  return {
  	render(Component) {
      const Comp = Component();
      Comp.render();
      return Comp;
    },
    useState(initialValue) {
      _val = _val || initialValue;
      
      const setState = (newVal) => {
	    _val = newVal;
      }
      
      return [_val, setState];
    }
  }
})()
function Counter() {
  const [count, setCount] = React.useState(0);
  
  return {
    click: () => setCount(count + 1),
    render: () => console.log('render:', { count })
  }
}

let App;
App = React.render(Counter); // render: { count: 0 }
App.click();
App = React.render(Counter); // render: { count: 1 }

위 프로토타입은 모듈 패턴, 그리고 클로저를 통해 useState를 구현하였습니다.

꽤 괜찮은 클론처럼 보이지만 결국 잘못 구현된 싱글톤일 뿐입니다.
우리는 다음과 같이 하나의 컴포넌트에서 useState를 여러번 사용할 수 있습니다.

function Counter() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
}

현재와 같은 싱글톤 패턴이라면, 동작하지 않을 것입니다.
따라서 다음과 같이 배열 형태로 Hook을 관리해야합니다.

const ReactV2 = (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];
    }
  }
})()

이렇게 하면 다음과 같이 다수의 useState 사용에 대응할 수 있습니다.

function Counter() {
  const [count, setCount] = ReactV2.useState(0);
  const [text, setText] = ReactV2.useState('foo');
  
  return {
    click: () => setCount(count + 1),
    type: txt => setText(txt),
    render: () => console.log('render', { count, text })
  }
}

let App;
App = ReactV2.render(Counter);
// render { count: 0, text: 'foo' }
App.click();
App = ReactV2.render(Counter);
// render { count: 1, text: 'foo' }
App.type('bar');
App = ReactV2.render(Counter);
// render { count: 1, text: 'bar' }

또한 이제는 사용자 정의 Hook을 만들 수도 있겠네요.

function Component() {
  const [text, setText] = useSplitURL('www.ridibooks.com');
  
  return {
    type: txt => setText(txt),
    render: () => console.log({ text })
  }
}

function useSplitURL(str) {
  const [text, setText] = ReactV2.useState(str);
  const masked = text.split('.');
  return [masked, setText];
}

let App;
App = ReactV2.render(Component);
// { text: ['www', 'ridibooks', 'com'] }
App.type('www.reactjs.org');
App = ReactV2.render(Component);
// { text: ['www', 'reactjs', 'org' ] }

정리

지금까지 자바스크립트의 기본 문법을 통해 Hook을 구현해보았습니다.
이제 정리해볼까요?

Hook 규칙 도출

우리는 앞서 useState를 직접 구현하며 어떻게 동작할 수 있는지 알아보았습니다.
여기에서 우리는 첫 번째 Hook 규칙인, "최상위 레벨에서만 훅을 호출"한다는 것을 쉽게 이해할 수 있습니다.

우리는 변수를 사용해서 호출 순서에 대한 React의 의존도를 명시적으로 모델링했습니다.

또한 두 번째 규칙인, "리액트 컴포넌트에서만 훅을 호출" 할 수 있다는 규칙도 필요에 의해 생겨난 규칙임을 알 수 있게 되었습니다.

...


출처

https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

profile
안녕하세요! FE개발자 최근원입니다.

0개의 댓글