여러 State 연속 업데이트
- Published on
- 리액트에서 상태 업데이트를 일괄 처리하며, 동일한 상태 변수를 여러 번 업데이트하려면 업데이터 함수를 사용하는 방법을 이해합니다.
Table of Contents
상태 변수를 설정하면 다음 렌더링이 예약됩니다. 하지만 때로는 다음 렌더링을 예약하기 전에 값을 여러 번 수정하고 싶을 수도 있습니다. 이를 위해 리액트가 상태 업데이트를 어떻게 일괄 처리하는지 이해하는 것이 도움이 됩니다.
배울 내용
- "일괄 처리(batching)"가 무엇이며 리액트가 여러 상태 업데이트를 처리하는 방법
- 같은 상태 변수에 여러 번 업데이트를 적용하는 방법
리액트는 상태 업데이트를 일괄 처리합니다.
"3 추가" 버튼을 클릭하면 카운터가 세 번 증가할 것으로 예상할 수 있습니다. 왜냐하면 setNumber(number + 1)을 세 번 호출하기 때문입니다.
하지만 이전 섹션에서 살펴보았던 것처럼 각 렌더링의 상태 값은 고정되어 있습니다, 따라서 첫 번째 렌더링의 이벤트 핸들러 내부에서 number의 값은 항상 0입니다. setNumber(1)을 몇 번 호출하더라도 그렇습니다.
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
그러나 여기에는 또 다른 요소가 작용합니다. 리액트는 이벤트 핸들러의 모든 코드가 실행된 후에 상태 업데이트를 처리합니다. 이것이 setNumber() 호출 이후에만 재렌더링이 발생하는 이유입니다.
이는 레스토랑에서 웨이터가 주문을 받는 것을 떠올리게 할 수 있습니다. 웨이터는 처음 주문을 받는 순간부터 주방으로 달려가지 않습니다! 대신 주문을 마무리하고 변경할 수 있도록 하고, 심지어 테이블에서 다른 사람들의 주문을 받을 수 있습니다.
이를 통해 여러 개의 상태 변수를(심지어 여러 컴포넌트에서도) 업데이트할 수 있지만, 재렌더링을 일으키지 않고도 많은 수의 상태 업데이트를 처리할 수 있습니다. 하지만 이는 이벤트 핸들러와 그 안의 코드가 완료된 후에야 UI가 업데이트되는 것을 의미합니다. 이러한 동작은 **일괄 처리(batching)**라고도 불리며 리액트 앱을 더 빠르게 실행시키는 데 도움이 됩니다. 또한 일부 변수만이 업데이트된 "미완성" 렌더링으로 처리하는 혼란스러운 상황을 피할 수 있습니다.
리액트는 클릭과 같은 여러 의도적인 이벤트를 일괄 처리하지 않습니다. 각 클릭은 별개로 처리됩니다. 리액트는 안전한 경우에만 일괄 처리를 수행하도록 합니다. 이렇게 함으로써 첫 번째 버튼 클릭이 양식을 비활성화하는 경우 두 번째 클릭이 양식을 다시 제출하지 않도록 합니다.
다음 렌더링 전에 동일한 상태를 여러 번 업데이트하기
이는 일반적으로 사용되지 않는 사용 사례이지만, 다음 렌더링 전에 동일한 상태 변수를 여러 번 업데이트하려면 setNumber(number + 1)
과 같은 다음 상태 값을 전달하는 대신, setNumber(n => n + 1)
과 같이 이전 상태를 기반으로 다음 상태를 계산하는 함수를 전달할 수 있습니다. 이는 리액트에게 "상태 값으로 무언가를 수행하라"고 말하는 방법입니다. 단순히 상태를 대체하는 것이 아니라 상태 값을 계산하는 함수를 전달하는 것입니다.
이제 카운터를 증가시켜보세요:
여기서 n => n + 1
은 **업데이터 함수(updater function)**라고 불립니다. 이를 상태 setter에 전달할 때:
- 리액트는 이 함수를 이벤트 핸들러의 다른 코드가 모두 실행된 후에 처리하기 위해 큐에 추가합니다.
- 다음 렌더링 중에 리액트는 큐를 확인하고 최종 업데이트된 상태를 제공합니다.
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를 업데이트하는 경우 다음 렌더링에서 number의 값이 어떻게 될지 생각해보세요.
<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의 값은 어떻게 될까요?
<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 상태 setter에 전달하는 내용을 다음과 같이 생각할 수 있습니다.
- 업데이터 함수 (예:
n => n + 1
)는 큐에 추가됩니다. - 다른 값 (예: number 5)은 "대체하기"를 큐에 추가하며, 이미 큐에 있는 내용을 무시합니다.
이벤트 핸들러가 완료되면 리액트가 재렌더링을 트리거합니다. 재렌더링 중에 리액트는 큐를 처리합니다. 업데이터 함수는 렌더링 중에 실행되므로 업데이터 함수는 순수 함수이어야 하며 결과만 반환해야 합니다. 업데이터 함수 내부에서 상태를 설정하거나 다른 부작용을 실행하지 않도록 주의해야 합니다. Strict Mode에서는 리액트가 각 업데이터 함수를 두 번 실행하지만 (두 번째 결과는 버림), 실수를 찾는 데 도움이 됩니다.
명명 규칙
일반적으로 업데이터 함수의 인수는 해당 상태 변수의 첫 글자를 사용하여 이름을 지정하는 것이 일반적입니다.
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);
더 상세한 코드를 선호하는 경우 다른 일반적인 관례는 setEnabled(enabled => !enabled)
와 같이 상태 변수 이름을 반복하거나, setEnabled(prevEnabled => !prevEnabled)
와 같이 접두사를 사용하는 것입니다.
요약
- 상태를 설정하면 기존 렌더링 내의 변수가 변경되지 않지만 새로운 렌더링을 요청합니다.
- 리액트는 이벤트 핸들러의 실행이 완료된 후에 상태 업데이트를 처리합니다. 이를 일괄 처리(batching)라고 합니다.
- 한 이벤트에서 여러 번 상태를 업데이트하려면
setNumber(n => n + 1)
과 같은 업데이터 함수를 사용할 수 있습니다.