본문 바로가기
Framework/React

리액트에서 setState는 비동기로 동작 ??

by cariño 2022. 10. 3.
728x90
반응형

setState는 비동기로 동작하는 것을 간과하지 못한 채 코드를 짜게 되면서 깊은 늪에 빠지게 됐다.

이 실수는 초보자라면 당연히 맞닿을 수 있는 일인데 해결 방법을 누가 딱딱 알려주면 얼마나 좋을까!!!

하여튼.. 깊게 공부하지 않았던 나의 미스텤...

이러한 실수를 범하고 있는 꽤 많은(?) 아니 초급자.. 분들께 좋은 피드가 됐으면 좋겠다...

 


🌻state

  • state는 상태 변화값을 보여준다.
  • 부모 컴포넌트에서 자식 컴포넌트로 데이터를 내보내는것이 아닌 해당 컴포넌트 내부에서 데이터를 전달한다.
  • 함수 내부에서 선언되어 사용되는 변수 처럼 컴포넌트 내부에서 사용이 가능하다.
  • state는 변경 가능한 자바스크립트 객체이며 state의 상태가 변하면 re-randering된다.
    ex) 검색 창에 글을 입력할 때 글이 변하게하는 것은 state를 바꿈

함수 컴포넌트 state

  • 함수 컴포넌트의 state는 useState 리액트 Hooks를 사용한다.

class 컴포넌트 state

  1. 초기화 작업
  2. constructor와 super에 props를 전달해주면 생성자 안에서 this.props를 이용할 수 있다.

 

//생성자사용
export class CounterClass extends Component {
constructor(props) {
    super(props) //필수 작성
    this.state = {count: 0}
}
  • state가 복잡하지 않을 경우나 props등의 constructor 내부에 필요하지 않은 경우는 아래처럼 constructor와 props를 생략할 수 있다.

 

//멤버변수
class CounterClass extends React.Component {
  state = {count: 0}
  1. state값 변경
  2. state의 초기화를 위해서 constructor 메서드 내부에서 바로 setState는 사용할 수 없다.
  3. construcotr 메서드 내부의 초기화는 멤버변수방식 혹은 this.state를 사용한다.
    state의 값을 변경하려면 setState를 통해 변경한다.
  4. state의 변화는 setState함수에 의해서만 변경시킬수 있다.

 


🌹비동기로 작동하는 setState 메소드

  • count 동작 함수를 만들었는데 화면에 표시가 안되거나 여러번 눌러야 동작이 된다구요? 아래 글을 확인해보세요.

 

[setState 를 사용하지 않았을 때]

import React, { Component } from 'react'
class CounterClass extends Component {

  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }

  increase() {
    // setState 를 사용하지 않았을 때
    this.state.count = this.state.count + 1
    console.log('this.state.count: ', this.state.count);

  }

  increaseSync() {
    // setState의 이전 결과를 받기위해선 callback function을 사용해야 함
    this.setState(
      (prevState, props) => { return { count: prevState.count + 1 } },
      () => { console.log('Callback value :: ', this.state.count) }
    )
  }


  sayMyName() {
    console.log(`good ${this.memberName}`)
  }

  render() {
    return (
      <div>
        <div>count: {this.state.count}</div>
        <button onClick={() => this.increase()}>increase</button>
        <button onClick={() => this.increaseSync()}>increaseSync</button>
      </div>
    )
  }
}

export default CounterClass;

increase 버튼을 눌렀을 때 카운팅 되는 화면을 만들었다.


위의 코드 실행 결과 console.log에는 count가 변화된 게 뜨지만, 브라우저에서는 count 값에 변화가 없다. 왜 그런 걸까?

state를 변경하는 setState가 비동기로 동작하기 때문이다.
increase 버튼을 눌러도 동작에 변화가 없는 것은 setState에 의해서 state가 변경되기 전을 의미한다.

여기서 중요한 말이 있는데,


setState 함수는 메인 프로세스 중에 호출된 모든 setState가 모두 처리되기 전에 컴포넌트가 렌더링 되지 않는다.

그렇다면 왜 리액트에서 setState를 비동기로 처리하는 것일까?

이유는 효율성 때문이다.
상태 값이 바뀌면 렌더링이 다시 되는 리액트의 특성상 위의 코드처럼 여러 개가 사용될 때는 setState가 사용된 개수만큼 렌더링이 되어야 한다.
만약 만일 setState가 여러 개 있다면 그 여러 번의 수만큼 렌더링이 이루어지게 된다. 이것은 성능 저하와 효율적인 운영이 되지 않는다.

 

즉 리액트는 객체의 값이 변할 때 해당 컴포넌트의 setState를 모두 취합한 후 한 번에 렌더링하도록 한다. 이러면 한 번의 렌더링으로 여러 개의 state가 한 번에 갱신될 수 있다.

setState는 이벤트 핸들러 안에서 현재 state의 값에 대한 변화를 요청하기만 하는 것이고, 그 요청사항은 이벤트 핸들러가 종료되고 react에 의해서 효율적으로 상태가 갱신된다.

 


 

그렇다면 state를 바꾸는 해결 방법은 무엇일까?
참고: https://youtu.be/KxHOHg5raQ4

[비동기를 간과했던 동작 예문]

//EX1)
export default function ChangeDiff() {
    let [name, setName] = useState('mike')

    let clickHandle = () => {
        setName('JANE')
        alert(name)
    }
    return (
        <div>
            <button onClick={() => clickHandle()}>{name}</button></button>
        </div>
    )
}

예상 결과 : 클릭 시 버튼명에 JANE -> alert에도 JANE 나올 줄 앎
정답 : 클릭 시 alert창에 Mike 먼저 출력 후 -> 버튼 명 JANE으로 변경 됨
//EX2)
export default function ChangeDiff() {
    let [count, setCount] = useState(0)

    let clickHandle = () => {
        setCount(count + 1)
        setCount(count + 1)
        setCount(count + 1)
        setCount(count + 1)
        setCount(count + 1)
    }

    return (
        <div>
            {count}
            <button onClick={clickHandle}>+5</button>
        </div>
    )
}
예상 결과 : 클릭 시 count가 +5씩 증감
정답 : 클릭 시 count가 +1씩 증가 됨

setState를 실행 한 직후에 state에 접근해도 아직 변경되기 전 값으로 얻게된다.
연속적으로 출력해도 결국은 한번만 실행된다.

 


🍀[드디어 해결 방법]

1. updater 함수를 이용해서 setState 이용하기

state를 인자로 받는데, 이 값은 항상 최신상태를 보장받는다.
반환 값으로 최신상태의 값에 + 변화 값을 적용해준다.

setState() 메서드에 전달할 인수를 상태값이 아닌 함수의 형태로 전달을 한다.
이렇게 전달 된 함수의 인자로 현재 컴포넌트 상태값이 통체로 전달된다.

export default function ChangeDiff() {
    let [count, setCount] = useState(0)

    let clickHandle = () => {
        setCount(prevState => prevState + 1)
        setCount(prevState => prevState + 1)
        setCount(prevState => prevState + 1)
        setCount(prevState => prevState + 1)
        setCount(prevState => prevState + 1)
    }

    return (
        <div>
            {count}
            <button onClick={clickHandle}>+5</button>
        </div>
    )
}

 

2. state는 불변으로 관리하자

