React - setState가 비동기인 이유

Jiwoo Joy Kim (zuzokim)·2021년 8월 26일
7

React

목록 보기
3/3

Q. 리액트에서 상태값이 100번 바뀌면 100번 렌더링이 되는가?

아.. 이 질문에 자신있게 답하기 위해서는 리액트 내부 동작원리와 상태값 변경, 렌더의 원리를 이해하고 있어야 할 것이다.

일단, 처음 이 질문에 나는 상태값을 어떻게 바꾸느냐에 따라, 그리고 어떤 상황이냐에 따라 다르지 않겠느냐는 답을 얼렁뚱땅 했었다. 대충 맞는 말이긴 한데, 솔직히 말해서 확신이 없었다. 왜냐면 프로젝트를 하면서 무수한 콘솔로그로 렌더가 여러번 되는 상황도 보았고, 또 상태를 직접 변경을 하는 경우엔 리액트가 감지하지 못해서 렌더가 되지 않는다는 것을 알고 있었기 때문이다.

이제부터 제대로 알아보자.

상태값 변경을 위해서는 setState를 이용한다.

클래스 컴포넌트에서는 this.setState를 사용하고, 함수형 컴포넌트에서는 useState를 이용해 직접 정의해서 setter 메소드를 통한 상태값 변경이 가능하다.

직접 상태값을 변경하지 말아야하는 이유는, 위에서 말했듯, 이전 상태값을 바탕으로 업데이트된 상태값을 비교해 리액트가 감지하기 때문이다.

비동기로 작동하는 setState

setStae가 비동기로 작동한다는게 위의 Q.질문에 답할 수 있는 가장 중요한 개념이 된다.

  • 이벤트핸들러에서 두 번의 setState를 실행시키는 코드를 보자.
class AsyncSetState extends Component{
	state = {count: 0};

	handleClick = () => {
		this.setState({count: this.state.count + 1}, console.log('state changed!'));
  		this.setState({count: this.state.count + 1}, console.log('state changed!'));
	}
    
    render(){
      	console.log('render!');
    	return(
          <>
            <div>현재값 {this.state.count}</div>
            <button onClick={this.handleClick}>count+</button>
          </>
        )
    }

}
  • 이 코드를 실행시키면 아래와 같이 콘솔이 찍힌다.

state changed!
state changed!
render!

  • 이번엔 함수형 컴포넌트로 작성된 다른 코드를 보자.
function AsyncSetState() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    console.log(count);
  };

  return (
    <>
     <div>현재 값 {count}</div>
     <button onClick={handleClick}>count+</button>
    </>
  );
}
  • 이번에는 state 값 반영은 되지만, 콘솔로 찍힌 값은 이전의 상태값을 출력하는 것을 볼 수 있다.

왜?????

리액트 내부적으로 setState를 비동기로 처리하기 때문인데, 하나의 어플리케이션, 하나의 페이지나 컴포넌트에도 수많은 상태값이 있을텐데, 상태가 각각 바뀔 때마다 화면을 리랜더링 해야한다면 문제가 생길 것이다.

그래서 리액트는 이런 문제를 효율적으로 해결하기 위해 setState를 연속 호출하면 setState를 모두 취합(batch)한 후에 한 번에 렌더링하도록 한다. 그러면 100번의 상태값 변화를 한 번의 렌더링으로 최신의 상태를 갱신할 수 있다.

정확히는 16ms 단위로 batch update를 진행하고, 그사이 변경된 상태값을 모아서(merge) 이전의 엘리먼트 트리와 변경된 state가 적용된 엘리먼트 트리를 비교하는 작업(reconciliation)을 거쳐 최종적으로 변경된 부분만 DOM에 적용시킨다.

state는 결국 객체이기 때문에 ...

merge 단계가 어떻게 이루어지는지는 Object.assign()으로 객체를 합칠 때는 생각하면 이해할 수 있다. state도 결국 객체이기 때문에, 같은 키값을 가진 경우라면 가장 마지막 실행값으로 덮어씌워진다.

만약 setState 연속 호출을 모두 적용시키고 싶다면?

setState의 인자로 함수를 넣어주면 된다.

setState가 비동기적으로 작동하지만, 렌더링 전에 모두 batch되는 것이 보장되고, 실행 순서대로 처리됨이 보장된다. 인자로 넘겨주는 함수는 큐에 저장이 되어 순서대로 실행이 되기 때문이다.

  const handleClick = () => {
    setState((prevState) => prevState + 1);
    setState((prevState) => prevState + 1);
    setState((prevState) => prevState + 1);
  };

이런 식으로.. 그러면 실행될때마다 +3씩 증가되는 것을 볼 수 있다.
이렇게 넣어준 함수의 인자로 이전 state객체가 전달이 되고, 이는 가장 최신값임이 보장된다. 큐에 저장된 함수가 순서대로 실행되면서 반환하는 값이 다음 함수의 prevState로 들어가는 방식이기 때문.

결국

리액트의 setState가 비동기로 작동하는 이유는 렌더링 횟수를 줄여 더 빠르게 동작하게 하기 위함인 것을 알 수 있다.

profile
- I make something! ✍🏽👩🏻‍💻🎬🎨💖🪑🔨🔜

0개의 댓글