React - useState는 동기적 비동기적

Khan·2022년 8월 9일
0

TIL

목록 보기
10/19
post-thumbnail

들어가기 전에

useState 란

  • useState는 컴포넌트에서 state값을 추가할때 사용된다. 함수형 컴포넌트에서는 클래스형 컴포넌트처럼 state를 사용할 수 없어, Hook을 사용해서 state와 같은 기능을 할 수 있도록 만들어주었다.

state와 Hook

  • state : state는 그냥 변수이다. 하지만 일반적인 const, let과 다르게 값이 변하면 렌더링이 일어난다. 즉, 값이 변하게 되면 연관있는 컴포넌트들이 다시 렌더링이 되어 화면이 바뀌게 된다.

  • Hook : Hook은 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 “연동(hook into)“할 수 있게 해주는 함수이다.

동기적? 비동기적?

useState는 비동기로 동작한다.
(정확하게 얘기하면 setState가 비동기로 동작한다.)

const Counter = () => {
  const [count, setCount] = useState(0);
  
  const plusNum = () => {
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)    
  }
  
  const minusCount = () => setCount((prev) => prev - 1);
  const resrtCount = () => setCount(0)
  
  return (
    <div>
     <div>{countValue}</div>
    
      <button onClick={plusCount}>+</button>
      <button onClick={minusCount}>-</button>
      <button onClick={resetCountValue}>reset</button>
    </div>
  );
}

실행하기 전에 예측을 해보면 버튼을 클릭할 때마다 setCount(count + 1)을 세 번 해줌으로써 count를 3씩 증가시킬것 같다.

하지만 결과를 보면 버튼을 클릭할 때마다 3이 아니라 1씩 증가하는 것을 확인할 수 있다.

이유

하나의 페이지나 컴포넌트 내에도 수많은 상태값이 존재한다. 만약 이 상태 하나하나가 바뀔 때마다 화면을 리렌더링 한다면 문제가 생길수도 있다.

때문에 리액트는 성능의 향상을 위해서 setState를 연속 호출하면 배칭 처리하여 한 번에 렌더링하도록 하였다. 아무리 많은 setState가 연속적으로 사용되었어도 배칭 처리에 의해서 한 번의 렌더링으로 최신 상태를 유지하는 것이다.

배칭이란?

배칭은 React가 더 나은 성능을 위해 여러 개의 state 업데이트를 하나의 리렌더링 (re-render)로 묶는 것을 의미한다.

예를들어, 하나의 클릭 이벤트 안에 두 개의 state 업데이트를 가지고 있다면, React는 언제나 이 작업을 배칭하여 하나의 리렌더링으로 만들었다. 다음과 같은 코드를 실행해보면, 매 번 누를 때마다, state를 두 번 변경하였지만, React가 단 한 번의 렌더링만 수행한 것을 볼 수 있다.

레스토랑 웨이터에 비유를 하면 더 쉽게 와닿을 수 있는데, 주문을 할 때 하나 고를 때마다 주방으로 달려가지 않고, 오더를 완성시킬 때까지 대기하는 것과 같다

const App = () => {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    setCount((c) => c + 1); // 아직 리렌더링 하지 않는다
    setFlag((f) => !f); // 아직 리렌더링 하지 않는다
    // React는 이 함수가 끝나면 리렌더링을 한다 (이것이 배칭이다!)
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style=>{count}</h1>
    </div>
  );
}

React는 언제 리렌더링할까?

  1. state 변경이 있을 때
  2. 부모 컴포넌트가 렌더링 될 때
  3. 새로운 props이 들어올 때
  4. shouldComponentUpdate에서 true가 반환될 때
  5. forceUpdate가 실행될 때

1번과 2번의 경우 얕은 비교를 통해 새로운 값인지 아닌지 판단 → 객체, 배열, 함수와 같은 참조타입은 실제 내부 값이 아닌 동일참조여부에 따라 판단한다.

⇒ state에 push, pop 등등 원본을 변형하는 메소드를 사용하면 안되는 이유(state는 immutable하기 사용하자)

따라서, React는 객체, 배열, 함수일 경우 컴포넌트가 다시 호출되면서 새로운 props가 생성될 때 새로운 참조타입으로 형성되기 때문에 새로운 값이라고 판단한다.

예제

아래의 예제를 보면 이 컴포넌트는 3초 뒤에 count 값과 함께 얼럿(alert)을 띄워준다.

function Counter() {
  const [count, setCount] = useState(0);
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  );
}

단계별로 이런 과정을 실행해보면

  • 카운터를 3으로 증가시킨다
  • “Show alert” 을 누른다
  • 타임아웃이 실행되기 전에 카운터를 5로 증가시킨다

이럴 때 얼럿에는 어떤 값이 나타날까요? 얼럿이 등장할 때의 값인 5가 나올까요? 아니면 제가 “Show alert” 버튼을 클릭할 당시의 값인 3이 나올까요?

  • 결과 : 3

이유

함수형 컴포넌트는 render 될 때의 값들을 유지한다.

  • handleAlertClick는 클릭핸들러가 호출됐을 때의 state(count)를 고정시켜둔다. 때문에 내가 ”Show alert”를 눌렀을 당시의 count 값을 간직할 수 있게된다.
// 처음 랜더링 시
function Counter() {
   const count = 0; // useState() 로부터 리턴
  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + 0);
    }, 3000);
  }
  // ...
  <button onClick={handleAlertClick} /> // 0이 안에 들어있음
  // ...
}
// 클릭하면 함수가 다시 호출된다
function Counter() {
   const count = 1; // useState() 로부터 리턴
  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + 1);
    }, 3000);
  }
  // ...
  <button onClick={handleAlertClick} /> // 1이 안에 들어있음
  // ...
}
// 또 한번 클릭하면, 다시 함수가 호출된다
function Counter() {
   const count = 2; // useState() 로부터 리턴
  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + 2);
    }, 3000);
  }
  // ...
  <button onClick={handleAlertClick} /> // 2가 안에 들어있음
  // ...
}

그래서 이 코드에서 이벤트 핸들러가 특정 랜더링에 “속해 있으며”, 얼럿 표시 버튼을 클릭할 때 그 랜더링 시점의 counter state를 유지한 채로 사용하는 것이다.

특정 랜더링 시 그 내부에서 props와 state는 영원히 같은 상태로 유지됩니다. props와 state가 랜더링으로부터 분리되어 있다면, 이를 사용하는 어떠한 값(이벤트 핸들러를 포함하여)도 분리되어 있는 것입니다. 이들도 마찬가지로 특정 랜더링에 “속해 있습니다”. 따라서 이벤트 핸들러 내부의 비동기 함수라 할지라도 같은 count 값을 “보게” 될 것입니다.

결론

  • useState는 비동기적으로 동작하는 훅이다.
  • 비동기적으로 동작하는 이유는 성능 최적화 때문이다.
  • 리액트는 성능을 최적화하기 위해 setState를 배칭 처리한다.
  • 우리의 함수는 여러번 호출되지만(랜더링마다 한 번씩), 각각의 랜더링에서 함수 안의 count 값은 상수이자 독립적인 값(특정 랜더링 시의 상태)으로 존재한다.
  • 특정 랜더링 시 그 내부에서 props와 state는 영원히 같은 상태로 유지된다.

출처

A Complete Guide to useEffect
함수형 컴포넌트와 클래스, 어떤 차이가 존재할까?

0개의 댓글