React

비동기적으로 작동하는 useState를 알아보자

승큐니 2023. 12. 18. 12:30

자바스크립트는 일반적으로 코드를 작성하면 동기적(Synchronous)으로 작동한다.

하지만 useState의 setter 함수인 setState는 비동기적(Asynchronous)으로 작동한다. 다시 말해 setState 함수의 실행이 완료될 때까지 기다려주지 않고 곧바로 다음 코드가 실핸된다는 것이다.

리액트에서 setState로 state를 관리하다보면 비동기적으로 작동해 내가 의도한대로 값을 갖고 있지 않을 때가 있다.

setState가 의도대로 동작하지 않는 이유

자바스크립트는 동기적(Synchronous)으로 동작한다. 하지만 때때로 동기/비동기에 대한 이해가 부족하거나 라이브러리에서 제공하는 함수들에 대한 이해 없이 사용하게 되면 개발자는 의도대로 동작하지 않는 코드에 당황할 수 밖에 없다.(그게 나…)

특히 setState가 동기적으로 동작하지 않아 문제를 겪은 적이 몇 번 있었는데, 그 이유에 대해 알아보면 setState 함수가 비동기적으로 동작하고 React가 하나의 이벤트 핸들러 함수 내의 로직을 모두 읽을 때까지 기다린 다음에 일괄적으로 처리(Batch)해서 한번에 렌더링 하기 때문이다.

예제를 통해 생각해보면

아래 예제에서 개발자는 버튼을 클릭 했을 때 age가 3 증가함을 기대하고 코드를 구현했을 것이다.

function App() {
	const [age, setAge] = useState(0);
	function ageHandler() {
		setAge(age + 1); // age = 0 (+ 1)
		setAge(age + 1); // age = 0 (+ 1)
		setAge(age + 1); // age = 0 (+ 1)
		console.log(`age : ${age}`); // age = 0
	}
return (
	<div className="App">
		<div>제 나이는 {age} 입니다.</div>
		<button onClick={ageHandler}>up</button>
	</div>
	);
}


하지만 의도와는 다르게 age는 1만 증가했고, 심지어 콘솔은 age가 0 이라고 출력하는 것을 볼 수있다. 이러한 이유는 앞서 이야기 한 것 처럼, 이벤트 핸들러 함수인 ageHandler 내부에서 age는 아직 state가 변경 및 렌더링되기 전이기 때문에 항상 0이 되고 age 값이 0이기 때문에 마지막 setAge 함수만 실행되는 것 처럼 동작하는 것이다.

setState가 비동기적으로 동작하게 하는 이유/장점

간단히 정리하면 setState가 비동기적으로 동작하는 이유이자 비동기적인 동작의 장점은 리렌더링을 효율적으로 수행하기 위함이다.

정확한 수치로 보면 리액트는 16ms 단위로 Batch update를 진행하고, 그 사이 변경된 상태값을 모아서 이전 엘리먼트 트리와 변경된 state가 적용된 엘리먼트 트리를 비교하는 작업을 통해 최종적으로 변경된 부분만 DOM에 적용시킴으로써 리렌더링을 수행한다.

개발자가 setState를 의도대로 동작하게 하려면?

setState의 인자로 함수를 넣어주는 방법

setState의 인자로 함수를 넣어 줌으로써 개발자의 의도대로 동작하게 할 수 있다. setState가 비동기적으로 동작하긴 하지만, 인자로 함수를 넣음으로써 렌더링 전에 모두 batch 되는 것을 보장하고 실행 순서대로 처리됨이 보장된다. 이럴 수 있는 이유는 인자로 넘겨주는 함수는 큐에 저장돼 순서대로 실행돼야하기 때문이다.

function ageHandler() {
	setAge((age) => age + 1);
	setAge((age) => age + 1);
	setAge((age) => age + 1);
}

useEffect를 활용

useEffect의 의존성 배열에 age를 넣어 age state가 변화할 때 리렌더링이 됨을 보장받을 수 있다.

function ageHandler() {
	setAge(age + 1);
	setAge(age + 1);
	setAge(age + 1);
}
  
useEffect(() => {
	// age의 state가 변경될 때 리렌더링 된다.
	console.log(age);
}, [age]);