Hengxi's 개발 블로그

[React] useState의 동작원리 본문

개발/React

[React] useState의 동작원리

HENGXI 2022. 11. 13. 19:55

React Hooks에서 가장 많이 사용되는 useState...!

가장 많이 사용하고 있음에도, useState가 정확히 어떻게 동작하는지 생각해 본 적이 없는 것 같다. React를 깊게 공부해보고 싶었고 이번 기회에 useState의 동작 원리에 대해 정리를 해보고자 했다.

 

클로져(Closure)

그전에 useState를 이해하기 위해 javascript의 개념인 클로져를 알고 가면 좋을 것 같다.

 

클로져는 자신이 사용하는 변수를 기억하고 어딘가에 저장해두는 특성이 있다. 변수를 Capture 한다고 하며, 일반적으로 사라져야 할 변수라도 어떤 클로져에서 사용된다면 사라지지 않고 잡아 붙들린 것이라고 보면 되겠다.

function outer() {
  let outerVar = 1;
  function inner() {
    console.log(outerVar);
  }
  return inner;
}
const closure = outer();
closure(); // 출력: 1

위 코드를 단순히 보면 outer() 안의 outerVar는 outer()의 호출이 끝나면 없어져야 한다. outer()가 실행되는 scope의 environment(환경)는 실행이 완료되면 분명 없어지기 때문이다.

하지만 outerVar는 살아남아 closure()를 호출했을 때 1이 호출된다.

 

outer를 호출하면 그 반환 값으로 명령 흐름을 담고 있는 inner라는 클로저를 얻을 수 있다. 이 inner는 실행하면 outerVar를 호출한다. 이 inner 가 정의되는 시점에 그 환경에는 outerVar 가 존재한다. 심지어 inner는 이 outerVar를 사용하기도 한다. 따라서 이 inner 가 살아있는 한 outerVar 도 죽지 못하고 살아남게 된다.

 

즉, outer()가 실행되는 scope가 없어지더라도 아직 inner 가 살아있다면 outerVar 도 사라지지 않는 것이다.

 

UseState의 동작원리

useState의 동작 원리도 위에서 설명한 클로져의 특징을 이용한다. 

const MyReact = (function() {
  let _val // hold our state in module scope
  return {
    render(Component) {
      const Comp = Component()
      Comp.render()
      return Comp
    },
    useState(initialValue) {
      _val = _val || initialValue
      function setState(newVal) {
        _val = newVal
      }
      return [_val, setState]
    }
  }
})()

위 코드는 우리가 사용하는 React 모듈 코드를 예로 든 것이다. MyReact 모듈은 익명 함수로부터 두 개의 Closure를 반환받아 저장하고 있는 모듈이다. render와 useState 이 두 가지를 제공해주고 있다. render는 컴포넌트를 랜더링 해주는 메서드이다.

 

_val은 익명 함수 scope 안에서 정의되는 것으로, 우리가 원하는 상태를 저장해 줄 것이다. 이는 익명 함수의 동작이 끝나고 나서도 유지된다. 이 _val을 바라보고 있는 useState가 MyReact에 저장되어 있기 때문에, _val은 이 메모리 세상 어딘가에 존재하게 될 것이다.

 

useState를 살펴보면, _val은 처음에는 undefined가 할당되어 있을 수 밖에 없는데, undefined일 경우에는 initialValue를 할당한다. useState가 두 번째로 불릴 때부터는 _val에 이미 값이 할당되었을 것이므로, 기존 값을 그대로 사용한다. 

 

useState 메서드 안에도 setState 메서드가 있다. 컴포넌트가 useState를 사용하면 반환 받는 setter로, 컴포넌트에서 값을 업데이트할 때는 이 setState를 이용하게 된다. 이 setState는 모듈 scope에 정의되어 있는 _val을 변경한다.

 

그러면 위에서 만든 MyReact를 이용했을 때 생기는 상황을 생각해보자.

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

Counter가 일종의 컴포넌트라고 생각하고 보자.

MyReact의 render 를 이용해 첫 Counter를 랜더링 한다. 랜더링 하면 MyReact의 useState 가 실행된다. 이때는 _val 에 아무것도 없기 때문에 초기 값으로 들어간 0이 할당된다.

 

그러면 useState 는 그 반환 값으로 0으로, setCount 는 _val 에 접근하는 setter를 돌려줄 것이다.

그 후 Counter는 click 과 render 가 들어있는 객체를 반환해 App에 저장된다. 일단 이 객체 입장에서 count 는 무조건 0이라는 값이다.

 

App.click()  () => setCount(count + 1)을 실행시킨다. 이러면 모듈 scope에 있는 _val 이 1로 변경될 것이다. 근데 setter를 실행시키면 리랜더링이 된다. 이것은 이 MyReact에는 딱히 구현되어있지 않지만 실제 React에서는 setter가 실행이 될 경우 컴포넌트를 리랜더링 한다. 여기서도 그 과정을 보여주기 위해 App.click()  render 를 한 번 더 실행한다.

 

다시 랜더링 하면 마찬가지로 Counter는 const [count, setCount] = MyReact.useState(0)부터 시작한다. 아까랑 같은 결과가 나와야 하지 않나?라고 생각할 수 있지만, 아까와는 다르다. 다시 useState 를 불러와도, 모듈 scope에 정의되어있는 동일한 _val 을 보게 된다. 좀 전에 App.click() 을 하면서 이 _val 값은 1로 변경되었다. 따라서 초기값 0이 아닌 1이 유지된다.

 

이러면 useState 가 반환하는 [count, setCount]의count 값은 1이 된다. 이를 기반으로 다시 만들어지는 새로운 객체 입장에서는 count 값이 1인 상태로 굴러간다고 볼 수 있겠다.

이런 식으로 Closure의 특징을 이용해 컴포넌트에서도 상태가 유지되는 useState 가 돌아간다고 볼 수 있다.

 

그래서?

위에서 확인했듯이 한 번 랜더링 된 컴포넌트가 가지고 있는 상태 값은 그 중간에 변하지 않는다.

[count, setCount]의 이용할 때, 이 setCount 는 메모리 어딘가에 있는 _val 을 변경한 것이지, 지금 옆에 가지고 있는 count 가 변경된 것이 아니다.

 

 count 값이 새로운 값이 되는 건 리랜더링이 된 이후다. (이때 count 는 바로 직전의 count 와는 전혀 관계가 없는 새로운 녀석이다.) 리랜더링 할 때(= Functional Component가 다시 호출될 때) 다시 useState 를 부르면 그때 변경된 _val 값을 가져온다고 생각할 수 있다.

const [state, setState] = useState(0);
useEffect(() => {
  setState(state + 1); // 분명 state에 1을 더했는데?
  console.log(state); // 호출: 0
}, []);

setState를 이용한 직후에 state 값을 불렀는데, 업데이트가 되지 않는 상황이다. state 값이 새로운 값이 되려면 리랜더링이 되어야 하는데, JS는 싱글 스레드로 돌아가기 때문에 useEffectuseEffect에 들어있는 콜백이 마무리된 이후에 리랜더링이 진행될 것이다.

그러면 아직 console.log(state)를 실행하는 시점에는 리랜더링이 되기 전이라는 거니까, state 값은 0인 것이 당연하다.

 

따라서 setter를 사용하고 바로 그 값을 이용하려고 한다면 useEffect 의 deps에 해당 state를 넣어 변경이 되었다는 점이 확인될 때 이용하거나 다른 방법을 찾아야 할 것이다.

Comments