업무를 하다보면 setState(state + 1)과 setState(prev => prev + 1)의 차이점에 대해 느끼게 되었는데, 문득 useState의 내부 구조와 동작원리에 대해 궁금해 깊게 공부해보고자 한다.
useState가 return하는 배열의 첫번째 인자로, readOnly
변수이다.
내부를 자세히 들여다보면 클로저 구조로 되어있어 state값은 단순히 할당으로 변경 할 수 없다.
반드시 내부의 setState 함수로 변경 가능하다.
setState는 컴포넌트를 렌더링한다.
따라서 내부에 render()
함수가 있다.
그런데 만약 setState를 아래처럼 연속적으로 실행하면 실행 횟수만큼 렌더링이 될까?
아니다. 왜냐하면 render함수는 throttle
이 걸려있기때문이다.
하나의 queue에 묶어서 처리하기때문이다
변수일때 : 리액트는 퍼포먼스 향상을 위해 특별한 배치 프로세스를 사용하기 때문에
여러 setState 업데이트를 한 번에 묶어서 처리한 후 마지막 값을 통해 state를 결정하는 방식입니다.
함수일때 : 새로운 상태가 바로 이전 상태를 통해 계산되어야 하면 함수
를 써야 합니다.
const [state1, setState1] = useState(0);
const [state2, setState2] = useState(0);
const increment01 = () => {
setState1(state + 1);
setState1(state + 1);
setState1(state + 1);
console.log(state1)
}
const increment02= () => {
setState2(prev => prev + 1);
setState2(prev => prev + 1);
setState2(prev => prev + 1);
console.log(state2)
}
increment01() // 1
increment02() // 3
{
memoizedState: 0, // first hook
baseState: 0,
queue: { /* ... */ },
baseUpdate: null,
next: { // second hook
memoizedState: false,
baseState: false,
queue: { /* ... */ },
baseUpdate: null,
next: { // third hook
memoizedState: {
tag: 192,
create: () => {},
destory: undefined,
deps: [0, false],
next: { /* ... */ }
},
baseState: null,
queue: null,
baseUpdate: null,
next: null
}
}
}
next는 연결 리스트의 일종으로, 한 컴포넌트 안에서 여러 번의 실행되는 hook들을 연결해주는 역할을 합니다.
{
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
}
리액트의 배치 프로세스는 이렇게 묶인 hook들을 한 번에 처리한 뒤 last를 생성합니다.
여기서 주목할 부분은 최종 반환될 상태인 eagerState를 계산하는 함수가 Reducer라는 것입니다.