본 게시글의 코드는 리액트 16버전을 기준으로 하고있습니다.
이 글을 쓰게된 계기
이전 게시글까지 리액트처럼 동작하는 코드를 작성해보았다.
그런데 이전 게시글에서 만든 useState는 한가지 치명적인 문제가 있다.
useState를 쓰는 컴포넌트가 조건부 렌더링이 되는 경우, states배열이 망가진다는 것이다.
const Component1=()=>{
const [inner,setInner]=useState("컴포넌트1입니다!");
return <div>{inner}</div>;
}
const Component2=()=>{
const [inner,setInner]=useState("컴포넌트2입니다!");
return <div>{inner}</div>;
}
const App=()=>{
const [state,setState]=useState(true);
return <div onclick=(()=>setState(false))>{state?<Component1/> : <Component2/></div>
어떻게 하면 이걸 해결할 수 있을까?라는 의문에서 출발하게 되었다.
혼자서 고민을 해보다가 리액트 본래 코드라는 좋은 답안이 있단 생각이 문득 들어 리액트 코드를 파보기로 결심했다.
정말정말정말 어려웠는데, 이 블로그와 이 블로그를 기반으로 설명되어있는 유튜브 영상을 통해 이해할 수 있었다.
useState본체를 찾아보기
packages/react/src/ReactHooks에서 useState의 본체를 찾을 수 있었다.
resolveDispatcher를 통해 dispatcher를 받아오고, 이 객체 내부의 useState를 사용하고 있는 것을 확인할 수 있었다.
그럼 resolveDispatcher를 찾아보자.
마찬가지로 packages/react/src/ReactHooks에 있었다.
ReactCurrentDispatcher 객체의 current 프로퍼티에 해당하는 값을 dispatcher로 return하는 것을 볼 수 있었다.
자 그럼 ReactCurrentDispatcher를 안따라가볼 수가 없다.
같은 파일의 ReactCurrentDispatcher.js파일을 찾을 수 있었다.
? 근데 current는 null이다...
놀랍게도 null에서 useState를 찾아오고 있단 말인가?!
그럴리없다. 어디선가 ReactCurrentDispatcher.current를 결정시켜주는 부분이 있을 것이다.
검색해보자 react-reconciler/src/의 ReactFiberHooks가 나오는 것을 알 수 있었다.
결과가 꽤나 놀랍다.
우리가 보고있던 패키지는 react패키지였다.
그런데 react패키지 내부의 ReactCurrentDispatcher의 current값을 react패키지가 결정해 주는게 아니었던 것이다.
이건 react-reconciler 패키지의 ReactFiberHook이라는 모듈이 결정해주고있었다!
음... 그럼 여기서 두가지 의문점?이라기보다는 모르는 단어가 등장했다.
1. Reconciler란 무엇인가?
2. Fiber란 무엇인가?
이 두가지 단어를 파헤치며, 왜 react패키지가 아니라 react-reconciler에서 useState훅을 주입시켜주고 있는지 확인해보자.
Reconciler
https://ko.legacy.reactjs.org/docs/reconciliation.html#gatsby-focus-wrapper
위 문서를 보면 알겠지만, reconcile이란 기존의 VDOM과 현재의 VDOM을 비교해서 차이점을 찾아내는 것을 말한다.
아하, 그럼 reconciler는 이 일을 해주는 패키지라고 이해할 수 있다!
이전 게시글에서 사용하던 updateDOM과 비슷한 역할을 하는 것이다.
리액트는 reconcile 과정을 둘로 나누어서 구성했다.
이 과정은 아주 유명한 그림을 보면 볼 수 있다. 왼쪽의 빨간 상자를 보자.
Render Phase
VDOM을 조작하는 단계다.
즉, 함수형 컴포넌트가 호출되고 , 그 결과인 객체가 VDOM에 반영되기 까지를 Render Phase라 한다.
reconcile단계 중 Render Phase에서 함수형 컴포넌트가 호출되는 것이다!!
이렇게 했을때의 장점은 무엇일까?
기존의 우리가 작성했던 react의 구동방식은 다음과 같다.
- App컴포넌트를 호출하고 재귀적으로 모든 함수형컴포넌트를 호출하면서 중첩 객체인 reactElement, 즉 VDOM을 얻어낸다.
- useState는 외부 배열로 구성되며, setState가 일어나면 rerender를 일으킨다.
- rerender는 App컴포넌트를 다시 호출하고, 모든 함수형컴포넌트를 재귀적으로 호출한다.
- 결과적으로 얻어진 현재의 VDOM과 과거에 render시 만들어진 VDOM을 비교해서 업데이트한다.
이렇게 했을 때의 문제점이 있다.
일단 rerender함수가 호출되면 매번 App부터 모든 함수형 컴포넌트가 재귀호출된다는 점이다.
하지만, 실제 react에서는 재귀형태로 모든 컴포넌트를 재호출하는게 아니라 reconcile단계에서 하나씩 실행시키기에 이런 문제가 없다.
Commit Phase
Render Phase가 완료된 VDOM을 실제 DOM에 적용하는 단계다.
왜 react패키지가 아니라 reconciler 패키지에서 값을 주입해주고 있는가? 에 대한 결론
이전 게시글에서 작성한 코드는 createElement의 tagName이 함수인 경우 재귀적으로 호출시키면서 전부 중첩객체로 변환해주었었다.
export const createElement = (tagName, props, ...children) => {
if (typeof tagName === 'function') { //함수형 컴포넌트를 재귀적으로 객체로 변환함
return tagName(props, ...children);
}
return {
tagName,
props,
children,
};
};
내코드를 그림으로 표현하면 이렇다.
하지만, 리액트는 이걸 중첩적으로 바로 호출하는 것이 아니라, 일단 두고 reconciler의 Render Phase에서 함수형 컴포넌트를 각각 호출해주는 것이다.
이때 ReactCurrentDispatcher.current에 적절한 useState 함수를 집어넣어줘야 useState를 함수형 컴포넌트에서 사용할 수 있을테니, 이걸 reconciler 패키지에서 주입시켜 주는것이다!
그림으로 표현하면 이렇다.
자 그럼 reconciler에서 훅을 주입하는 이유에 대해서는 정리가 된 것 같다.
Fiber
Fiber는 뭘까?
Fiber는 컴포넌트에 대한 정보를 가진 객체이며, 동시에 스택프레임의 재구성이다.
이 두가지 뜻을 하나씩 뜯어보자.
첫째, Fiber는 컴포넌트에 대한 정보를 가진 객체다.
JSX는 ReactElement로 변환된 뒤, 다시 Fiber로 확장된다.
위의 그림을 다시 그린 최종적 형태는 아래와 같다.
두번째, Fiber는 스택프레임의 재구성이다.
React어플리케이션에서는 설명했듯, 굉장히 많은 일들이 일어나게된다.
또한, 기존의 호출방식은 현재 우리 코드의 호출방식처럼 재귀, 혹은 stack을 이용한 방식이었기에, 컴포넌트가 중첩되면 중첩될수록 중첩호출되는 함수들이 많아졌다.
알다시피 js는 싱글스레드이다. 이렇게 재귀적으로 함수가 계속해서 실행되다보면 콜스택이 깊어지고, 이로인해 일시적인 블로킹 현상이 일어날수 있게된다.
(블로킹에 대한 설명은 이전에 작성한 nodejs 환경에서 블로킹에 대한 글을 참고하면 좋을 것 같다.)
그리고 이것이 가장 치명적으로 나타내는 프론트엔드만의 영역이 있으니...
그것은 바로 애니메이션이다.
이렇게 콜스택이 비워질때까지 기다렸다가 DOM을 업데이트 하고, 애니메이션을 실행시키기 때문에 애니메이션이 뚝뚝 끊기게된다.
이런 문제를 해결하기위해서는 어떻게 해야할까?
간단하게 생각해보면, 애니메이션 작업에 대해서 먼저 처리를 해주면될 것이다.
이를 위해 리액트 팀은 Fiber를 도입한다.
Fiber는 콜스택에 쌓이는 스택을 커스텀한 것이다.
그래서 우선순위가 높은 작업에 대해 먼저 작업을 할 수 있는 것이다.
결론
1. useState는 ReactCurrentDispatcher.current에 주입된다.
2. 이건 Reconciler가 컴포넌트를 호출시키기 때문이다.
3. 근데 그전에 React Element가 Fiber로 확장된다.
4. 다 되면 컴포넌트가 호출되어 적절하게 주입된 useState가 실행된다!
다음 게시글에서는 ReactCurrentDispatcher를 주입하는 renderWithHooks를 좀 더 코드단에서 살펴보자.