useState의 setter는 왜 비동기적으로 동작할까?

se-een·2023년 4월 26일
4

React 탐구하기

목록 보기
1/7
post-thumbnail

useState에서 setter가 비동기적으로 동작한다는 얘기는 다들 한 번쯤 들어보셨을 것 같습니다.

왜 비동기적으로 동작할까요? 비동기적으로 동작한다면 리액트를 사용하는 입장에서 어느 부분을 주의해야할까요?

두 가지 질문에 대해 정리해보고자 합니다. 이번 글에서는 왜 비동기적으로 동작하는지에 대해서만 정리해보겠습니다.

React 버전에 따른 동작 방식 차이

들어가기 전에 React 버전에 따른 동작 방식 차이에 대해서 짧게 짚고 넘어가겠습니다. 우선 우리가 현재 사용하는 React 18 부터는 기본적으로 상태 업데이트를 일괄 처리한다고 합니다. (+ 공식문서)

하지만 React 17과 그 이전 버전에서는 이벤트 핸들러 내부의 업데이트만 일괄 처리되었습니다. 따라서 이벤트 핸들러 외부에서는 (ex: AJAX 응답 핸들러 등)
상태 업데이트가 즉시 처리되었습니다. 즉, 다음과 같은 코드가 동기적으로 동작했습니다.

(아래의 예시는 setState를 바탕으로 작성되었습니다.)

promise.then(() => {
  // 이벤트 핸들러가 아님. 따라서 즉시 처리 됨. (비효율적인 리렌더링 발생)
  this.setState({a: 1}); // {a: 1, b: undefined }
  this.setState({b: 2}); // {a: 1, b: 2 }
});

따라서 다음과 같이 일괄 처리를 위해서는 React에서 제공하는 API를 사용하여 강제로 일괄 처리 하는 작업이 필요했습니다.

promise.then(() => {
  // Forces batching
  ReactDOM.unstable_batchedUpdates(() => {
    this.setState({a: 1}); // 리렌더링 되지 않음
    this.setState({b: 2}); // 리렌더링 되지 않음
  });
  // {a: 1, b: 2 }. 리렌더링이 단 한 번만 됨.
});

비동기적으로 동작하는 이유

이제 본격적으로 useState의 setter가 비동기적으로 동작하는 이유에 대해서 알아보겠습니다.

다음 내용은 공식 문서에서 언급한 글(gaearon git comment)을 바탕으로 정리하였습니다.

렌더링 효율성

비동기적으로 동작하는 이유 중 하나는 렌더링의 효율성 때문입니다. 간단한 예시를 한 번 들어보겠습니다.

// setter 가 동기적으로 동작할 경우

<button onClick={() => {
    setName('누구게');
    setCurrentName('지금은 누구게');
    setYourName('너는 누군데');
  }}>이름 설정</button>

만약 setter가 동기적으로 동작한다면 위의 버튼을 누를 때 setter가 실행될 때마다 리렌더링을 해줘야하므로 총 세 번의 렌더링이 일어났을 것입니다.

setter를 모두 실행한 뒤 한 번의 렌더링을 해주는 것의 결과와 차이가 없으므로 불필요한 렌더링이 두 번이나 발생한 것이라고 볼 수 있겠습니다.

또한 부모 컴포넌트와 자식 컴포넌트 모두 state를 setter 한다면, 부모 컴포넌트의 state와 자식 컴포넌트의 state가 갱신될 때마다 리렌더링 되므로, 자식 컴포넌트의 리렌더링 효율이 배로 떨어질 것이라고 예상해볼 수 있습니다.

일관성 보장

위에서 언급한 리렌더링 효율성을 개선하기 위해 setter를 실행하더라도 동기적으로 업데이트는 하되 리렌더링이 즉시 되지 않고 일괄 처리한다고 추가로 가정해보겠습니다.

그렇다면 꽤 괜찮지 않아보이나요? 아래 코드는 적상적으로 동작할 것입니다.

// setter 가 동기적으로 동작 + 리렌더링을 일괄처리 할 경우

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

console.log(state) // 0
setState(state + 1);
console.log(state) // 1
setState(state + 1);
console.log(state) // 2
// 한 번만 렌더링

return <div>{state}</div> // 2

하지만 state는 정상적으로 동작 하더라도 prop은 그렇지 않아 React 객체(state, prop, ref)의 일관성이 깨지게 됩니다.

다음 코드를 보겠습니다.

// setter 가 동기적으로 동작 + 리렌더링을 일괄처리 할 경우

function Parent() {
  const [state, setState] = useState(0);
  
  return ( 
    <Child value={state} increase={() => {
      setState(state + 1)}}/>
  );
}

function Child({ value, increase }) {
  const increaseThreeTimes = () => {
  	console.log(value); // 0
    increase();
    console.log(value); // 0
    increase();
    console.log(value); // 0
    increase();
    console.log(value); // 0
  }
  
  return (
    <button onClick={increaseThreeTimes}>
      3번 더하기
    </button>
  );
}

위와 같은 결과가 나오는 이유는, 부모 컴포넌트가 리렌더링이 되지 않으면 prop은 갱신되지 않기 때문에 그렇습니다.

보통의 경우 리액트를 사용하다보면 '상태 끌어올리기' 때문에 state들이 점점 부모 컴포넌트로 가는 경향이 잦습니다.

따라서 부모 컴포넌트의 stateprops로 전달받아 사용하는 상황이 많은데요. 이런 경우에 setter를 동기적으로 동작하면서 일괄 처리를 할 때 문제가 됩니다.

결국 '렌더링 효율성을 포기할 것인가', 'React에서 제공하는 객체(state, prop, ref)의 일관성을 깨드릴 것인가' 둘 중 하나를 선택할 수 밖에 없는 상황이 발생하는 것입니다.

위와 같은 이유에 의해서 useState의 setter는 비동기적으로 동작한다고 합니다. 😀

profile
woowacourse 5th FE

0개의 댓글