useState, setState의 동작원리(react 18)

useState를 사용할 때 최초에 initial state의 값을 할당한 후 setState를 사용하여 값을 업데이트 하게 되면 항상 리렌더링은 일어날까?
아니다.
처음에는 '왜?'라는 생각이 잠시 들었지만 조금 더 깊이 생각해보면 아니라는 것을 알 수 있다.
만약 사용자가 같은 값을 계속해서 주입시키는데도 렌더링이 반복적으로 일어난다면 얼마나 쓸모없는 연산이 이뤄지겠는가.
그렇다면 어떻게 가능할까?
먼저 useState와 setState의 원리를 알아야한다.
react의 원리에 대해 알아보는 강의와 문서들을 참고하고 react 18버전에는 어떻게 반영되어있는지 확인해봤다.
우리가 useState를 사용하려고 react에서 import하게 되고 이 useState가 어디서 왔는지 어디서 import해왔는지 확인해보면,

useState를 살펴보면 initialState로 초기상태 값이나 초기상태 값을 생성하는 함수를 받는다.
initialState와 setState를 배열에 담아 return한다.

Hook들을 관리하는곳을 살펴보면 useState는 mountState와 관련있는 것으로 보인다.
(https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js)
useState와 관련이 있는 mountState, mountStateImpl, setState와 관련이 있는 dispatchState, enqueueConcurrentUpdateHook에 대해 알아보자.
1. mountState, mountStateImpl

mountState는 initialState를 인자로 받으며 initialState를 mountStateImpl함수에 인자로 넘겨 hook에 할당한다.
hook의 queue를 queue에 할당하며 queue.dispatch에는 dispatch가 할당되는데 이때 dispatch는 dispatchSetState함수이다.
dispatchSetState는 bind를 하고 있는데 currentlyRenderingFiber(지금 작업중인 fiber)와 queue를 binding해주는 것을 알 수 있다.
moutState가 dispactch를 return하고 있는 것이 보이며 dispatch는 setState이다.

mountStateImpl는 initialState를 hook의 baseState, memoizeState에 할당한다.
queue라는 객체를 만들고 업데이트가 보류 중인지(pending), 렌더링이 어떤 레인(lane)에 속하는지, 디스패치(dispatch) 함수가 무엇인지 등을 추적한다.
lastRenderedReducer는 상태 업데이트를 처리하는 리듀서 함수를 나타내며, lastRenderedState는 마지막으로 렌더링된 상태를 나타낸다. 초기 상태를 여기에 할당한다.

workInProgressHook이 null이면(현재 작업중인 훅이 없는 경우) currentlyRenderingFiber의 memoizeState에 hook을 할당하고 그렇지 않으면 workInProgressHook의 다음 주소값으로 hook을 할당한 뒤 return한다.

basicStateReducer는 state와 acton을 인자로 받고 action이 함수이면 state를 인자로 넣어 action을 실행하고 아니면 action을 return 한다.
2. dispatchSetState
(https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js)
dispatchSetState 함수는 총 4단계로 이루어진다(강의에서는 dispatchAction으로 나온다).
1. 업데이트 객체 생성(업데이트에 대한 정보를 가지고 있음.)
2. 업데이트 객체를 queue에 저장
3. 불필요한 렌더링이 발생하지 않도록 최적화 함.
4. 업데이트를 적용하기 위해 Work를 Scheduler에 예약(Scheduling)

** Work : reconciler가 컴포넌트의 변경을 DOM에 적용하기 위해 수행하는 일.
update객체에 있는 값들을 알아보자. action은 setState의 인자값.
next는 update객체를 linked list(circular linked list)로 연결할때 다음 reference값을 넣기 위해 사용.
eagerReducer, eagerState는 불필요한 렌더링을 하지 않게 만들어주는 것과 연관이 있음.
18버전 이전에는 fiber의 expirationTime의 유무에 따라 queue가 비어져있는지 확인했다면 18버전에서는 fiber의 lanes 값 유무에 따라 확인함.
isRenderPhaseUpdate이 false이면(else block) idle상태(일을 안하는 상태, 즉 기본상태)이다.
lastRenderReducer에 lastRenderState와 action을 인자값으로 넣어 return값을 eargerState에 할당한다.
update객체의 hasEagerState에는 true를 할당한다. 이때 eargerState와 currentState가 같은 경우 리렌더링이 일어나지 않으며 이후 필요한 상황이 생길지 모르니 queue에 넣어두는enqueueConcurrentHookUpdateAndEagerlyBailout이라는 함수를 실행시킨다.

