일련의 상태 갱신 대기열
상태 변수를 설정하면 다른 렌더링이 대기열에 추가됩니다. 그러나 때로는 다음 렌더링을 대기열에 넣기 전에 값에 대해 여러 작업을 수행해야 할 수도 있습니다. 이렇게 하려면 리액트가 상태 갱신을 일괄 처리(batch)하는 방법을 이해하는 것이 좋습니다.
- 일괄 처리란 무엇이며 리액트가 이를 사용하여 복수의 상태 갱신을 처리하는 방법
- 동일한 상태 변수에 복수의 갱신을 연속으로 적용하는 방법
리액트는 상태 갱신을 일괄 처리한다
+3
버튼을 클릭하면 setNumber(number + 1)
가 세 번 호출되므로 카운터가 세 번 증가할 것이라고 예상할 수 있습니다.
그러나 이전에 배웠듯이 각 렌더링의 상태 값은 고정이므로, 첫 렌더링의 이벤트 처리기 내부의 number
값은 setNumber(1)
를 몇 번 호출하더라도 항상 0
입니다.
jsx
setNumber(0 + 1);setNumber(0 + 1);setNumber(0 + 1);
jsx
setNumber(0 + 1);setNumber(0 + 1);setNumber(0 + 1);
그러나 여기에는 또 다른 요인이 있습니다. 리액트는 상태 갱신을 처리하기 전에 이벤트 처리기의 '모든' 코드가 실행될 때까지 기다립니다. 이것이 바로 모든 setNumber()
호출 이후에만 리렌더링이 발생하는 이유입니다.
이는 레스토랑에서 주문을 받는 웨이터를 생각나게 합니다. 웨이터는 여러분이 첫 번째 요리를 말할 때 바로 부엌으로 달려가지 않습니다! 대신 여러분의 주문이 끝날 때까지 기다리고, 주문이 바뀔수도 있으며, 테이블에 있는 다른 사람의 주문도 받을 수 있습니다.
이렇게 하면 너무 많은 리렌더링을 트리거하지 않고도 여러 컴포넌트에서 여러 상태 변수를 갱신할 수 있습니다. 그러나 이는 또한 이벤트 처리기와 그 안의 모든 코드가 완료될 때까지 UI가 갱신되지 않는다는 것을 의미합니다. 일괄 처리라고도 하는 이 동작을 통해 리액트 앱이 훨씬 빠르게 실행됩니다. 또한 일부 변수만 갱신되는 혼란스러운 렌더링(절반만 완료된)을 처리하는 것을 피합니다.
리액트는 클릭과 같은 '복수'의 의도적인 이벤트를 일괄 처리하지 않습니다. 각 클릭은 별도로 처리됩니다. 리액트는 일반적으로 안전한 경우에만 일괄 처리를 수행하므로 안심하세요. 예를 들어 첫 번째 버튼 클릭으로 양식이 비활성화된 경우, 두 번째 클릭으로 양식이 다시 제출되지 않습니다.
다음 렌더링 전에 동일한 상태를 여러 번 갱신하기
흔하지 않은 사용 사례이지만 다음 렌더링 전에 동일한 상태 변수를 여러 번 갱신하려면 다음 상태 값(예: setNumber(number + 1)
)을 전달하는 대신 대기열의 이전 상태 값을 기준으로 다음 상태를 계산하는 함수(예: setNumber(n => n + 1)
)를 전달할 수 있습니다. 이는 리액트에게 단순히 상태 값을 바꾸는 대신 상태 값으로 무언가를 수행하라고 지시하는 것입니다.
이제 카운터를 증가시켜 보세요.
여기서 n => n + 1
을 갱신 함수라고 부릅니다. 이 함수를 상태 설정자에 전달하면 다음이 수행됩니다.
- 리액트는 이벤트 처리기의 다른 모든 코드가 실행된 후에 이 함수가 처리되도록 함수를 대기열에 넣습니다.
- 다음 렌더링에서 리액트가 대기열을 통과하고 최종 갱신된 상태를 제공합니다.
jsx
setNumber((n) => n + 1);setNumber((n) => n + 1);setNumber((n) => n + 1);
jsx
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
상태는 0
이므로, 리액트는 이 값을 첫 번째 갱신 함수의 n
인수에 전달합니다. 그런 다음 리액트는 이전 갱신 함수의 반환 값을 가져와 다음 갱신 함수의 n
에 전달합니다.
대기 중인 갱신 | n | 반환 값 |
---|---|---|
n => n + 1 | 0 | 0 + 1 = 1 |
n => n + 1 | 1 | 1 + 1 = 2 |
n => n + 1 | 2 | 2 + 1 = 3 |
리액트는 최종 결과로 3
을 저장하고 useState
에서 이를 반환합니다.
따라서 위의 예시에서 +3
을 클릭하면 값이 3씩 올바르게 증가하는 것입니다.
상태를 바꾼 후에 갱신하면 어떻게 될까
이 이벤트 처리기는 어떨까요? 다음 렌더링에서 number
가 무엇이 될 것 같나요?
jsx
<button onClick={() => {setNumber(number + 5);setNumber(n => n + 1);}}>
jsx
<button onClick={() => {setNumber(number + 5);setNumber(n => n + 1);}}>
이 이벤트 처리기가 리액트에게 지시하는 것은 다음과 같습니다.
setNumber(number + 5)
:number
는0
이므로setNumber(0 + 5)
입니다. 리액트는 대기열에 '5
로 바꾸기'를 추가합니다.setNumber(n => n + 1)
:n => n + 1
은 갱신 함수입니다. 리액트는 대기열에 이 함수를 추가합니다.
다음 렌더링에서 리액트는 다음 상태 대기열을 통과합니다.
대기 중인 갱신 | n | 반환 값 |
---|---|---|
'5 로 바꾸기' | 0 (미사용) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
리액트는 최종 결과로 6
을 저장하고 useState
에서 이를 반환합니다.
setState(5)
는 실제로 setState(n => 5)
처럼 작동하지만 n
은 사용되지 않습니다!
상태를 갱신한 후에 바꾸면 어떻게 될까
예시를 한 가지 더 살펴보겠습니다. 다음 렌더링에서 number
가 무엇이 될 것 같나요?
jsx
<button onClick={() => {setNumber(number + 5);setNumber(n => n + 1);setNumber(42);}}>
jsx
<button onClick={() => {setNumber(number + 5);setNumber(n => n + 1);setNumber(42);}}>
이벤트 처리기를 실행하는 동안 리액트가 위의 코드 줄을 통해 작동하는 방식은 다음과 같습니다.
setNumber(number + 5)
:number
는0
이므로setNumber(0 + 5)
입니다. 리액트는 대기열에 '5
로 바꾸기'를 추가합니다.setNumber(n => n + 1)
:n => n + 1
은 갱신 함수입니다. 리액트는 대기열에 이 함수를 추가합니다.setNumber(42)
: 리액트는 대기열에 '42
로 바꾸기'를 추가합니다.
다음 렌더링에서 리액트는 다음 상태 대기열을 통과합니다.
대기 중인 갱신 | n | 반환 값 |
---|---|---|
'5 로 바꾸기' | 0 (미사용) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
'42 로 바꾸기' | 6 (미사용) | 42 |
리액트는 최종 결과로 42
를 저장하고 useState
에서 이를 반환합니다.
요약하면 setNumber
상태 설정자에 전달할 것을 다음과 같이 생각할 수 있습니다.
- 갱신 함수(예:
n => n + 1
)가 대기열에 추가됩니다. - 다른 값(예: 숫자
5
)은 이미 대기열에 있는 것을 무시하고 대기열에 '5
로 바꾸기'를 추가합니다.
이벤트 처리기가 완료되면 리액트는 리렌더링을 트리거합니다. 리렌더링에서 리액트는 대기열을 처리합니다. 갱신 함수는 렌더링 중에 실행되므로 갱신 함수는 순수해야 하며 결과만 반환해야 합니다. 함수 내부에서 상태를 설정하거나 다른 부작용을 실행하지 마세요. 엄격 모드에서 리액트는 실수를 찾는 데 도움이 되도록, 각 갱신 함수를 두 번 실행합니다. (그러나 두 번째 결과는 버려집니다.)
명명 규칙
갱신 함수에서 인수의 이름은 해당 상태 변수의 첫 글자로 지정하는 것이 일반적입니다.
jsx
setEnabled((e) => !e);setLastName((ln) => ln.reverse());setFriendCount((fc) => fc * 2);
jsx
setEnabled((e) => !e);setLastName((ln) => ln.reverse());setFriendCount((fc) => fc * 2);
자세한 코드를 선호한다면, setEnabled(enabled => !enabled)
와 같이 상태 변수의 전체 이름을 반복하거나, setEnabled(prevEnabled => !prevEnabled)
와 같이 접두사를 사용할 수 있습니다.
요약
- 상태 설정은 기존 렌더링의 변수를 변경하지 않고 새로운 렌더링을 요청합니다.
- 리액트는 이벤트 처리기의 실행이 완료된 후에 상태 갱신을 처리합니다. 이를 일괄 처리라고 합니다.
- 하나의 이벤트에서 일부 상태를 여러 번 갱신하려면
setNumber(n => n + 1)
갱신 함수를 사용할 수 있습니다.