- 프론트 작업을 하면서 정말 나를 끈질기게 괴롭히고 또 이해하기 힘든 제일 큰 문제가 하나 있었다.
- 바로 useState의 비동기 문제이다.
- 구글링을 아무리 하고 구현을 아무리해도 안되고, 또 어쩔때는 됐었다.
- 그나마 useEffect를 사용해서 매번 해결은 했었지만 불완전한 이해를 바탕으로 한 사상누각이었고 무엇보다 나 자신이 확신이 없었다.
- 모든게 완전하게 이해를 못했어서 발생한 일이었다.
- 심지어 어떨때 비동기문제가 발생하고 어떨때 안 발생하는지도 몰랐으니
- 그야말로 슈뢰딩거의 비동기였다..
- 그래도 이제는 useState의 비동기 문제를 조금은 이해하고 해결할 수 있게 되어 정리를 해본다.
문제 상황
import { useState } from "react";
export default function StateTest() {
const [memberId, setMemberId] = useState("");
const updateMemberId = e => {
console.log(e.target.value);
// (1)
setMemberId(e.target.value);
// (2)
validateMemberId();
};
const validateMemberId = () => {
console.log(memberId);
if (memberId.length === 5) {
console.log("5 이상");
return 0;
}
};
return (
<>
<input
onChange={e => {
updateMemberId(e);
}}
/>
</>
);
}
- 위와 같은 컴포넌트가 있을 때 우리는 자연스럽게 input에 ‘a’ 라는 값을 입력할 경우 그 값이 validateMemberId 를 통해 console로 찍히게 될 것을 예상한다.
- 하지만 그 결과는 우리의 예상과 다르다.
// 첫번째 console.log
>> a
// 두번째 console.log
>>
- e.target.value값은 그대로 잘 나왔지만 그 값을 useState를 이용해 다른 함수에서 쓰려고 하니 값이 제대로 나오지 않았다..
- 왜 이런 결과가 나온걸까?
useState가 비동기로 작동하는 이유
- 하나의 페이지나 컴포넌트에는 수많은 state들이 존재한다.
- 이때 이 state들이 하나하나 바뀔때마다 화면을 리렌더링 한다면 성능 저하가 발생할 것이다.
- 리액트에서는 이 문제를 batch처리를 통해 해결한다.
- setState가 연속으로 호출된다면 배치처리를 통해 한 번에 렌더링하도록 한다.
- 즉, 아무리 많은 setState들이 연속적으로 사용되었어도 배치처리에 의해 한번의 렌더링으로 최신상태를 유지한다.
- 배치란
- 배치란 React가 너 나은 성능을 위해 여러개의 state 업데이트를 하나의 리렌더링으로 묶는 것을 의미한다.
- React는 16ms 동안 변경된 상태 값들을 하나로 묶는다. (16ms 단위로 배치를 진행한다.)
- 좀 더 자세히 들어가자면
- setState는 이벤트 핸들러 내에서 비동기적으로 동작한다.
- 하나의 이벤트 핸들러 내에서 setState가 여러 번 호출된다면, 이벤트가 끝날 시점에 state를 일괄적으로 업데이트하고 렌더링한다.
- 즉, 리액트는 setState 호출 즉시 state를 변경하고 리렌더링하는 것이 아니라, 여러 차례 setState가 있으면 여러 state의 변경을 통합해서 한꺼번에 리렌더링한다.
- 이 얘기를 내 방식으로 풀어 쓰자면
- 하나의 함수에서(그 함수 내부에 선언된 다른 함수들까지도!, 이벤트 핸들러니까!)
- setState가 한번 호출되든
- 여러번 호출되든
- 그 외의 다른 다양한 setState들이 호출되든
- 그 state는 그 하나의 함수(함수 내부 다른 함수들에서도 포함)내에서 바뀌어 있지 않다!
- 그 하나의 함수 밖으로 나왔을 때 비로소 그 state는 값이 바뀌어 있다.
- 이게 핵심이다.
useState가 비동기인 이유 with React module
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState)
}
- React의 useState 함수이다. 이 함수는 resolveDispatcher라는 함수가 반환하는 객체의 useState라는 메서드를 실행하여 반환되는 값을 리턴한다.
- 그럼 이제 resolveDispatcher를 살펴보자.
function resolveDispatcher() {
var dispatcher = ReactCurrentDispatcher.current;
if (!(dispatcher !== null)) {
{
throw Error( "Invalid hook call. Hook can only
}
}
return dispatcher;
}
- resolveDispatcher함수는 다시 ReactCurrentDispathcer라는 객체의 current속성을 반환한다.
var ReactCurrentDispatcher = {
/**
* @internal
* @type {ReactComponent}
*/
current: null
};
- 즉 useState는 ReactCurrentDispatcher 객체의 useState 메서드를 실행시키는 것이다.
- 이때 주목할 점은 ReactCurrentDispatcher가 객체라는 점이다.
- 객체이기 때문에 동일한 key 값에 대하여 이전의 값을 계속해서 덮어쓴다.
- 결국에는 마지막 명령어만 수행되는 셈이다.