[React][공식문서] setState()에 대해 알아야 할 세 가지

Gyuwon Lee·2022년 6월 2일
0
post-thumbnail

📌 [React] 공식문서 풀어쓰기: 5-1. state와 생명주기 에서 이어지는 글입니다.

React 공식 튜토리얼을 바탕으로, 필요한 개념을 보충하여 학습한 기록입니다.

1. State를 올바르게 사용하자!

리액트의 state는 특히나 불변성과 밀접하게 관련되어 있는 개념으로, 업데이트 방식과 업데이트 이후의 형태 등을 꼼꼼하게 알아 두어야 나중에 점차 데이터의 형태가 복잡해지더라도 state를 혼란 없이 사용할 수 있다.

1) "직접 State를 수정하지 마세요": immutability

간단히 말해, setState() 이외의 방식으로 state를 수정하는 것은 페이지를 렌더링시키지 못한다.

// Wrong
this.state.comment = 'Hello';

위처럼 state의 속성에 직접 접근해 값을 바꾸는 것은 아주아주 지양될 뿐만 아니라, 변경사항이 화면에 재렌더링되지 않는다. 즉, state값을 직접 변경해도 render() 함수는 새로 호출되지 않는다.

// Correct
this.setState({comment: 'Hello'});

setState() 를 호출했을 때에만 렌더링 함수가 호출(trigger)되어 화면에 변경사항을 반영할 수 있다. 이 때는 리액트 엔진이 자동으로 render() 함수를 호출하므로 화면에 변경된 state값을 새롭게 출력할 수 있게 된다.

이와 같이, state값을 직접 변경하면 안되는 이유는 render() 함수로 화면을 그려주는 시점을 리액트 엔진이 정하기 때문이다. 이는 setState() 함수가 리액트 컴포넌트의 생명주기와 깊이 연관되어있기 때문인데, 실제로 리액트 엔진은 setState() 함수로 state값을 변경하면 몇 단계의 검증 과정을 거쳐 render() 함수를 호출한다고 한다.


2) "State 업데이트는 비동기적일 수도 있습니다": async

리액트는 성능을 위해 여러 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있다.

자바스크립트는 어떤 함수들에 대해 비동기적(asynchronous) 처리를 지원한다. ajax, 이벤트 리스너, setTimeout 같은 함수들을 쓸 때 그런 현상이 일어날 수 있는데, 대부분 "처리 시간이 오래 걸린다"는 가능성을 갖고 있는 함수들이다.

문제는, 리액트의 state 변경함수들이 전부 비동기적으로 처리된다는 점이다. 여기 setState() 도 포함된다. 그래서 setState() 실행이 오래걸리면 이 코드의 실행을 잠시 보류하고 다른 밑에 있는 코드들부터 실행될 수도 있다. 이는 예상치 못한 문제를 유발하기도 한다.

해결책 (1) 함수를 인자로 사용하는 다른 형태의 setState()를 사용 (클래스형 컴포넌트)

// assuming this.state = { value: 0 };
this.setState({ value: this.state.value + 1});
this.setState({ value: this.state.value + 1});
this.setState({ value: this.state.value + 1});

즉 앞서 한 얘기가, 위의 코드가 모두 실행되고 나서도 this.state.value === 1 인 상태일 수 있다는 뜻이다. 이전 줄의 실행결과를 기다리지 않고 바로 그 아래 줄을 실행하기 때문에, 마지막 this.setState 에서도 this.state.value 가 여전히 0 인채로 연산되어버릴 수 있다.

그래서 리액트 공식문서에서는 객체보다는 함수를 인자로 사용하는 다른 형태의 setState() 를 사용하라고 한다.

// assuming this.state = { value: 0 };
this.setState((state) => ({ value: state.value + 1}));
this.setState((state) => ({ value: state.value + 1}));
this.setState((state) => ({ value: state.value + 1}));

바로 위의 코드에서 setState 의 인자 부분만 함수로 바꾸었다. 이 함수는 우리가 기대했던 대로 이전 시점의 state를 인자로 받아들여, 각 줄을 순차적으로 연산할 것이다.

참고한 글에 따르면, "만약 setState 의 첫 번째 인자로 함수가 넘겨질 경우 리액트는 setState 를 호출하고 이 때의 state는 호출 시점에서의 현재 state (at-call-time-current state) 다. 여기에 사용된 함수는 그 state에 병합할 객체를 반환하고 있어야 한다."

간략히 이해하자면 인자로 함수를 넘겨받은 setState 는 호출 시점에서의 최신 state를 사용하려 하므로 이전 줄들을 모두 실행한 결과를 기다린 뒤 최신 state를 가져오는 것이 아닐까 싶다.

해결책 (2) useEffect 사용 (함수형 컴포넌트)

useEffect() 함수는 리액트 컴포넌트가 렌더링될 때마다 특정 작업을 실행할 수 있도록 해 주는 Hook이다. 앞선 글에서 생명주기 메소드는 클래스형 컴포넌트의 특징이라고 했는데, 이 함수를 잘 사용하면 함수형 컴포넌트 역시 컴포넌트의 마운트, 업데이트, 언마운트 단계별로 특정 작업을 처리할 수 있게 된다.

useEffect(function, deps)
  • function: 수행하고자 하는 작업
  • deps: 배열 형태로, 검사하고자 하는 특정 값 또는 빈 배열이 들어올 수 있다.

3) "State 업데이트는 병합됩니다"

setState() 를 호출할 때 리액트는 제공한 객체를 현재 state로 병합한다. 여기서 병합이란 업데이트된 변수만을 뜻한다.

  constructor(props) {
    super(props);
    this.state = {
      posts: [],
      comments: []
    };
  }

예를 들어, 위처럼 생긴 state를 갖는 컴포넌트가 있다고 하자. 그러면 setState() 호출 때마다 모든 속성의 값을 다시 명시해서 반환해야 하는 것이 아니라, 별도의 setState() 호출로 각 속성을 독립적으로 업데이트할 수 있다.

  componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts
      });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments
      });
    });
  }

이는 얕은 병합으로, 위 코드를 보면 this.setState({comments})this.state.posts 에 영향을 주진 않지만 this.state.comments 는 완전히 대체된다.



참고:

profile
하루가 모여 역사가 된다

0개의 댓글