기존 코드
const [arrState, setArrState] = React.useState([]);
const tempArray = [1, 2, 3, 4, 5];
React.useEffect(() => {
const twoSec = 2000;
tempArray.forEach((item, index) => {
setTimeout(() => {
setArrState([...arrState, item]);
}, twoSec * index);
}, []);
arrState
상태가 계속 초기 []
빈 배열에 있는 것을 확인했다.
리액트 공식 문서를 읽어보며 원인을 분석해본 결과는 이렇다.
위 코드의 forEach
문을 풀어 써보면 아래와 같은 코드로 볼 수 있다.
setTimeout(() => {
setArrState([...[], 1]); // 다음 렌더링 시에 빈배열 arrState에 1 추가
}, 2000 * 0);
setTimeout(() => {
setArrState([...[], 2]); // 다음 렌더링 시에 빈배열 arrState에 2 추가
}, 2000 * 1);
setTimeout(() => {
setArrState([...[], 3]); // 다음 렌더링 시에 빈배열 arrState에 3 추가
}, 2000 * 2);
setTimeout(() => {
setArrState([...[], 4]); // 다음 렌더링 시에 빈배열 arrState에 4 추가
}, 2000 * 3);
setTimeout(() => {
setArrState([...[], 5]); // 다음 렌더링 시에 빈배열 arrState에 5 추가
}, 2000 * 4);
arrState
배열에 값이 누적되지 않는 이유다. 위 코드가 실행 될 때의 렌더링에서는 arrState
값이 []
이기 때문이다.
문제해결 한 코드
const [arrState, setArrState] = React.useState([]);
React.useEffect(() => {
const twoSec = 2000;
tempArray.forEach((item, index) => {
setTimeout(() => {
setArrState(prev => [...prev, item]);
}, twoSec * index);
}, []);
상태 업데이터 콜백함수를 이용하면 매번 setTimeout
의 콜백함수가 실행될때마다 arrState
의 이전 상태를 이어서 업데이트 가능
setTimeout
함수를 쓰지 않는 경우에는 어떨까..
export default function App() {
const [arr, setArr] = React.useState([]);
const tempArr = [1, 2, 3, 4];
React.useEffect(() => {
tempArr.map((item, index) => {
setArr([...arr, item]);
})
}, [])
console.log(arr);
return (
<main>
{arr.map((item) => <div>{item}</div>)}
</main>
)
}
React.useEffect(() => {
setArr([...[], 1]); // 다음 렌더링 때 arr배열에 1추가
setArr([...[], 2]); // 다음 렌더링 때 arr배열에 2추가
setArr([...[], 3]); // 다음 렌더링 때 arr배열에 3추가
setArr([...[], 4]); // 다음 렌더링 때 arr배열에 4추가
}, [])
이 코드에서 마지막에 처리된 setArr
은 빈 배열에 4를 추가하고 있다. 리액트는 상태 스냅샷을 계속 업데이트 할 것인데 스냅샷들이 덮어 씌워져 마지막의 4를 추가하는 스냅샷이 다음 렌더링때 반영된다.
그렇지만 setTimeout
으로 인해 일정 시간후 setState
가 호출되는 코드라면 이미 렌더링이 일어난 후 다음 렌더링때 상태를 업데이트하라고 리액트에 요청하기 때문에 리렌더링이 setTimeout
이 호출되는 만큼 발생하게 된다.
export default function App() {
const [arr, setArr] = React.useState([]);
const tempArr = [1, 2, 3, 4];
React.useEffect(() => {
tempArr.map((item, index) => {
setTimeout(() => {
setArr([...arr, item]);
}, index)
})
}, [])
console.log(arr);
return (
<main>
{arr.map((item) => <div>{item}</div>)}
</main>
)
}
위 코드는 4개의 setTimeout
에 걸린 timeout
이 각각 0, 1, 2, 3, 4
밀리초로 설정된다.
코드를 실행해 본 결과, 시간이 너무 짧아 마지막 결과인 4만 화면에 보일 뿐이다.
콘솔에는 [] [1] [3] [4]
만 찍히는데 2밀리초 후에 실행되는 setArr은 무시된 걸 알 수 있었다. 현재 개발 환경의 리액트에서 arr의 값이 [1]
인 렌더링까지 걸린 시간이 2밀리초 이상 3밀리초 이하라는 것을 알 수 있는 부분이었다.
만약 위 코드를 굉장히 빠르게 여러번 재실행해보면 [2]
가 포함될 때도 있다.
이게 실행 시점에서 코드의 처리 속도가 달라질 수 있다는 것을 증명하는게 아닐까 싶다.