왜 setState에 대한 글만 따로 빼서 설명하게 되었는가
평소에 짜던 모든 코드가 동기로 작동해서 비동기로 동작하는 setState의 동작 원리가 이해가 가질 않았다. 공식 문서 + 블로그 를 참고하면서 공부한 내용을 정리해보고자 한다.
setState(nextState, callback?)
리액트 공식 문서 중 일부
setState를 호출해서 React 컴포넌트이 state를 변경한다.
setState는 컴포는트 state에 대한 변경 사항을 큐에 넣는다. 이 컴포넌트와 그 자식이 새로운 state로 다시 렌더링해야 한다는 것을 React에게 알려준다. 이것이 상호작용에 반응하여 사용자 인터페이스를 업데이트하는 주요 방법
setState를 사용해서 state를 업데이트해야 사용자와 상호작용할 수 있다는 말인것 같다.
state변경은 무조건 setState를 사용해야한다.
setState는 비동기로 동작한다.
function handleClick() {
console.log(this.state.name); // "Taylor"
this.setState({
name: 'Robin'
});
console.log(this.state.name); // Still "Taylor"!
}
리액트 공식 문서 중 일부
setState를 호출해도 이미 실행중인 코드의 현재 state는 변경되지 않는다
오로지 다음 렌더링부터 this.state가 반환할 내용에만 영향을 준다.
"오로지 다음 렌더링부터 this.state가 반환할 내용에만 영향을 준다."
다음 코드를 보면 이 내용이 무슨 말인지 깨닫게 될거다.
import { useState } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
console.log('rerender');
const handleUpCount = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return (
<div className="App">
<h1>{count}</h1>
<button onClick={handleUpCount}>+Up</button>
</div>
);
}
이 코드를 실행해보면 버튼을 눌러도 count는 1씩 증가한다.
그 이유는? 함수 내에서 실행중인 코드(handleUpCount함수 내)에서는 현재 state가 변경되지 않기 때문이다.
count 초기 값이 0이면 handleUpCount가 처음 실행 됐을때 setCount는 모두 count = 0 인 값을 바라보고 있음
아무리 setCount로 count의 상태를 변경해도 count는 1씩밖에 증가하지 않음
그럼 이전 state기반으로 state를 업데이트하려면 어떻게 해야하나?
setState에 함수 전달
리액트 공식 문서 중 일부
setState에 함수를 전달할 수도 있습니다. 이 함수를 사용하면 이전 state를 기반으로 state를 업데이트할 수 있습니다.
동일한 이벤트 중에 state를 여러 번 업데이트하려는 경우 유용합니다.
handleIncreaseAge = () => {
this.setState(prevState => {
return {
age: prevState.age + 1
};
});
}
const handleUpCount = () => {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
};
리액트 공식 문서에서 아래와 같이 말하고 있다.
React는 state 업데이트를 batch 합니다.
모든 이벤트 핸들러가 실행되고 set 함수를 호출한 후에 화면을 업데이트합니다.
이벤트가 끝날때 일괄 수정된다고 생각하면 되겠다..
setState는 this.state를 즉시 업데이트하지 않습니다.
따라서 setState를 호출한 직후 this.state를 읽는 것은 잠재적인 위험이 될 수 있습니다.
대신, 업데이트가 적용된 후에 실행되도록 보장되는 componentDidUpdate 또는 setState callback 인자를 사용하십시오.
이전 state를 기반으로 state를 설정해야 하는 경우 위에서 설명한 대로 함수를 nextState에 전달할 수 있습니다.
위에서본 `setCount(count+1)` 와 같은 상황을 말하는 것이다.
여기서 궁금한 점은 `setState((prev) => prev+1)` 에서 prev는 어떻게 이전 state값을 알 수 있는 것일까?
updater 함수
리액트 공식 문서에서 setState(function)으로 넘겨줄 경우 아래와 같이 설명하고 있다.
nextState: state가 될 값입니다. 값은 모든 데이터 타입이 허용되지만, 함수에 대해서는 특별한 동작이 있습니다.
함수를 nextState로 전달하면 업데이터 함수로 취급합니다.
이 함수는 순수해야 하고, 대기 중인 state를 유일한 인수로 사용해야 하며, 다음 state를 반환해야 합니다. React는 업데이터 함수를 대기열에 넣고 컴포넌트를 리렌더링 합니다.
다음 렌더링 중에 React는 대기열에 있는 모든 업데이터를 이전 state에 적용하여 다음 state를 계산합니다.
React는 업데이터 함수를 큐에 넣습니다. 그러면 다음 렌더링 중에 동일한 순서로 호출합니다.
아래와 같은 예시를 보자
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}
1. a => a + 1은 대기 중인 state로 42를 받고 다음 state로 43을 반환합니다.
2. a => a + 1은 대기 중인 state로 43을 받고 다음 state로 44를 반환합니다.
3. a => a + 1은 대기 중인 state로 44를 받고 다음 state로 45를 반환합니다.
대기 중인 다른 없데이트가 없으므로 ,React는 결국 45를 현재 state로 저장한다.
규칙상 대기 중인 state 인수의 이름을 age의 a와 같이 state 변수 이름의 첫 글자로 지정하는 것이 일반적입니다.
그러나 prevAge 또는 더 명확하다고 생각하는 다른 이름으로 지정해도 됩니다.
여기서 큐에 대해서 알아보자.
state 업데이트 큐
state 변수를 설정하면 다음 렌더링이 큐에 들어갑니다.
그러나 때에 따라 다음 렌더링을 큐에 넣기 전에, 값에 대해 여러 작업을 수행하고 싶을 때도 있습니다.
이를 위해서는 React가 state 업데이트를 어떻게 배치하면 좋을지 이해하는 것이 도움이 됩니다.
업데이터 함수 동작 방식
- React는 이벤트 핸들러의 다른 코드가 모드 실행된 후에 이 함수(업데이터 함수)가 처리되도록 큐에 넣는다.
- 다음 렌더링 중에 React는 큐를 순회하여 최종 업데이트된 state를 제공한다.
리액트가 이벤트 핸들러를 수행하는 동안 여러 코드를 통해 작동하는 방식은 다음과 같다
- `setAge(a=>a+1)` : `a=>a+1` 함수를 큐에 추가
- `setAge(a=>a+1)` : `a=>a+1` 함수를 큐에 추가
- `setAge(a=>a+1)` : `a=>a+1` 함수를 큐에 추가
다음 렌더링 중에 useState를 호줄하면 리액트는 큐를 순회한다. (다음 렌더링 중에!!)
- 이전 `age`의 state는 0이였으므로 리액트는 첫번째 업데이터 함수에 `a`인수로 전달한다.
- 그 다음 리액트는 이전 업데이터 함수의 반환값(1)을 가져와서 다음 업데이터 함수에 `a`으로 전달하는 식으로 반복
리액트 공식 문서의 사진을 가져오자면 아래와 같다
리액트는 3을 최종 결과로 저장하고 useState에서 반환한다.
만약 하나의 이벤트 핸들러 안에 setState(value)값으로 전달하는 함수와 setState(updater) 업데이터 함수로 전달하는 함수가 같이 있다면?
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>Increase the number</button>
</>
)
}
버튼을 누르면 6이 나오게 된다.(공식 문서에 직접 확인해볼 수 있다)
동작 과정
- setNumber(number + 5): number는 0 이므로 setNumber(0+5). 리액트 큐에 `replace with 5`를 추가한다.
- setNumber(n => n + 1): n => n+1는 업데이터 함수이다. 리액트는 해당 함수를 큐에 추가
- 다음 렌더링 하는 동안 리액트는 State 큐를 순회한다.
따라서 큐에서 5가 먼저 반환되고 number는 5로 먼저 바뀌고, 그 다음에 업데이터 함수가 큐에서 나오면서 인자로 5가 넘어간다.
최종적으로 5 + 1 이 반환된다.
큐는 다음과 같은 상태이다.
여기서 중요한 점은 setState(number+5)가 실제로는 setState(n=>5)처럼 동작하지만, n 이 사용되지 않았다.
이벤트 핸들러가 완료되면 React는 리렌더링을 실행합니다. 리렌더링하는 동안 React는 큐를 처리합니다.
업데이터 함수는 렌더링 중에 실행되므로, 업데이터 함수는 순수해야 하며 결과만 반환해야 합니다.
업데이터 함수 내부에서 state를 변경하거나 다른 사이드 이팩트를 실행하려고 하지 마세요.
Strict 모드에서 React는 각 업데이터 함수를 두 번 실행(두 번째 결과는 버림)하여 실수를 찾을 수 있도록 도와줍니다.
'ASAC 웹 풀스택' 카테고리의 다른 글
React 의 특장점, 렌더 라이프사이클 및 Hook(4) - State (1) | 2024.09.22 |
---|---|
React 의 특장점, 렌더 라이프사이클 및 Hook(5) - Props (0) | 2024.09.22 |
React 의 특장점, 렌더 라이프사이클 및 Hook(2) - 단방향 바인딩 + Hook (1) | 2024.09.20 |
React 의 특장점, 렌더 라이프사이클 및 Hook(1) - JSX문법 (0) | 2024.09.20 |
자바스크립트 기본, 심화 문법 및 엔진 동작 원리(5) - 자바스크립트 객체 정의 및 사용 + 모듈 시스템 (2) | 2024.09.11 |