state변수를 설정하면 다음 렌더링이 큐에 들어가는데, 경우에 따라서 다음 렌더링을 큐에 넣기 전에 값에 대해 여러 작업을 수행하고 싶을때가 있다. (렌더링 기다리지 않고 바로 업데이트된 값을 사용하고 싶을때!)
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 5);
setNumber(number + 10);
}}>+3</button>
</>
)
}
위와같은 코드가 있을때, button을 클릭할때마다 16씩 number가 증가하기를 기대하지만, 사실상 버튼을 누를때마다 10씩 증가하게 된다.
왜냐하면 setNumber(number + 1)
, setNumber(number + 5)
, setNumber(number + 10)
이렇게 3번 호출이 되지만 실제로는 아직 re-rendering이 이루어 지지 않아 각각의 setNumber에 들어가는 number의 값은 initial value인 0이 들어가기 때문이다.
왜냐하면 React는 state 업데이트를 하기전에 이벤트 핸들러의 모든 코드가 실행될 때까지 기다리기 때문임!
그래서 re-rendering은 모든 setNumber()
가 호출된 완료에 이후에 일어나므로, 마지막의 setNumber(number + 10)
이 수행되게 된다.
리액트는 손님에게 주문을 받는 웨이터라고 했는데 손님이 만약
콜라주세요
→ 아 아니다 사이다 주세요
→ 아니다 그냥 마운틴듀 주세요
라고 연달아서 웨이터에게 주문하면, 콜라+사이다+마운틴듀를 주는게 아니라 마지막에 주문한 마운틴듀
를 갖다주지 않나?!
사실 성능측면에서 생각을 해보면 setNumber(number + 1)
, setNumber(number + 5)
, setNumber(number + 10)
이렇게 연달아 3번 수행하게 되면 re-rendering을 3번해야한다.
그럼 어제 정리했던 react life cycle을 다시한번 상기시켜보면 re-rendering을 위해서 getDerivedStateFromProps가 일어나고 shouldUpdate를 확인하고 render -> DOM update 와 같은 과정이 불필요하게 일어날거고 이는 performance 측면에서 lose라고 생각한다.
또 단순히 여기서는 state가 number 하나뿐이지만, 다수의 state가 존재할때는 더욱 lose가 되겠지?
따라서 이벤트 핸들러의 모든 코드가 실행될때까지 기다리면, 너무 많은 re-rendering을 trigger 하지 않고 여러 component에서 다수의 state를 update할 수 있다.
다음 렌더링이 되기전에 동일한 state를 여러번 업데이트 하고싶다면 setNumber(number + 1)
대신 setNumber(n => n + 1)
처럼 큐의 이전 상태(previous state)를 기반으로 다음 상태(next state)를 계산하는 함수를 전달할 수 있다.
이는 단순히 state값을 대체하는게 아니라 React에게 state값으로 무언가를 해! 라고 지시하는 방법이다.
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>
</>
)
}
여기서 setNumber(n => n + 1);
는 다른글에서 정리한것 처럼 updater function이라고 한다.
그리고 이를 state setter
(여기선 setNumber) 함수 인자로 전달할때,
1. React는 이벤트 핸들러의 다른 코드가 모두 실행된 후에 이 함수가 처리되도록 큐에 푸시한다
2. 다음 렌더링 중에 React는 큐를 순회해 최종 업데이트된 state를 제공한다
다음 렌더링 중에 useState
를 호출하면 React는 큐를 순회하는데,
이전 number
의 state는 0이므로, React는 첫번째 setNumber
함수인자 n에 0을 전달한다.
그 다음 React는 이전의 setNumber
의 반환값을 가지고와서 다음 setNumber
의 함수인자 n에 넣어준다.
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>
</>
)
}
위 코드를 실행시켜보면 버튼울 누르면 number는 6씩 증가하게 된는데, 이벤트 핸들러가 React에게 지시하는 작업은 아래처럼 된다.
1. setNumber(number + 5)
초기 number값은 0이니 0 + 5값인 5를 큐에 푸시
2. 큐에서 이전 값인 5에 1을 더해서 6
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);
setNumber(42);
}}>Increase the number</button>
</>
)
}
위 코드가 실행되면 리액트는 아래와 같은 순서로 작동하게 될건데
1. setNumber(number + 5)
: number는 0이고 0+5인 5를 queue에 푸시
2. setNumber(n => n + 1)
: 업데이터 펑션이고, 큐에있는 5에 1을 더해서 6을 큐에 푸시
3. setNumber(42)
: 리액트는 number를 42로 바꾸라는 액션을 큐에 추가
결과적으로 number는 42가 된다.
업데이터 함수 인수의 이름은 해당 state 변수의 첫글자로 지정하는것이 일반적이다.
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);
좀 더 자세한 코드를 사용하고 싶을경우 setEnabled(enabled => !enabled)
와 같이 전체 state의 이름을 반복하거나, setEnabled(prevEnabled => !prevEnabled)
와 같이 접두사 previous를 사용하는게 일반적이다.