[핵심] useState는 비동기로 작동된다.
[핵심] useState는 객체 또는 함수를 인자로 받을 수 있다.
[핵심] setState를 연속으로 호출할 경우 batch update 한다. batch update란 "묶어서" 변경한다는 것이다.
export default function App() {
const [number, setNumber] = useState(1);
const add = () => setNumber(number + 1);
const subtract = () => setNumber(number - 1);
const multiplyBy2 = () => setNumber(number * 2);
const multiplyBy2AndAddBy1 = () => {
multiplyBy2();
add();
};
return (
<div className="outer-wrapper">
<h1>Number : {number}</h1>
<div className="btn-wrapper">
<button onClick={add}>+ 1</button>
<button onClick={subtract}>- 1</button>
<button onClick={multiplyBy2}>*2</button>
<button onClick={multiplyBy2AndAddBy1}>*2 + 1</button>
</div>
</div>
);
}
위 코드에서 setNumber는 "객체"를 인자로 받고 있다. 그리고 multiplyBy2AndAddBy1 함수는 두 개의 함수를 호출하고 있고, 각각 setNumber를 통해 state를 변경하고 있다. 즉 setState가 연속적으로 실행되어 batch update되는 상황에 있다.
multiplyBy2AndAddBy1를 실행하면...
const prevState = {
{ number: 1 }
}
const newState = {
prevState,
{ number: number * 2 }, // multiplyBy2
{ number: number + 1 } // add2
}
바로 newState 객체는 multiplyBy2, add2에 대한 내용을 하나로 묶고있다. 그리고 객체는 중복된 값이 있을 경우 마지막 값을 사용한다. (덮어 씀) 이러한 흐름때문에 multiplyBy2AndAddBy1함수를 실행했을때 number + 1만 실행된 것이다.
결과적으로는 setNumber의 인자로 콜백함수를 넣어 사용하면 문제는 해결된다.
export default function App() {
const [number, setNumber] = useState(1);
const add = () => setNumber(prev => prev + 1);
const subtract = () => setNumber(prev => prev - 1);
const multiplyBy2 = () => setNumber(prev => prev * 2);
const multiplyBy2AndAddBy1 = () => {
multiplyBy2();
add();
};
return (
<div className="outer-wrapper">
<h1>Number : {number}</h1>
<div className="btn-wrapper">
<button onClick={add}>+ 1</button>
<button onClick={subtract}>- 1</button>
<button onClick={multiplyBy2}>*2</button>
<button onClick={multiplyBy2AndAddBy1}>*2 + 1</button>
</div>
</div>
);
}
setNumber의 매개변수인 함수는 state의 이전 값을 매개변수로써 가지고 있고 그 매개변수를 업데이트 하여 반환해주는 구조이다. 다시 한번 multiplyBy2AndAddBy1를 뜯어보기에 앞서 queue에 대해 알고 있어야 한다.
multiplyBy2함수는 queue라는 줄에서 제일 앞쪽에 선다. 그 다음에는 add2 함수가 선다. queue는 FIFO(First In First Out)으로 실행되기 때문에 multiplyBy2함수가 실행되어 state를 업데이트하고, add2함수가 순차적으로 실행되어 state를 다시 업데이트한다.
useState는 여전히 비동기로 작동되고 있으나 콜백함수를 매개변수로 넣음으로써 동기적으로 작동한 것 처럼 보인 것이다.
그림으로 설명해보면...
useState 코드를 열어보면 setState 함수는 인자로 state 객체(그림1) 를 받을 수도 있고, 이전 state 객체를 인자로 받고 새로운 state 객체를 반환하는 함수(그림2) 를 받을 수도 있다.
setState가 객체를 인자로 받을 경우 batch update를 하기 때문에 객체의 키값이 동일한 경우 마지막에 선언된 키와 값이 덮어쓰여진다. 하지만 함수를 인자로 받을 경우 그림2와 같이 이전 state 값을 객체로 받아 새로운 state 객체를 반환한다는 부분과, queue의 순차적인 실행방식에 의해 merging되는 룰을 피할 수 있게되어 정상적으로 작동되게 된다.
const heavyWork = () => {
console.log("엄청 무거운 작업~!");
return ["a", "b", "c"];
}
const App = () => {
const [ names, setNames ] = useState(heavyWork());
const [ input, setInput ] = useState("");
const handleInputChange = (e) => {
setInput(e.target.value);
}
const handleUpload = () => {
setNames((prev) => {
return [input, ...prev];
})
}
}
위 코드는 input안에 텍스트가 입력될때마다 handleInputChange함수가 작동해 input의 상태값을 업데이트해준다.
상태값이 변경되기 때문에 App 컴포넌트는 텍스트가 입력될때 마다 리렌더링된다. 그리고 heavyWork함수가 App 컴포넌트가 리렌더링될때마다 호출되고 이는 엄청난 성능저하를 가져오기도 하며 버그가 발생할 수 있다.
아래와 같이 useState의 초기값으로 콜백함수를 사용해 최초 렌더링 시 한번만 호출하게 만들 수 있다. (Lazy initial state라고 공식문서에서 설명한다 | https://ko.reactjs.org/docs/hooks-reference.html#lazy-initial-state)
const heavyWork = () => {
console.log("엄청 무거운 작업~!");
return ["a", "b", "c"];
}
const App = () => {
const [ names, setNames ] = useState(() => heavyWork());
const [ input, setInput ] = useState("");
const handleInputChange = (e) => {
setInput(e.target.value);
}
const handleUpload = () => {
setNames((prev) => {
return [input, ...prev];
})
}
}
공식문서에서 설명하길 useState는 state값을 비교할때 자바스크립트 비교알고리즘 중 Object.is를 사용한다고한다. (참고링크)
'React.js' 카테고리의 다른 글
React Virtual DOM 비교 원리와 얕은 비교(feat. 자바스크립트 비교알고리즘 Object.is( )) (0) | 2022.10.15 |
---|---|
[심층분석] useEffect & dependency array (feat. API race condition, useEffect 클린업 코드) (0) | 2022.10.12 |
리액트의 작동원리 (0) | 2022.10.06 |
[애니메이션] GSAP 적용 (0) | 2022.08.12 |
깨달음 (리액트 비동기) (0) | 2022.08.11 |
댓글