리액트의 useEffect는 데이터 가져오기, 구독 관리, DOM 업데이트, 사이드 이펙트 처리 등 다양한 작업에 사용된다. 그러나 useEffect를 과도하게 사용하면 성능 저하, 불필요한 렌더링, 디버깅의 복잡성 같은 문제가 발생할 수 있다. "Leave useEffect Alone!" 라는 가이드 글을 참고하여 올바른 useEffect 사용법에 대한 추가 설명을 덧붙여서 정리해봤다.
경쟁 상태는 여러 비동기 작업이 동시에 실행될 때, 실행 순서나 결과가 예측하지 않은 방식으로 작동하는 현상을 가리킨다.
아래 코드에서 버튼을 여러 번 클릭하면 counter 값이 증가하고, 각 요청은 랜덤한 시간만큼 대기한 후 response 상태를 업데이트한다. 하지만 이 과정에서 경쟁 상태가 발생할 수 있다.
function RaceConditionExample() {
const [counter, setCounter] = useState(0);
const [response, setResponse] = useState(0);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const request = async (requestId) => {
setIsLoading(true);
await sleep(Math.random() * 3000);
setResponse(requestId);
setIsLoading(false);
};
request(counter);
}, [counter]);
const handleClick = () => {
setCounter((prev) => ++prev);
};
return (
<>
<h3>Current Value: {counter}</h3>
<h3>Settled Response: {response}</h3>
<button onClick={handleClick}>Increment</button>
{/* ... */}
</>
);
}
이처럼 response 상태는 가장 최신 요청의 결과를 반영해야 하지만, 비동기 요청의 완료 순서가 랜덤하기 때문에 의도치 않은 결과가 발생하고 있다.
이러한 경쟁 상태는 클린업 함수를 사용해서 처리할 수 있다. 클린업 함수는 다음 이펙트 함수가 실행되기 전에 호출되며, 이전 이펙트 함수의 스코프에서 동작한다.
useEffect(() => {
let ignore = false; // 현재 useEffect 함수의 스코프
const request = async (requestId) => {
setIsLoading(true);
await sleep(Math.random() * 3000);
if (!ignore) {
setResponse(requestId);
setIsLoading(false);
}
};
request(counter);
return () => {
ignore = true; // 이전 useEffect 함수의 스코프
};
}, [counter]);