이 시리즈는 "가장 쉬운 웹개발 with Boaz" 님의 [React 까보기 시리즈] 를 기반으로 만들어졌습니다.
useState()
함수를 호출하게 되면 state
와 setState
를 반환받습니다.
우리가 useState
함수를 호출하게 되면 hook
객체를 만들게 됩니다. 이 hook
객체가 1:1로 가지고 있는 queue
객체에 대해서 같이 살펴봐야합니다.
또한, queue
객체는 update
객체를 담고 있습니다.
먼저 mountState
함수를 다시 살펴봅시다.
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
// Flow doesn't know this is non-null, but we do.
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
return [hook.memoizedState, dispatch];
};
여기서 훅을 얻기 위해 mountWorkInProgressHook
함수를 호출하네요.
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
여기서 hook
객체가 보입니다.
useState
를 호출할 때, 당연히 state 값을 가지게 될 것이고, 이것을 관리하는 속성이memoizedState
입니다. 즉, 컴포넌트에 적용된 마지막 상태값입니다.
baseState
와 baseUpdate
속성은 업데이트에 관련된 속성입니다.
next
는 링크드리스트를 위한 속성값입니다. 다음 훅을 연결하는 링크드 리스트의 속성입니다.
queue
는 hook
객체가 가지고 있는 또 다른 queue
객체를 가지게 됩니다.
이렇게 저장된 링크드 리스트는 어디에 저장될까?
fiber
안에 firstWorkInProgressHook
이 할당되고 있었으며 fiber
안에 저장된 firstWorkInProgressHook
이 첫번째 hook이자 링크드 리스트의 head입니다.
해당 코드는 아래와 같았습니다.
const renderedWork: Fiber = (currentlyRenderingFiber: any); renderedWork.memoizedState = firstWorkInProgressHook;
즉, fiber
의 memoizedState
프로퍼티 안에 hook
노드로 구성된 링크드리스트가 저장되어 있던 것입니다.
workInProgress
가 null인 경우는 작업 중인 hook
이 없다는 의미이므로 첫 번째 hook
으로 할당됩니다.
workInProgress
와 firstWorkInProgress
에 차례대로 할당되어 집니다.반대로 workInProgressHook
이 null이 아닌 경우는 작업 중인 hook
이 있다는 뜻이므로 next 속성에 새로운 hook을 연결해줍니다.
workInProgress.next
에 hook
을 할당한 다음에 workInProgress
에 hook
을 할당하여 temp
변수 없이도 연결을 시킬 수 있게 됩니다.그 다음 workInProgressHook
을 반환하게 됩니다. 즉, 내부적으로 생성된 hook
객체를 반환하는 거죠.
이제 다시 mountState
함수로 돌아가봅시다.
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
// Flow doesn't know this is non-null, but we do.
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
return [hook.memoizedState, dispatch];
};
코드를 살펴보면 인자로 받은 initialState
가 함수인 경우, 함수를 실행한 값을 initialState
로 다시 할당해줍니다.
저희가 useState
의 인자로 함수를 넣을 수 있었던 이유가 여기 있었습니다.
그 다음, hook의 memoizedState
, baseState
프로퍼티에 initialState
를 할당하게 됩니다.
최종적으로 render하고 있는 state 값을 memoizedState
에 저장하고 있습니다.
(저희는 일단 mount하였을 때만 가정하므로 초기값이 initialState
로 저장되는 것이죠.)
이 이후에는 mount
구현체가 아닌 update
구현체를 사용하므로 initialState
가 실행되는 일은 없습니다.
그 다음 queue
객체를 봅시다.
훅을 이용하여 컴포넌트 상태를 변경하고자 할 때 업데이트 정보를 담고 있는 update
객체가 생성됩니다. 이 객체는 hook
의 queue
에 저장됩니다.
만약, 한 번의 컴포넌트 호출에서 단일 훅의
setState()
가 여러 번 호출되었다면 매 호출 생성된update
객체는 이queue
에 쌓이게 되는 것입니다. 그 후 컴포넌트가 리렌더링 될 때queue
에 저장되어 있던update
을 차례대로 실행해 최종적으로 적용될 state를 도출하게 됩니다.
먼저 코드를 보면 queue
객체는 hook.queue
에도 할당되고, queue
변수에도 할당되는 것을 볼 수 있습니다.
그리고 queue
객체는 4가지 프로퍼티를 담고 있는데, 그중 last
와 dispatch
에 대해 먼저 보도록 하겠습니다.
먼저 last
에는 마지막 업데이트가 저장되어 있습니다.
queue
객체 안에는 update
객체가 큐의 형태로 저장됩니다.
update
객체는 상태를 업데이트하기 위해 필요한 정보를 가지고 있는 자바스크립트 객체입니다.last
에 update
객체의 마지막을 가지고 있게 됩니다.queue
는 Circular Linked List의 형태로 구현되어 있습니다.dispatch
속성에는 밑에 선언된 dispatch
함수를 할당하게 됩니다.
dispatch
함수는 queue
객체에 update
를 추가할 수 있는 함수를 의미합니다.
코드를 통해 보면 dispatchAction
이라는 함수를 할당하고 있습니다.
bind()
를 통해 currentlyRenderingFiber
(hook
과 매핑되는 컴포넌트의 fiber
)와 queue
를 bind 하고 있습니다. 왜냐하면 dispatch는 우리가 사용하는 setState
의 모습으로 외부로 노출이 되기 때문입니다.예시를 들어 설명하면 좀 더 이해가 잘 될 것이므로, 예시를 들어보겠습니다.
function FunctionComponent() {
const [a, setA] = useState(0) // aHook
const [b, setB] = useState(0) // bHook
setA(_a => _a + 1) // firstUpdate
setA(_a => _a + 1) // secondUpdate
setA(_a => _a + 1) // thirdUpdate
}
FunctionComponent
는 Babel을 통해 React.createElement()
함수가 호출되어 React element
로, VDOM에 올라가야 하므로 fiber
노드로 확장될 것 입니다.
지금까지 분석한 Fiber 아키텍처에 따르면 FunctionComponent
의 fiber
는 아래처럼 되겠죠?
물론 더 많은 속성들이 있긴 합니다...