setState의 비동기성으로 생기는 문제

2021. 2. 5. 08:46Frontend/React

리액트는 state가 변화할 때마다 매번 render가 작동됩니다.

setState함수는 state의 상태를 변화시키기 때문에 component를 re-render하게 되는데, 종종 setState를 했음에도 

값이 변하지 않는 상황을 마주하곤 합니다.

 

문제

setState를 사용하면 state의 상태변화가 한템포씩 늦는 경우를 종종 보게된다.

 

 

먼저 흔히 겪는 setState의 문제점을 코드로 보겠습니다.

this.state.value = 1;

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

// 당연히 value는 4가 될 것이라 생각하지만,
// 결과는 2이다. 
// this.state.value = 2

 어려운 말로 풀어보자면, 리액트는 여러번 setState를 만나게되면 인자로 전달받은 객체를 하나로 합친 뒤에 업데이트한다. 이는 render가 여러번 발생하지 않고 한번만 일어나도록 하기 위함입니다.

 


하나로 합친 뒤에 업데이트 한다는 뜻이 무엇일까요? 

이제 쉬운말로 풀어보겠습니다.

 

우선 Object.assign() 메소드를 이해하신다면 좋습니다.

const Lim = { age: '10' }
const Kim = { age: '90' }

hisAge = Object.assign({}, Kim);
console.log(hisAge.age);
>>> 90


hisAge = Object.assign({}, Kim, Lim);
console.log(hisAge.age)
>>> 10

두번째 Object.assign()에서 확인할 수 있듯이, 객체가 동일한 키(age)를 가지고 있다면 가장 마지막에 전달된 객체의 키값(age:'10')이 덮어쓰여지는 것을 볼 수 있습니다.

 

이런 과정을 Object Composition이라 부르는데, 바로 setState에서도 이와 같은 일이 벌어집니다. 위에서 객체를 하나로 합친뒤에 업데이트 한다고 했죠? 바로 Kim과 Lim 객체가 하나로 합쳐져 업데이트 되는 상황이 그대로 일어나게 됩니다.

 

즉 아래의 상황에서 가장 마지막에 있는 setState가 이전의 setState들을 덮어쓰게 되는 상황이기에 value는 결과적을 +1만 적용됩니다.

this.state.value = 1;

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

// this.state.value = 2

 

문제의 원인이 이해가 되시나요? 사실 문제라기 보단, 리액트가 setState를 통해 여러번 render하지 않고 한번의 render만 발생시키기 위한 장치로 인해 일어난 상황이었습니다.

 


 

그럼 어떻게 하면 우리가 원하는 결과를 출력할 수 있는지 아래 코드를 보면서 확인해보겠습니다.

handleOnTab(tab) {
	
    let {tabA, tabB, tabC} = this.state;
    
    let aTriple = 'AAA';
    let bTriple = 'BBB';
    let cTriple = 'CCC';
    
    this.setState(() => ({tabA: aTriple})) // 함수형으로 setState실행 => curr_tab은 즉시 'AAA'로 변경된다.
    
    this.setState({tabB: bTriple}, () => {
    	console.log(tabB) 	// BBB 출력
    })
    
    this.setState({tabC: cTriple});
    console.log(tabC) 		// CCC로 업데이트 되기 이전의 tabC 값 출력
}

 

1. 함수형으로 setState 사용 Good Case

this.setState(() => ({tabA: aTriple})) // 함수형으로 setState실행 => curr_tab은 즉시 'AAA'로 변경된다.

첫번째 setState에선 객체가 아닌 함수를 인자로 받았습니다. 함수형 setState가 호출되면 merge할 객체가 없기 때문에 호출된 순서대로 함수를 큐에 넣게 됩니다. 

이런식의 setState는 render가 한번만 일어나게 된다는 장점도 있다.

 

 

2. 콜백을 통한 setState

tabB의 state가 BBB로 업데이트 되면서, 업데이트된 tabB 변수 값을 콘솔에 출력할 수 있습니다

 this.setState({tabB: bTriple}, () => {
    	console.log(tabB) 	// BBB 출력
    })

 

 

3. 비동기성으로 인하여 업데이트 되지 않은 tabC의 값이 출력됩니다.

즉 CCC로 setState되기 이전 단계의 value가 콘솔에 찍힙니다.