ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] useState의 동작원리
    프로그래밍/React 2023. 12. 16. 11:36

    개요

    • 기본개념
    • useState 동작 원리
    • 헷갈리는 useState
    • 결론

    React에서 많이 사용되는 useState 왜 const로 선언이 되고 어떻게 변화를 시키는지 알아보자!!

     

    기본 개념

    Functional Component에서 상태)

    • 불변성의 특징을 통해 사용하는 값을 예측 가능하게 만들어준다.
    • 함수의 반환 값으로 컴포넌트가 나타나고 컴포넌트가 가진 값, 클로저 등의 정보는 기본적으로 변하지 않는다.
    • Class Component는 this가 바인딩 된 것, 내부 상태값의 변화 등의 문제로 메서드 실행시점의 값을 예측하는 것이 쉽지 않다.
    • 리랜더링이 필요한 시점에서 새롭게 호출을 하여 새로 만들어버린다.

    초기 시점에 불변성을 활용하는 부분에 어려움이 있어 Class Component가 많이 사용되고 복잡하지 않은 경우 Functional Component가 사용되었다.

     

    Hook의 등장)

    React Hook의 등장으로 Functional Component에서 상태관리를 쉽게할 수 있게 되었다.

    const App = () => {
      const [num, setNum] = useState(0);
      return (<div onClick={() => setNum(num + 1)}>{num}</div>);
    };

    useState를 이용한 상태는 어떻게 기억되는 것인가?

     

    간단히 알아보는 Closure)

    Javascript의 Closure에서 주요하게 생각해야하는 부분은 자신이 사용하는 변수를 기억하고 어딘가에 저장해두는 특성이 있다는 것이다. (변수를 Captuer한다.)

     

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

    위 예제를 보면 outer() 안의 outerVar의 경우 outer()의 호출이 끝나면 없어져야하지만 closure()를 호출했을 때 올바르게 출력이 된다(inner에서 outerVar를 가져다 쓴다!)

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

    이러한 Closure의 특성을 기억하자


    useState 동작 원리

    앞서 말한 Closure의 특성을 이용해서 작동한다.

    이러한 개념을 이용해 Functional Component가 실행된 후 outerVar와 같이 살아서 유지되는 변수를 만들어 보자

     

    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는 Functional Component를 랜더링해주는 메서드이다._val는 익명 함수 scope 안에서 정의된 변수로 우리가 원하는 상태를 저장해줄 것이다.Closure의 개념과 같이 _val는 살아서 유지되는 변수가 될 것이다.useState에서 보이듯 setState가 모듈 scope에 정의된_val를 변경하는 setter 함수로 useState를 통해 반환 받는 setter이고 컴포넌트에서 값을 업데이트 할 때 이용 된다.

     

    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 }

    앞서 만든 MyReact를 통해서 React의 useState를 구현하면 이러한 느낌이 된다. (반환값이 JSX는 아니지만 Functional Component라고 생각하고 보자)

    1. MyReact의 render를 이용해 Counter를 렌더링 하고 useState가 실행된다.
    2. useState가 실행될 때 _var는 값이 없으므로 매개변수값이 초기 값으로 할당된다.
    3. 이후 countd와 setter인 setCount가 초기 값을 기반으로 반환 해줄 것이다.
    4. 이후 setter를 실행시키면 리랜더링이 되는데 이는 MyReact에서는 구현되지 않았기에 click이후에 render를 한번 더 호출한 것이다.
    5. 그렇기에 click이후에 render를 다시 실행하게 되면 const [count, setCount] = MyReact.useState(0)가 다시 실행되서 초기값이 다시 0으로 할당된다고 생각 할 수도 있지만 _val가 이미 1로 살아있기때문에 (Closure 특성) _val 값을 기반으로 반환해준다.

    위에서 구현한 MyReact는 여러개의 useState를 사용했을 때에는 감당을 못하는데 이런 부분은 React에서 알아서 했을 것임..ㅎ

    이러한 Hook 호출은 컴포넌트와 어떻게 연관을 짓는 것인가)

    React문서를 보면 알 수 있듯 각 컴포넌트에 대한 정보를 가지오 있는 별도의 메모리 공간이 각각 있다. 여기서 정보를 저장해 놓고 있기에 _val로 만들었던 것을 메모리를 잘 활용해서 구현하고 있는 것이다.

     

    비동기로 작동하는 useState)

    이러한 useState를 사용한 컴포넌트들이 하나의 페이지에 여러 부분에 존재할 것이다. 이때 state 하나하나 바뀔때마다 화면을 리랜더링한다면 성능부분에서 굉장히 안좋을 것이다.

    그렇기에 React에서는 batch처리를 통해서 이를 해결하고 있다.

     

    batch처리는 무엇일까)

    간단하게 setState가 연속으로 호출된다고 하면 이를 한번에 묶어서 한 번만 랜더링하도록 하는 것이다.

    그렇기에 많은 setState들이 연속적으로 사용되었다 해도 batch처리를 통해서 한번에 랜더링으로 최신 상태를 유지하는 것이다. 이러한 batch처리는 useState가 비동기로 작동하는 원인이 된다.

     


    useState에서 발생할 수 있는 문제

    useState를 사용할 때 불변성의 특징을 까먹고 이용한다면 발생하는 문제

    한번 랜더링된 컴포넌트가 가지고 있는 상태값은 중간에 변하지 않는다는 것을 기억하자!

     

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

    위와 같은 코드를 작성했을 때 현재 컴포넌트에서 setState를 통해서 state를 변경했는데도 불구하고 state를 호출했을 때 변경된 state가 출력되지 않는 이유는 이러한 불변성때문이다.

    • 랜더링 이후에 state가 변경되기 때문에 setState()를 사용했다고 바로 변하지 않는다.
    // input에 a를 입력했을 때
    const [memberId, setMemberId] = useState("");
    
    const updateMemberId = e => {
    console.log(e.target.value); // a 출력 
    setMemberId(e.target.value);
    validateMemberId(); // "" 출력
    };
    
    const validateMemberId = () => {
    console.log(memberId); // "" 출력
    if (memberId.length === 5) {
      console.log("5 이상"); // 출력되지 않음
      return 0;
    }
    };
    
    return (
    <>
      <input
        onChange={e => {
          updateMemberId(e);
        }}
      />
    </>
    );

    위 코드에서는 input에 입력을 했을 때 value는 바뀌었지만 state의 값의 경우 batch처리 되기 이전에 랜더링이 되지 않았기 떄문에 두번째 console.log에서 원하는 값을 출력하지 못함. (원하는 값 = a)

     


    결론

    비동기 문제로 설명된 부분도 결국 Closure의 불변성의 개념으로도 접근할 수 있다는 생각이 문득 들었다. 그래서 관련된 자료를 좀 서치를 해보다가 비동기 관련되서 래퍼런스가 더 많아서 좀 애매하다는 생각을 하게 되었다.
    두 문제 설명 방식에서 틀린 것이라고 생각되지 않기에 개념의 접근에 문제라고 생각되고 batch처리하고 closure개념으로 동작하는 것도 맞으니깐 무튼 그렇다 ㅎㅎ
    useState의 개념에 대해서 좀 더 알 수 있었고, 이로 인해서 setState()를 했을 때 같은 렉시컬환경에서 state값을 활용하는 것은 내가 원하는 값이 나오기 힘들겠구나라는 생각을 하게 되었음.

     

     

    참고자료: Article, Blog1, Blog2

    댓글

Designed by Tistory.