  • 리액트는 이전상태와 이후 상태값을 비교해서 다른경우에만 업데이트를 실행한다.
export default function ChangeDiff() {
    const Mike = {
        name: 'Mike',
        age: 30
    }
    let [user, setUser] = useState(Mike)

    function clickHandle() {
        setUser(prevState => {
            Mike.age = 31
            console.log(prevState === Mike)
            return Mike
        })
    }

    return (
        <div>
            <p>{user.name} ({user.age})</p>
            <br />
            <button onClick={() => clickHandle}>변경</button>
        </div>
    )
}

//console.log에는 true만 여러번 찍히게 된다.

 

이 전 update함수를 사용해서 객체 property를 확인해보면
value가 바뀌어도 안 바뀌었다고 판단되서 아무 작업도 일어나지 않는다.

최신값을 가지고 오지만 해당 객체들이 바라보는 주소값을 생각해야한다.

그렇다면 방법은 새로운 객체를 만들어줘야 한다.

export default function ChangeDiff() {
    const Mike = {
        name: 'Mike',
        age: 30
    }
    let [user, setUser] = useState(Mike)

    function clickHandle() {
        const newMike = { ...Mike, age: 31 }
        setUser(newMike)
    }

    return (
        <div>
            <p>{user.name} ({user.age})</p>
            <br />
            <button onClick={() => clickHandle}>변경</button>
        </div>
    )
}

Object.assign() 메서드나 restParameter를 통해서 객체를 복사한 후 할당시킨다.

(얕은복사 깊은복사를 통한 react렌더링에 대한 글은 블로그 목록에서 확인!)

 


💐[결론적으로..]

setState는 모두 비동기로 처리되지만 렌더링 전에 모두 묶음으로 처리됨이 보장되고 처리 순서도 실행 순서대로 처리됨이 보장된다.

 

기본적으로 자바스크립트는 싱글 스레드 프로세스(콜 스택이 한개뿐)이므로 동기적으로 작업을 진행한다.
함수코드가 평가되면 실행 컨텍스트가 생성되고 콜 스택에 쌓이게 된다.
단 하나의 콜 스택을 사용하기 때문에 최상위 실행 컨텍스트가 종료되어야만 다음 실행 컨텍스트를 실행 할 수 있다. 콜 스택에 제거되기 전까지는 어떤 태스크도 실행되지 않는다.

 

잠시 짚고 넘어가는 브라우저 동작 원리 (Stack, Queue, event loop)

📮태스크 큐(task queue)

  • 비동기 함수의 콜백 함수 또는 이벤트 핸들러가 일시적으로 보관되는 영역이다.
  • 보통 이벤트리스너, ajax, setTimeout 등

 

📮이벤트 루프(evnet loop)

  • 현재 실행중인 실행 컨텍스트가 있는지, 대기중인 함수가 있는지 반복해서 확인한다.
    콜 스택이 비어있고 태스크 큐에 대기 중인 함수가 있다면 이벤트 루프는 순차적으로 태스크 큐에 대기 중인 함수를 콜 스택으로 이동시킨다.

 

태스크 큐 보관 함수 -> 이벤트 루프가 스택이 비어있는지 확인 후 -> 콜 스택으로 이동

비동기로 처리되는 setState는 큐(queue)에 넣어져서 순서대로 처리된다.

728x90

'Framework > React' 카테고리의 다른 글

React Context API  (0) 2022.10.12
conditional rendering  (0) 2022.10.11
React TodoList 만들기  (0) 2022.10.03
useEffect()  (1) 2022.09.29
class 컴포넌트에서 function 컴포넌트로  (0) 2022.09.26

댓글