사건의 발단
채팅방의 한분께서 위와 같은 질문을 해주셨다.
여러가지 답변이 오갔고 내가 똥답변을 해버렸다.
이때 까지만해도 setState 안의 함수를 넣는 형태인 함수형 업데이트를 사용하면 batching 이 발생하지 않고 setState 마다 리렌더가 발생할 것이라고 생각했었다.
하지만 아래의 코드를 돌려본 결과.
내 생각이 잘못되었다는 것을 알게되었다.
import { useState } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
console.log("render");
return (
<div className="App">
<button
onClick={() => {
setCount((c) => c + 1);
setCount((c) => c + 1);
}}
>
{count}
</button>
</div>
);
}
count가 2씩 증가하고 render 콘솔이 한번만 찍히는 것으로 보아. Batching이 아주 잘 동작하고 있음을 나타낸다.
오케이! 답변을 함으로써 내 지식이 잘못되었다는걸 깨달았어! 오히려 좋아!
설명을 할 수 없다면 아는 것이 아니다.
그런데 두가지에 대한 의문점이 생겼다.
1. 일반적인 setState
import React, { useState } from 'react'
export default function App() {
const [count, setCount] = useState(0)
const plus = () => {
setCount(count + 1) // 0 + 1
setCount(count + 1) // 0 + 1
setCount(count + 1) // 0 + 1 <- 이 코드만 적용됨
console.log(count) // 0
}
return (
<>
<h2>{count}</h2>
<button onClick={plus}>+3</button>
</>
)
}
2. setState 함수 업데이트
import React, { useState } from 'react'
export default function App() {
const [count, setCount] = useState(0)
const plus = () => {
setCount(prev => prev + 1) // 0 + 1
setCount(prev => prev + 1) // 1 + 1
setCount(prev => prev + 1) // 2 + 1
console.log(count) // 0
}
return (
<div className="App">
<h2>{count}</h2>
<button onClick={plus}>+3</button>
</div>
)
}
위의 두가지에 대해 어렴풋이 알고있어 제대로 설명을 할 수 없다는 걸 깨달았다.
파헤쳐 보자.
그럼 이제 저런 동작이 발생하는 근거를 정리해보자.
1. 일반적인 setState
import React, { useState } from 'react'
export default function App() {
const [count, setCount] = useState(0)
const plus = () => {
setCount(count + 1) // 0 + 1
setCount(count + 1) // 0 + 1
setCount(count + 1) // 0 + 1 <- 이 코드만 적용됨
console.log(count) // 0
}
return (
<>
<h2>{count}</h2>
<button onClick={plus}>+3</button>
</>
)
}
- closure에 의해 컴포넌트가 렌더링 될때의 state 와 props 는 고정된다.
- plus 함수가 호출될때의 count 는 모두 0 이다.
- setState 는 각각 업데이트가 되지 않고 batching 을 통해 한번에 업데이트가 이루어진다.
- 오브젝트 컴포지션으로 인해 state 객체가 merge 된다.
React의 state 는 객체로 구현되어있다.
const currentState = {
number: 1
};
const newState = Object.assign(currentState, { number: 1 }, { number: 1 });
setCount(newState)
Object.assign은 같은 키값은 덮어씌우기 때문에 마지막 setState 만 동작하게 되는 것이다.
5. setState의 내부 동작 (다음 내용은 해당 블로그에서 발췌한 내용입니다.)
5-1. update가 일어나기 전 hook의 상태 (setCount 호출 전)
{
memoizedState: 0,
baseState: 0,
queue: {
last: null,
dispatch: dispatchAction.bind(bull, currenctlyRenderingFiber$1, queue),
lastRenderedReducer: basicStateReducer(state, action),
lastRenderedState: 0,
},
baseUpdate: null,
next: null
}
baseState가 0 이라는 것을 알 수 있다.
5-2. update가 일어나기 전 hook의 상태 (setCount 호출 전)
last에는 setCount를 통해 넘어온 액션과, React의 Batching Process를 통해 최종적으로 업데이트될 상태를 담고 있는 eagerState 변수, 그리고 action으로부터 eagerState를 계산하는 eagerReducer의 값이 세팅된다. 또한 basicStateReducer 는 후에 함수 업데이트 과정에서 한번 더 언급되기 때문에 한번 더 살펴주면 좋다.
{
memoizedState: 0,
baseState: 0,
queue: {
last: {
expirationTime: 1073741823,
suspenseConfig: null,
action: 1, // setCount를 통해 설정한 값.
eagerReducer: basicStateReducer(state, action),
eagerState: 1, // 실제로 상태 업데이트를 마치고 렌더링되는 값.
next: { /* ... */},
priority: 98
},
dispatch: dispatchAction.bind(bull, currenctlyRenderingFiber$1, queue),
lastRenderedReducer: basicStateReducer(state, action),
lastRenderedState: 0,
},
baseUpdate: null,
next: null
}
위와 같은 과정을 통하여 결국 +3 이 아닌 +1이 된다는 것을 알 수 있다.
2. setState 함수 업데이트
그렇다면 함수 업데이트에서는 어떻게 동작하길래 변경 state 값을 이용할 수 있었던 걸까?
import React, { useState } from 'react'
export default function App() {
const [count, setCount] = useState(0)
const plus = () => {
setCount(prev => prev + 1) // 0 + 1
setCount(prev => prev + 1) // 1 + 1
setCount(prev => prev + 1) // 2 + 1
console.log(count) // 0
}
return (
<div className="App">
<h2>{count}</h2>
<button onClick={plus}>+3</button>
</div>
)
}
- closure에 의해 컴포넌트가 렌더링 될때의 state 와 props 는 고정된다.
- plus 함수가 호출될때의 count 는 모두 0 이다.
- setState 는 각각 업데이트가 되지 않고 batching 을 통해 한번에 업데이트가 이루어진다.
- 하지만 이번에는 함수를 콜백으로 넣어주었기 때문에 객체의 composition 이 발생하지 않는다.
따라서 호출된 순서대로 setState 를 큐에 넣는다. - 큐에 넣어진 setState가 실행되는 과정 (자세한 내용은 해당 블로그를 참고해 주세요.)
아까 언급드렸던 basicStateReducer 의 코드를 보도록하자.
action 이 함수일때와 그렇지 않은 경우가 분기처리 된 것을 알 수 있다.
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
따라서 일반적인 setState에서는 다음과 같이 동작한다. action 의 type 이 함수가 아니기 때문에 기존 state 에 +1 을 하게 되는 것이다.
last: {
...other options // 필요한 부분만 남겨놓고 생략하였음.
action: count + 1,
eagerReducer: basicStateReducer(state, action),
eagerState: count + 1,
next: {
last: {
... otherOptions,
action: count + 1,
eagerReducer: basicStateReducer(state, action),
eagerState: count + 1,
next: null
}
}
},
그렇다면 함수 업데이트에서는 어떻게 동작할까?
다음처럼 큐에 들어있던 함수들이 실행되게 된다.
last: {
...other options // 필요한 부분만 남겨놓고 생략하였음.
action: count => count + 1,
eagerReducer: basicStateReducer(state, action),
eagerState: count + 1,
next: {
last: {
... otherOptions,
action: count => count + 1,
eagerReducer: basicStateReducer(state, action),
eagerState: (count + 1) + 1,
next: null
}
}
},
위 예시대로 생각해본다면 eagerState 는 ((count + 1) + 1) +1 이 될 것 이다.
이러한 이유로 인해 state 는 +3이 된다.
참고자료
https://overreacted.io/ko/react-as-a-ui-runtime/
https://overreacted.io/ko/a-complete-guide-to-useeffect/
https://overreacted.io/how-are-function-components-different-from-classes/
https://velog.io/@ja960508/setState%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%84%B1
https://yeoulcoding.tistory.com/169?category=806488
https://velog.io/@tastestar/state-batch-update
https://www.youtube.com/watch?v=hSdVDBPTT0U&t=333s
https://usecode.pw/functional-set-state-is-the-future-of-react/
'공부기록 > 웹 개발' 카테고리의 다른 글
Glitch 에 json-server 배포한 과정 (2) | 2023.03.10 |
---|---|
[React Query] staleTime vs cacheTime (2) | 2023.01.26 |
[우아한 테크코스 프리코스] 1차 탈락 (4) | 2022.12.15 |
[우아한 테크코스 프리코스] 4주차 (0) | 2022.11.30 |
[우아한 테크코스 프리코스] 3주차 (0) | 2022.11.15 |