리액트에서 리덕트로 넘어가기전, state관리, props drilling에 대해 적용하다가보니 넘어가지 않는 부분이 생겼습니다.
state 값을 변경하는 가운데, state를 여러개 입력하게 되었고 값이 넘어가지않아 이 문제에 대해 찾아보니 이를 state batch update 불린다는 걸 알게되었습니다.
const increase1 = () => {
setCount(count+1)
setCount(count+1)
setCount(count+1)
}
const increase2 = () => {
setCount(count => count+1)
setCount(count => count+1)
setCount(count => count+1)
}
라는게 있을 경우, count에 3이 더해지는게 아니라, 첫번째에는 1이 더해지고 두번째에는 3이 더해집니다.
왜그렇냐하면 리액트에 state bath update 때문입니다.
리액트는 업데이트 되어야할 항목을 각각 업데이트 하는 게 아니라 batch update(일괄 업데이트)를 통해 렌더링 횟수를 최소화합니다. 즉 불필요한 렌더링을 방지하기 위해서입니다
const increase1 = () => {
setCount(count+1)
setCount(count+1)
setCount(count+1)
}
위와 같은 경우, 세 번의 리렌더링이 이루어지는게 아니라 하나의 이벤트 핸들러 안에 있는 동기적 상태 업데이트는 단 한번만 진행되기 떄문에 마지막 setCount(count+1)만 진행되어 1만 더해집니다.
이러한 걸 함수형 인자를 통해 접근할 수 있는 방법도 제공하는 것입니다.
const increase2 = () => {
setCount(count => count+1)
setCount(count => count+1)
setCount(count => count+1)
}
찾다보니, state batch process를 이해하기 위해서는 Hook의 내부가 어떻게 돌아가는지 이해할 필요가 있었습니다. hook의 내부를 보고나면 useReducer에서 왜 action 객체를 쓰는지도 이해할 수 있었습니다.
import { useState } from "react"
리액트를 만들면 맨 위에 있는 코드입니다. 이 말은 'react'라는 모듈에서 useState를 import해서 사용하고 있습니다.
node_modules/react/cjs/react.development.js 에 들어가보면 hooks 함수가 선언된 곳을 볼 수 있습니다.
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState)
}
각 hook이 이렇게 되어있는 걸 볼 수 있습니다.
useState가 dispatch라는 인스턴스를 생성하고, 인자로 초기값을 받고 dispatcher.useState에 전달후 반환값을 리턴한다는 말입니다.
그럼 resolveDispatcher은 어떤 값을 받아올까요? 같은 모듈에 있습니다.
dispatch에 ReactCurrentDispatcher.current 값이 들어감을 볼 수 있습니다.
ReactCurrentDispatcher는 전역에 선언되었다는 말입니다. 그리고 current가 담긴 객체가 있습니다. 이 current에 dispatcher가 담길 장소입니다.
ReactCurrnetDispatcher가 우선 current가 담긴 객체가 있는 곳인걸 알게되었습니다.
이제는 다른 곳을 봐야하는 데 같은 모듈에 ReactSharedInternals 입니다.
`
var ReactSharedInternals = {
ReactCurrentDispatcher : ReactCurrentDispatcher,
....
};
ReactSharedInternals
는 모든 패키지를 가지고 있는 폴더의 역할을 합니다.
각각은 외부에서 데이터가 들어오기를 기다리고 있습니다.
이제 데이터가 저장되는 곳을 알았으니 어디로 들어오는지를 찾아야합니다!
react-dom이 react에 있는 ReactSharedInternals 객체를 변경합니다.
react-dom
은 component가 mount되거나 update될 때 ReactSharedInternals에 있는 hooks 함수를 잉해 react-dom에 전역변수에 접근할수 있고, component가 업데이트되어도 전역변수에 값을 가져다 씁니다.
왜 이곳으로 왔냐면,
ReactCurrentDispatcher의 current에 값이 드디어 들어가게됩니다.
어떤 값이 들어가냐면 HooksDispatcherOnMountInDev입니다. Hook 과 관련된 것들이 여기에 들어가있습니다.
useState()가 안에 있으니
return mountState(initialStae) 값이 보일 겁니다.
다음은 mountState를 살펴보겠습니다
useState() 를 호출할 경우 memoizedState는 null 이므로 mountState()를 구현합니다.
mountState() 코드를 보면 hook을 선언하고 mountWorkInprogressHook() 결과값을 넣는 걸 볼 수 있습니다.
중간부분에 보면 currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
hook을 workInProgressHook에 넣고 이걸 또 currentlyRenderingFiber$1.memoizedState 넣어줍니다.
여기서 currentlyRenderingFiber$1은 전역변수 입니다.
hook들은 fiber의 memoizeState에 likned list형태로 저장되는 것도 알려줍니다.
연결 리스트
리액트는 많은 곳에서 built-in collection 대신 연결 리스트를 이용하여 구현하였습니다. 지금까지 확인한 것만 짚어 보자면 fiber 그 자체가 effect 연결 리스트의 노드였으며, 컴포넌트와 여러 훅을 매핑 시키기 위해, 하나의 훅이 여러 번 호출될 때 정보를 저장하기 위한 queue 등이 있었습니다.
built-in collection이 아닌 연결리스트를 사용하여 구현한 이유는 리스트 탐색 흐름 제어나 노드 삭제 등 조작이 쉽고 리스트 병합에 많은 리소스가 필요하지 않는다는 것입니다. 또한, 랜덤 액세스가 필요한 부분이 없으므로 더욱이 연결리스트를 쓰지 않을 이유가 없습니다.
훅 객체를 생성했으니 mountState를 보겠습니다.
function mountState(initialState) {
var hook = mountWorkInProgressHook(); // 훅 객체를 생성
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = hook.queue = {
pending: null, //
dispatch: null, //push 함수 //여기에 밑에 dispatch가 들어갑니다.
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
위 코드에서 주목해야할 부분은 dispatch 함수입니다.
dispatch
를 보면 bind()를 통해 인자를 잡아둔 상태로 외부에 노출시키고 있습니다. dispatch는 홀로 외부로 노출되기 때문에 필요한 인자들을 잡아둘 필요가 있습니다. 그 중 하나는 훅과 매핑되는 컴포넌트의 fiber이며 다른 하나는 자신의 존재 목적인 queue입니다.
dispatchAction가 뭔지 살펴보겠습니다.
dispatchAction이 하는일은
1.사용자의 업데이트 정보를 담는 update 객체를 만듭니다.
2.update를 queue에 저장합니다.
3.불필요한 렌더링이 발생하지않도록 최적화한다.
매우 긴 코드가 있지만 특정 부분만 보겠습니다.
function dispatchAction(fiber, queue, action) // action에 setState(조건) 조건이 들어가게됩니다.
var update = {
lane: lane,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
var pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
useState를 호출하면 dispatchAction.bind(null, currentlyRenderingFiber$1, queue)
ReactCurrentDispatcher에 새로운 값이 들어옴을 또한 볼 수 있습니다.
컴포넌트 상태를 변경하고자 할 때 업데이트 정보를 담고있는 update 객체가 생성됩니다. 이 객체는 훅의 queue에 저장됩니다. 한 개의 컴포넌트 호출에서 하나의 훅에서 setState()가 여러번 호출되었다면 매 호출 생성된 update 객체는 이 queue에 쌓이고, 컴포넌트가 리렌더링 될 때 queue 저장되어있던 update를 차례대로 실행해 최종적으로 적용될 state를 도출하게 됩니다.
상태변경, setState함수의 호출이 일어나면
1.queue에 last 값이 할당
dispatch에는 setState()를 통해 넘어온 액션과 React의 Batching Process를 통해 최종적으로 업데이트될 상태를 담고있는 eagerState 변수, action으로부터 eagerState를 계산하는 eagerReducer의 값이 세팅됩니다.
사용자가 넘긴 action 으로 Batch Process 이후 최종 반환될 상태인 eagerState를 계산하는 함수는 Reducer입니다.
이 Reducer에 넘기는 action은 함수일 경우 이전 상태를 파라미터로 넘겨주어 함수를 싱행한 값을 리턴하고, 값일 경우 그냥 값을 리턴합니다.
actio 함수에 넣어주면 update시에 리듀서가 함수를 이용해서 eagerState를 계산하고 다음 update로 넘어가고 지속적으로 값이 업데이트 될 수 있습니다.
hook.queue = {
pending: null, //
dispatch: {
/*...*/
}//여기에 dispatchAction에서 전달된 값이 들어갑니다.
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
dispatchAction을 디테일하게 다시 볼수 있는 기회가 생기면 , 더 보충하도록 하겠습니다.
생각없이 쓰는 useState() 였는데 하나의 기능을 구현하기 위해서 얼마나 많은 코드를 써야하는지 느끼게 되었습니다. 마지막까지와서 어디에 값이 들어가고 저장되었다가 선언되면 리렌더링되고하는 걸 알수는있겠는데 설명하기가 어렵네요... 좀더 공부하겠습니다.
훅은 hooks 배열에 자신의 데이터를 추가합니다.
렌더링 과정에서 하나의 컴포넌트를 처리하는 함수입니다.
hooks를 빈배열로 초기화합니다.
컴포넌트 내부에서 훅을 사용한 만큼 hooks 배열에 데이터가 추가됩니다.
생성된 배열을 저장하고 hooks변수를 초기화합니다.
https://yeoulcoding.tistory.com/169?category=806488