requestUpdateLane는 동시모드가 아니면 동기레인을 반환하고동시모드는 아니지만 렌더링 커넥스트에서 실행중이고 현재 렌더링 중인 레인이 있는 경우 pickArbitraryLane을 실행시켜 return한다.
render phase의 update가 과거에는 하나의 스레드로 간주하여 처리했고 동일한 렌더링 사이클 내에서 나중에 호출된 setState가 해당 컴포넌트를 즉시 다시 렌더링 할 수 있도록 도왔지만 이제는 이 패턴을 지원하지 않는다고 한다.

isRenderPhaseUpdate는 fiber의 alternate의 값이 null이 아니고 현재 렌더링 되는 fiber와 동일하거나 fiber와 현재렌더링 되는 fiber가 동일하다면 true, 아니면 false를 return한다.
true일 경우 enqueueRenderPhaseUpdate함수를 호출한다.

enqueueRenderPhaseUpdate는 렌더링 단계 업데이트를 큐에 추가한다.
pending이 null이면 update.next에 update를 할당하여 circular linked list를 만든다.

enqueueConcurrentHookUpdateAndEagerlyBailout은 리렌더링은 할 필요가 없지만 이후에 더 높은 우선순위의 업데이트가 발생하여 이 업데이트가 rebase되어야 할 경우를 대비하여 업데이트를 큐에 넣는다.

enqueueConcurrentHookUpdate는 fiber, queue, update, lane을 인자로 받아 enqueueUpdate를 호출하여 hook update를 queue에 추가하고 getRootForUpdatedFiber를 호출해 업데이트된 fiber의 root를 가져와 반환한다.
enqueueUpdate는 fiber에 대한 업데이트를 queue에 추가하는 역할을 한다. 현재 렌더링 중인지 확인하고 queue에 fiber, update, lane을 추가한다.
업데이트 된 레인을 병합하고 파이버와 동시성 fiber lane도 병합한다.
getRootForUpdatedFiber는 업데이트된 fiber에 대한 루트를 가져오는데 사용되고 무한루프가 있을 경우 감지해 예외를 발생시킨다.
sourceFiber를 받아서 업데이트 된 루트를 반환한다.
detectUpdateOnUnmountedFiber 함수를 호출하여 언마운트된 fiber에 업데이트가 있는지 확인하고 있다면 경고를 하는 역할을 한다(개발 환경에서만).
첫번째 실행에는 첫번째 인자로 넘겨진 sourceFiber는 검사중인 fiber이며 두번째 인자로 넘겨진 sourceFiber는 부모 fiber이다. fiber가 언마운트된 fiber인지 확인한다.
두번째 호출에서는 fiber tree를 거슬러 올라가며 업데이트 여부를 확인한다. 이때 sourceFiber는 검사중인 fiber를 가리키며 함수 내에서 업데이트가 있는지 확인하는데 사용된다. fiber와 그 부모 fiber를 통해 업데이트가 있는지 확인한다.
반환경로를 따라가며 업데이트를 확인하고 hostRoot인 경우에 해당 루트를 반환한다.


detectUpdateOnUnmountedFiber는 sourceFiber와 parent를 인자로 받으며 언마운트된 fiber에 대한 업데이트가 있는지 검사한다. 여기서 인자로 받은 sourceFiber는 검사중인 fiber를 나타낸다.
scheduleUpdateOnFiber함수는 업데이트를 예약하고 관리하는데 사용된다.
작업루프가 현재 일시중단되어 데이터 로딩을 기다리고 있는지 확인한 후, 현재 시도를 중단하고 상위로부터 다시 시작한다.
markRootUpdated(root, lane)은 루트에 업데이트가 예약되었음을 표시한다.
렌더링 컨텍스트에서 실행중이고 현재 작업중인 루트와 일치할 경우, 렌더링 단계 업데이트를 추적하기 위해 workInProgressRootRenderPhaseUpdatedLanes에 레인을 추가한다.
그렇지 않은 경우 일반적인 업데이트로 간주하고 enableUpdaterTracking이 활성화되어 있고 개발도구가 존재하는 경우 레인 맵에 파이버와 레인을 추가한다.
업데이트가 렌더링 중인 트리로 전달된 경우, 현재 렌더링 중에 중첩된 업데이트 작업이 있음을 표시한다.
동기 레인 & concurrent mode가 아닌 경우 동기적으로 작업을 플러시 한다.
참고자료
https://goidle.github.io/
https://www.youtube.com/watch?v=wGCZHxKEo28
https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js