이 시리즈는 "가장 쉬운 웹개발 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는 아래처럼 되겠죠?
물론 더 많은 속성들이 있긴 합니다...
