복습 : 각 렌더링의 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 + 1); setNumber(number + 1); }}>+3</button> </> ) }
버튼 누르면 setState() 세번 실행해도 → 0 에서 1로 바뀐다.
사실상 이것과 같다 :
setNumber(0 + 1); setNumber(0 + 1); setNumber(0 + 1);
위 현상이 setState()가 몇번 불려도 state의 스냅샷을 가지고 가기 때문이라는 사실 이외에도 한가지 개념을 더 알아야 한다.
그래서 setNumber()
세 개가 다 실행되고 나서야 리렌더링이 일어난다.
비유 : 레스토랑에서 주문받기
웨이터는 주문을 다 받고, 손님이 바꿀 것 다 바꾸고, 다른 테이블 오더도 받고 나서 주방으로 움직인다.
이리하야 여러 state 값을 업데이트 할 때-심지어 다수의 컴포넌트의 state값들도- 너무 많은 리렌더링을 하지 않을 수 있는 것이다.
이 말은 이벤트 핸들러 안의 모든 코드가 실행 완료 될 때까지 UI가 업데이트 되지 않는다는 뜻이기도 하다.
이것이 바로 Batching. 리액트 앱 속도를 빠르게 만들어준다. 변수 중 일부만을 업데이트하는 렌더링을 방지하기도 함.
클릭을 여러번 한다고 하여 여러 클릭 간 state 업데이트를 batch (묶기) 하지 않음. 각 클릭은 따로 처리된다.
따라서 버튼을 눌렀을 때 disabled 로 만드는 함수를 넣었다면, 두번째로 누르면 버튼이 작동하지 않을 것이다!
자주 안 나오는 사례이긴 함. 그러나 위의 예시를 실제로 하고 싶다면, setNumber(number + 1)
처럼 바뀌기 원하는 다음 state값 을 즉시 setState의 인자로 넘기지 마라 (=replace).
기존 state값을 가지고 다음 state를 만들어주는 함수 를 인자로 넘겨줘라.
→ setNumber(n => n+1)
이건 리액트에게 즉시 새로운 값을 state 자리에 넣는게 아니라 "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>
</>
)
}
n => n+1
은 updater 함수 라고 불린다. 이것을 setState()의 인자로 넣으면 :
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
이 코드는 이렇게 실행됨 :
setNumber(n => n+1)
: n => n + 1
은 함수다. 리액트가 이 함수를 큐에 올려놓는다.setNumber(n => n+1)
: n => n + 1
은 함수다. 리액트가 이 함수를 큐에 올려놓는다.setNumber(n => n+1)
: n => n + 1
은 함수다. 리액트가 이 함수를 큐에 올려놓는다.useState
를 다음 렌더에서 호출하면, 리액트는 큐를 쭉 실행시킴. 이전 number
state값이 0
이었기 때문에, 리액트는 이 값 0
을 첫번째 updater 함수 의 n
인자로 넘겨준다. 그 뒤 리액트는 이전 updater 함수의 리턴값을 다음 updater 함수의 n
인자로 넘겨준다. 더보기..
그래서 아까와 달리, 위 코드를 실행하면 제대로 0
→ 3
으로 바뀐다.
이렇게 하면 어떻게 될까? number
state가 다음에 무슨 값이 될 것이라고 생각하는가?
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
=> 리액트는 이렇게 한다.
setNumber(number+5)
: 현재 number
는 0
이라서 setNumber(0+5)
다. 리액트는 큐에 "5
로 replace"를 추가한다.
setNumber(n => n+1)
: n => n+1
은 updater 함수. 리액트는 그 함수 를 큐에 추가.
다음 렌더링 시, 리액트는 state 큐를 실행한다...
que된 업데이트 | n | 리턴값 |
---|---|---|
5 로 바꿔줘(replace) | 0 (사용 안됨) | 5 |
n=>n+1 | 5 | 5 + 1 = 6 |
6
을 최종 결과값으로 저장하고 useState
에서 리턴한다.
setState(x)
(replace 하기) 는 사실상setState(n => x)
이렇게 작동하지만,n
은 사용 안된다는 사실!
이 경우는 어떨까?
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>
setNumber(number+5)
: 현재 number
는 0
이라서 setNumber(0+5)
다. 리액트는 큐에 "5
로 replace"를 추가한다.
setNumber(n => n+1)
: n => n+1
은 updater 함수. 리액트는 그 함수 를 큐에 추가.
(여기까지는 똑같다)
setNumber(42)
: 리액트는 "42
로 replace" 를 큐에 추가.다음 렌더에 리액트는 state 큐를 실행한다...
que된 업데이트 | n | 리턴값 |
---|---|---|
5 로 바꿔줘(replace) | 0 (사용 안됨) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
"`42로 replace" | 6 (사용 안됨) | 42 |
이제 리액트는 42
를 최종 결과로 저장, useState
에서 리턴함.
setNumber
state setter에 어떤 인자를 넘겨줄지는 이렇게 생각하면 된다.n => n+1
)이 큐에 추가됨5
) 는 "5
로 replace"를 큐에 추가. 기존 큐는 무시된다!!setState() 인자에도 네이밍 컨벤션이 있는줄 몰랐다. 디테일하군..
// [enabled, setEnabled]
setEnabled(e => !e);
// [lastName, setLastName]
setLastName(ln => ln.reverse());
// [friendCount, setFriendCount]
setFriendCount(fc => fc * 2);
조금 더 자세한 규칙은 그냥 state 이름을 풀네임으로 쓰기.
setEnabled(enabled => !enabled)
또는 접두사 prev
사용
setEnabled(prevEnabled => !prevEnabled)
setState는 기존 렌더의 변수값을 바꾸지 않음. 대신 새로운 렌더링을 요청함. (state = const)
리액트는 이벤트 핸들러가 전부 실행 완료되고 나서 state 업데이트를 실행함.
한 이벤트에서 여러번 state를 바꾸고 싶다면, setNumber(n => n + 1)
처럼 updater 함수 를 사용할 것.