본문 바로가기
  • soldonii's devlog
Javascript 공부/React

비동기로 작동하는 setState 이해하기

by soldonii 2019. 12. 11.

*Udemy의 "Complete React Developer in 2020" 강의에서 학습한 내용을 정리한 포스팅입니다.

*자바스크립트와 리액트를 배우는 단계라 오류가 있을 수 있습니다. 틀린 내용은 댓글로 말씀해주시면 수정하겠습니다. 감사합니다. :)


1. 비동기로 작동하는 setState 메소드

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  constructor() {
    super();
    this.state = {
      meaningOfLife: 47
    }
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            {this.state.meaningOfLife}
          </p>
          <button
            
          >
            Update State
          </button>
        </header>
      </div>
    );
  }
}

export default App;

앞선 글에서도 살펴봤지만, 리액트는 state가 변경될 때마다 매번 DOM을 re-rendering하는 것은 아니다. 퍼포먼스 효율성을 위해서 여러 차례 setState가 있을 경우 다른 state의 변경까지 한꺼번에 통합해서 리액트 자신이 판단하기에 가장 적절한 시기에 DOM을 re-rendering한다.

 

따라서 setState의 기본적인 작동 방식은 Asynchronous(비동기적)이다.

 

handleClick = () => {
    this.setState({meaningOfLife: this.state.meaningOfLife + 1});
    console.log(this.state.meaningOfLife);
  }

예를 들어 handleClick이라는 메소드는 setState lifecycle 메소드를 이용해서 meaningOfLife라는 state의 값을 1씩 더하고 있다. 위 코드 실행 이후 바로 console.log를 해도 항상 DOM에 보여지는 것보다 한 박자씩 늦게 숫자가 기록된다. 왜냐하면 setState가 비동기적으로 실행되므로, console.log가 실행되는 시점은 아직 setState에 의해 state가 변경되기 전이기 때문이다. 특히 위 코드에서 주목할 점은, setState의 첫번째 인자가 meaningOfLife: this.state.meaningOfLife + 1인 점이다. 변경할 state의 값을 현재 this.state의 값을 기반으로 설정하였다. 뒤에서 보겠지만 이는 Bad Practice이다.

 

현재는 무척 간단한 코드이므로 작동하지만, 만약 다른 위치, 다른 컴포넌트에서 여러 차례 setState 메소드가 호출된다고 가정해보자. 만약 앱의 다른 부분에서 setState가 실행되어서 현재 meaningOfLife에 담긴 값이 47이 아니라 56으로 변경되었을 수도 있다. 그런데 이를 고려하지 않고 단순히 두번째 인자로 콜백 함수를 넘겨줄 경우 의도와는 다른 실행결과로 인해 버그가 발생할 가능성이 높다.

 

만약 위 handleClick 메소드에서 this.setState의 인자가 현재 this.state의 값을 이용하지 않고, 단순히 고정된 값으로 변경을 원할 경우에는 위처럼 해도 문제가 없다. 즉 예를 들어,

handleClick = () => {
    this.setState({meaningOfLife: 'dolphins');
  }

만약 handleClick 메소드가 이와 같은 형태라면, 변경하고자 하는 state의 값이 현재 state의 값을 참조하지 않고 단순한 문자열이다. 따라서 다른 곳에서 state가 변경되었는지의 여부와 관계가 전혀 없으므로, 이럴 경우에는 위처럼 코드를 작성해도 괜찮다. 하지만 그렇지 않을 경우, 즉 현재의 state 값을 참조해서 state의 값을 변경해야 할 경우, Best Practice는 무엇일까?

 

# 비동기적 setState 활용의 Best Practice

handleClick = () => {
  this.setState((prevState, prevProps) => {
    return {meaningOfLife: prevState.meaningOfLife + 1};
  }, 
	() => console.log(this.state.meaningOfLife));
}

1) setState의 첫번째 인자로 콜백함수를 넘겨주었는데 이 콜백함수는 인자로 prevState와 prevProps를 받는다.(받을 props가 필요없을 경우에는 생략해도 무방)

2) prevState는 현재까지 update된 state의 가장 최신 버전을 담고 있다. 따라서 다른 컴포넌트, 다른 장소에서 state가 변경되었더라도 항상 그 변경된 값까지 포함해서 최신의 상태를 보장한다.

3) 따라서 meaningOfLife의 값을 prevState를 기반으로 다뤄주면 위에서 말한 오류를 없앨 수 있다.

4) 물론 state가 변경되자마자 특정 동작을 실행하기를 원하면, 두번째 인자로 콜백함수를 넘겨줄 수 있다.

 

prevProps가 필요한 경우도 살펴보자.

 

props를 이용하기 위해서 우선 App 컴포넌트에 increment 프로퍼티를 아래처럼 주었다

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App increment={1}/>, document.getElementById('root'));

그리도 handleClick 메소드에 아래처럼 prevProps를 이용해 가장 최신의 props를 이용할 수 있다.

handleClick = () => {
  this.setState((prevState, prevProps) => {
    return {meaningOfLife: prevState.meaningOfLife + prevProps.increment};
  }, 
	() => console.log(this.state.meaningOfLife));
}

 

2. consturctor에 props 전달하기

보통은 class 컴포넌트의 경우 constructor와 super에 인자로 prosp를 전달해주는 것이 Best Practice이다. 그렇게 함으로써 constructor 안에서 this.props를 이용할 수 있기 때문이다.

constructor(props) {
    super(props);
    this.state = {
      meaningOfLife: 47 + this.props.increment;
    }
  }

 

# Alternative Class Syntax

마지막으로, 일반적으로는 불가능하지만 create-react-app 패키지에는 babel 컴파일러가 내장되어 있기 때문에 class 문법을 간략화할 수 있다. state가 복잡하지 않을 경우, 그리고 props 등 인자가 constructor 내부에 필요하지 않은 경우에는 아래처럼 constructor와 super를 생략할 수 있다.

// 기본 코드
class App extends Component {
  constructor() {
    super();
    this.state = {
      meaningOfLife: 47
    }
  }
}

// 아래처럼 간략한 코드로 대체 가능.
class App extends Component {
  state = {
    meaningOfLife: 47
  }
}

댓글