https://dev.to/bytebodger/synchronous-state-with-react-hooks-1k4f
해당 글을 번역한 내용입니다.
setState, 상태값 변경 함수는 비동기적으로 동작한다. 리액트는 효율적으로 렌더링하기 위해 여러 개의 상태값 변경 요청을 배치로 처리한다. 리액트가 상태값 변경 함수를 동기로 처리하면 하나의 상태값 변경 함수가 호출될 때마다 화면을 다시 그리기 때문에 성능 이슈가 생길 수 있다. 만약 동기로 처리하지만 매번 화면을 다시 그리지 않는다면 UI 데이터와 화면 간의 불일치가 발생해서 혼란스러울 수 있다.
이러한 비동기적인 특징 때문에 우리가 의도한 결과와는 다른 결과물이 나올 수 있다. 이를 피하기 위해서는 setState
를 동기적으로 동작시킬 수 있어야 한다.
가장 간단한 방법은 이전 값을 받아서 실행하는 방법이다. ex) setState(prev => prev + 1)
하지만, 더욱 복잡한 로직에서는 단순히 이전 값을 받아서 실행하는 것으로 해결할 수 없는 경우들이 있다.
export default function App() {
const [memberId, setMemberId] = useState('');
const [validateEntireForm, setValidateEntireForm] = useState(false);
const updateMemberId = ({ value }) => {
// (1)
setMemberId(value);
// (2)
validateMemberId();
if (validateEntireForm)
validateForm();
}
const validateForm = () => {
if (!validateEntireForm)
setValidateEntireForm(true);
validateMemberId();
// validate the rest of the fields in the form
}
const validateMemberId = () => {
if (memberId.length === 5) {
return validOrNot;
}
}
return (
<>
<input onChange={updateMemberId} />
</>
);
input
에 텍스트 입력시 updateMerberId
함수가 동작하면서 memberId
값이 설정된다. 이후 validateMemberId
함수 호출validateMemberId
는 memberId
값을 확인하게 된다.이때 memberId
의 값은 setMemberId()
가 호출되기 이전인 (1)의 상태일까? 아니면 setMemberId()
가 호출된 이후인 (2)의 상태일까?
setState
함수가 비동기로 동작한다는 사실을 모른다면 당연히 (2)의 상태이기를 기대하면서 코드를 작성하게 된다. 하지만, 우리의 기대와는 달리 setState
함수는 비동기로 동작하고 이 때문에 memberId
의 값은 (1)의 상태를 가지게 된다.
setState
함수가 가지는 비동기적인 특징으로 인해 의도하지 않은 결과가 만들어지게 된다. 이러한 에러를 어떻게 해결할 수 있을까?
모든 함수를 하나로 합친다.
만약 모든 함수가 하나로 합쳐진다면, 그 안에서 하나의 임시변수를 통해 새로운 값을 저장하고 사용하면 된다. 하지만, 이 방법은 함수의 크기를 과하게 크게 만드는 부작용이 있다.
명시적으로 값을 다음 함수에 넘긴다.
위의 경우에서는
updateMemberId
함수 내부에서validateMemberId
를 호출할 때value
를 인자로 넘겨주자는 의미이다. 하지만, 이러한 방식은 코드를 이해하기 더욱 어렵게 만든다. 만약 로직이 조금 더 복잡해진다면 계속해서 인자값이 넘어가야하는 상황이 발생하고, 이는 유지보수의 비용을 증가시킨다.
reducer를 사용하는 방법
useState()
대신useReducer()
를 사용하는 것이다.useReducer()
는 동기적으로 동작하기 때문에useState()
가 비동기적으로 동작함으로 인해 발생됐던 문제들을 해결할 수 있다. 하지만, 하나의 컴포넌트에서만 사용될 상태값들을 이렇게 전역으로 관리하는 것 자체가 너무 별로이다.
useRef()
를 사용하는 방법
값을
state
,useRef()
이 두 곳에 저장하는 방법이다. 비동기로 인해 문제가 발생하는 지점에서는state
대신useRef()
에 저장해놓은 값을 사용해서 이 문제를 해결할 수 있다. 고전적인 방법들 중에서는 가장 훌륭하지만, 저자는 뭔가 아쉽다고 한다.
useEffect()
를 사용하는 방법
useEffect 훅이 마운트 이후에 실행된다는 특징을 이용한 방법이다.
import { useState, useEffect } from "react";
export default function CountWithEffect() {
const [count, setCount] = useState(0);
const [doubleCount, setDoubleCount] = useState(count * 2);
const handleCount = () => {
setCount(count + 1);
};
useEffect(() => {
setDoubleCount(count * 2); // This will always use latest value of count
}, [count]);
return ()
}
이런 식으로 코드를 작성하여
setDoubleCount
는 항상 맨 마지막에 실행되도록 보장시킬 수 있다. 하지만,setState
함수 하나하나useEffect
훅으로 묶을 수는 없는 노릇이다.
useCallback()
을 사용하는 방법
const validateMemberId = useCallback(() => {
if (memberId.length === 5) {
return validOrNot;
}
}, [memberId])
다음과 같이
useCallback
을 활용하여 이슈를 해결할 수도 있다. 하지만 이 또한 동기적인 동작이 필요한 모든 함수에useCallback
을 감싸야 한다는 문제가 존재한다.
즉, useState
를 동기적으로 사용할 수 있는 방법을 저자가 제시하고 있다.
export const useSyncState = <T> (initialState: T): [() => T, (newState: T) => void] => {
const [state, updateState] = useState(initialState)
let current = state
const get = (): T => current
const set = (newState: T) => {
current = newState
updateState(newState)
return current
}
return [get, set]
}
const SomeComponent = () => {
const [counter, setCounter] = useSyncState<number>(0);
const increment = () => {
console.log('counter =', counter()); // 0
const newValue = setCounter(counter() + 1);
console.log('newValue =', newValue); // 1
console.log('counter =', counter()); // 1
}
return (
<>
Counter: {counter()}
<br/>
<button onClick={increment}>Increment</button>
</>
);
}
하지만, 이 방법 또한 다음과 같은 문제점들을 지닌다.
useTrait()
doesn't work if the value being saved is returned in a truly asynchronous manner. For example, if the variable is supposed to hold something that is returned from an API, then you won't be able to simply set()
the value and then, on the very next line, get()
the proper value. This is only meant for variables that you wouldn't normally think of as being "asynchronous" - like when you're doing something dead-simple, such as saving a number or a string.set()
operations.useReducer()
. I'm not going to try to argue them off that cliff. Similarly, the Children of Hooks would probably try to address this with useEffect()
. Personally, I hate that approach, but I'm not trying to fight that Holy War here.실전 리액트 프로그래밍 개정판 [인사이트]
https://dev.to/shareef/react-usestate-hook-is-asynchronous-1hia
https://dev.to/bytebodger/synchronous-state-with-react-hooks-1k4f