React 컴포넌트의 상태관리를 위해 정말 자주 사용하는 useState ⭐️
하지만 반드시 주의해야 할 점은 "useState는 비동기적으로 동작" 한다는 것이다.
비동기적으로 동작한다는 것의 의미는 특정 코드의 동작이 끝날 때까지 기다리지 않고 다음 코드의 동작을 바로 실행시키는 방식을 말한다. 이를 useState의 동작에 적용시켜 본다면, 상태 변화 함수를 통해 state를 변화시키더라도 그것을 바로 적용시키지 않고 다음 렌더링 시점까지 기다리는 것이다.
간단한 예시를 통해 useState의 비동기 동작 과정을 자세히 알아보자.
const [count, setCount] = useState(0); //state의 초기값 = 0
const handleClick = () => {
setCount(count + 1); //✅ 업데이트 요청, 실제로 값에는 반영 X
console.log(count); //❌ 여기서는 아직 이전 값 (0)
};
1️⃣ count라는 상태 변수와 2️⃣ handleClick라는 이벤트 핸들러 함수가 있다.
본래 의도는 handleClick 함수 내의 setCount 상태 변화 함수가 실행되면서 count값이 1 증가하고,
1 증가한 값을 console.log로 확인하는 것이다.
하지만 기대와는 달리 console에 찍힌 값은 여전히 0일 것이다! 🤔
그 이유는 바로 useState의 비동기적 특성 때문이다.
handleClick 함수 내에서 setCount가 실행되더라도 상태 값인 count의 변화는 바로 적용되지 않는다. count 값이 여전히 0인채로 console.log(count)에 도달하게 된다. 이후 이벤트 핸들러 함수가 완전히 종료되고 ⭐️해당 컴포넌트를 다시 렌더링 하는 시점⭐️ 에서야 count 값의 변화가 적용된다.
setCount가 이벤트 핸들러 함수 내부에서 실행될 당시의 동작은 단순히 상태값 변화를 "요청"하는 것으로 이해하면 된다. 그리고 그 요청을 실제로 "수행"하는 것은 그 즉시가 아닌, 다음 렌더링 시점인 것이다!
따라서 이벤트 핸들러 함수 내부의 console.log(count)가 실행될 때에는 아직 이벤트 핸들러 함수의 동작이 끝나기 전이므로 count 변수는 이전 상태 값인 0을 값으로 가지게 되고, 이후에 컴포넌트가 렌더링 된 후 1로 변화하게 된다.
이제부터 내가 로그인 페이지 개발 중 useState의 비동기적 특성을 고려하지 않아 발생했던 오류와 해결 방법을 작성해보겠다!
1) 로그인 버튼 클릭
2) 입력받은 이메일(아이디)가 유효한 형식인지 확인
3-1) 유효한 이메일이라면 로그인 로직 실행
3-2) 유효하지 않은 이메일이라면 경고 메세지 출력
아직 UI를 구현하는 단계였어서 로그인 과정까지 구현하지는 않았고 3-1의 로그인 로직은 console에 "로그인 진행 중..." 을 출력하는 것으로 코드를 짰다!
//로그인 제출과 관련된 상태 객체
const [submission, setSubmission] = useState({
isSubmitted: false,
isValidEmail: false,
});
//로그인 버튼 클릭시 실행되는 함수
const handleSubmit = (e) => {
e.preventDefault();
setSubmission(() => ({
isSubmitted: true,
isValidEmail: validateEmail(info.address),
}));
if (!submission.isValidEmail) {
console.log("이메일 형식 오류");
return;
}
console.log("로그인 진행 중...");
console.log(info);
//로그인 로직 진행 (ex. API 요청)
};
그런데 버튼을 처음 클릭했을 때는 유효한 이메일 형식이더라도 console에 이메일 형식 오류가 출력되었고, 두 번을 눌러야만 로그인이 정상적으로 진행되는 오류를 확인하였다.
해당 오류는 useState의 비동기 동작을 고려하지 않아 발생했던 것이다 🤯
handleSubmit 함수를 보면 setSubmission 상태 변화 함수가 실행되고, 해당 함수 내부에서 이메일 형식이 유효한지에 대한 정보를 담고있는 isValidEmail 상태 변수가 업데이트(실제로는 요청만!) 된다.
그런데 앞서 말했듯, 이 코드가 실행되어도 isValidEmail 변수는 즉각적으로 값이 업데이트 되지 않기 때문에 처음으로 handleSubmit이 실행되는 경우에 if (!submission.isValidEmail) 조건문의 isValidEmail은 여전히 초기값이 false로 저장되어 있던 것이다.
이후 handleSubmit 함수 실행이 완전히 종료되고 컴포넌트가 리렌더링 되면서 isValidEmail은 그제서야 true 값을 가지므로, 두 번째로 로그인 버튼을 눌렀을 때는 이메일의 형식이 유효하다고 옳게 판단하여 로그인 로직을 진행하게 된다.
이처럼 useState 업데이트 직후에는 해당 값에 의존하는 코드를 작성하면 안된다.
따라서 조건문과 같이 특정 값에 대한 연산이 필요한 경우 해당 값을 저장하는 javascript 변수를 선언하여 사용하면 된다! 아래는 이메일 형식 검사의 결과값을 담는 isValid 변수를 선언하고 그것을 조건문에 사용한 코드이다.
해당 코드를 실행시키면 이메일 형식이 유효한 경우 로그인 버튼을 누르면 바로 로그인 로직이 동작하는 것을 확인할 수 있다!
const handleSubmit = (e) => {
e.preventDefault();
const isValid = validateEmail(info.address);
setSubmission(() => ({
isSubmitted: true,
isValidEmail: validateEmail(info.address),
}));
if (!isValid) {
console.log("이메일 형식 오류");
return;
}
console.log("로그인 진행 중...");
console.log(info);
//로그인 로직 진행 (ex. API 요청)
};
useState를 공부하며 비동기에 관한 내용을 공부했었는데 막상 코드 짜면서는 고려를 하지 않고 막 쓰다가 드디어(ㅎ) 오류를 만난 덕분에 다시금 중요한 개념을 짚고 넘어갈 수 있었다.
아무리 열심히 공부를 해도 기억하려고 노력하지 않으면 금방 까먹게 되는 것 같다. 개발을 하면서도 무작정 하는 것이 아니라 그동안 배웠던 것을 끄집어내고 체화시키는 과정도 함께한다면 더 나은 개발자가 될 수 있을 것 같다 💪💪 파이팅!!