// JSX 엘리먼트
const element = <div className="container">Hello</div>;
// 변환 후 - React.createElement() 호출 ( _jsx as v17)
const element = React.createElement("div", { className: "container" }, "Hello");
- JS엔진은 JSX문법을 모른다, 따라서 빌드타임에 babel은 JSX element를 React.createElement() 호출로 트랜스파일한다. ( v17에서는 _jsx() 호출로 개선되었지만, 우선 createElement호출로 생각해보자 )
- createElement호출은 JSX엘리먼트를 기반으로 한 React Element 반환한다.
// React Element의 기본 구조
const element = {
// XSS 공격 방지를 위한 식별자. JSON으로 직렬화할 수 없는 Symbol 타입을 사용하여
// 서버에서 악의적인 JSON이 주입되는 것을 방지
$$typeof: Symbol.for('react.element'),
// 요소의 타입. HTML 태그면 문자열('div', 'span' 등),
// React 컴포넌트면 함수나 클래스가 들어감
type: 'div',
// React가 리스트를 렌더링할 때 변경사항을 식별하기 위한 고유값.
// 형제 요소들 사이에서 변경/추가/제거를 추적하는데 사용
key: null,
// DOM 노드나 React 컴포넼트 인스턴스에 접근하기 위한 참조값
ref: null,
// 해당 엘리먼트의 모든 속성값들을 포함하는 객체.
// children은 자식 엘리먼트들을 포함
props: {
children: 'Hello World', (자식요소가 여러개일 경우 배열 형태로 존재)
className: 'greeting'
},
// 이 엘리먼트를 생성한 React 컴포넌트를 가리킴.
// 주로 개발 도구나 에러 처리에 사용
_owner: null
};
// Fiber 노드의 기본 구조
const fiber = {
// 태그 - 파이버의 종류를 식별 (함수형/클래스형 컴포넌트, 호스트 컴포넌트 등)
tag: WorkTag,
// 해당 엘리먼트의 고유 식별자
key: null | string,
// 요소의 타입 (div, span, 컴포넌트 함수/클래스 등)
elementType: any,
// DOM 노드나 컴포넌트 인스턴스와 같은 실제 노드를 참조
stateNode: any,
// Fiber 트리 구조를 구성하는 참조 링크들
return: Fiber | null, // 부모 Fiber
child: Fiber | null, // 첫번째 자식 Fiber
sibling: Fiber | null, // 다음 형제 Fiber
index: number, // 형제들 사이에서의 인덱스
// 작업 관련 정보
pendingProps: any, // 처리해야 할 새로운 props
memoizedProps: any, // 이전에 처리된 props
updateQueue: Queue, // 상태 업데이트 큐
memoizedState: any, // 이전 상태 값
// 사이드 이펙트 관련
flags: Flags, // 수행해야 할 작업 종류 (생성,수정,삭제 등)
subtreeFlags: Flags, // 자식들의 사이드 이펙트 플래그
deletions: Array<Fiber> | null, // 삭제될 자식들
// 작업 우선순위 관련
lanes: Lanes, // 이 Fiber의 우선순위
childLanes: Lanes, // 자식들의 우선순위
// 대체 Fiber (더블 버퍼링용)
alternate: Fiber | null, // workInProgress <-> current 전환용
};
// React 17 이전 (Legacy)
ReactDOM.render(<App />, document.getElementById("root"));
// React 18 이후 (New) // 동시
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
createRoot ( ReactDOM.render → createRoot().render로 변경 )
- concurrent 렌더링 모드 도입을 위해 render메서드 호출에 변경이 발생함.
createRoot.render() 호출이 갖는 의미 ?
- App컴포넌트를 시작으로 전체 컴포넌트 트리를 Fiber Tree (VDOM 가상돔)으로 만드는 진입점임.
- App컴포넌트의 렌더는, 전체 컴포넌트 트리를 파이버트리로 구성하는 작업
- 파이버트리는 current, workInProgress 파이버 트리로 구성
- workInProgress는 current를 자가복제한 파이버트리이다.
- 더블 버퍼링구조를 통해 현재 보여주고있는 UI를 건드리지 않고, 백그라운드에서 workInProgress 파이버트리를 완성하여, 한번에 commit하여 브라우저에 페인팅하므로써 UX를 개선하기 위한 목적임.
리액트는 초기 파이버노드 트리를 기반으로 이를 재귀적으로 탐색하며
해당 재귀탐색 과정에서 Fiber트리는 상태와 hook 관련 정보를 주입받게됨
- ReactFiberWorkLoop.js
- renderRootConcurrent
- workLoopConcurrent
- performUnitOfWork
- beginWork(), completeWork(), commitWork()
- performUnitOfWork
- workLoopConcurrent
- renderRootConcurrent
( ⇒ 파이버객체를 재귀적으로 탐색하며 , beginWork(), completeWork() 호출 반복 → 탐색 완료시 commitWork() )
Fiber트리 구조
A1
├── B1
├── B2
│ └── C1
│ ├── D1
│ └── D2
└── B3
- A1 beginWork
- B1 beginWork
- B1 completeWork (자식 없음)
- B2 beginWork
- C1 beginWork
- D1 beginWork
- D1 completeWork (자식 없음)
- D2 beginWork
- D2 completeWork (자식 없음, 형제 없음)
- C1 completeWork (D1, D2 처리 완료)
- B2 completeWork (C1 처리 완료)
- 각 파이버 노드를 탐색하며 한번씩 beginWork 호출한다.
- 더 이상 탐색할 자식이 없으면 completeWork를 호출한다.
- 모든 트리를 탐색했다면 commitWork 호출한다.
- beginWork > updateFunctionComponent > renderWithHooks ( ReactFiberHooks.js) // fiber에 주입될 hook을 결정하는 핵심코드
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress; (중요! mountWorkInProgressHoook에서 사용됨 )
...
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// The following should have already been reset
// currentHook = null;
// workInProgressHook = null;
// didScheduleRenderPhaseUpdate = false;
// localIdCounter = 0;
// thenableIndexCounter = 0;
// thenableState = null;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because memoizedState === null.
// This is tricky because it's valid for certain types of components (e.g. React.lazy)
// Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used.
// Non-stateful hooks (e.g. context) don't get added to memoizedState,
// so memoizedState would be null during updates and mounts.
if (__DEV__) {
if (current !== null && current.memoizedState !== null) {
ReactSharedInternals.H = HooksDispatcherOnUpdateInDEV;
} else if (hookTypesDev !== null) {
// This dispatcher handles an edge case where a component is updating,
// but no stateful hooks have been used.
// We want to match the production code behavior (which will use HooksDispatcherOnMount),
// but with the extra DEV validation to ensure hooks ordering hasn't changed.
// This dispatcher does that.
ReactSharedInternals.H = HooksDispatcherOnMountWithHookTypesInDEV;
} else {
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
}
} **else {
ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}**
핵심코드
ReactSharedInternals.H = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate;
**ReactSharedInternals.H
**에
current Fiber가 null이거나 , memoizedState가 null이면 HooksDispatcherOnMount
객체를 할당하고, 그렇지 않으면(:이미 current Fiber가존재한다면) HooksDispatcherOnUpdate
객체를 할당한다.
ReactSharedInternals
는 리액트의 내부 모듈 중 하나로, 리액트의 여러 패키지나 컴포넌트들이 공유해야 하는 내부 기능과 객체를 담고 있는 모듈
**>shared/ReactSharedInternals.js**
import * as React from 'react';
const ReactSharedInternals =
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
export default ReactSharedInternals;
client와 serverside에서 사용되는 ReactSharedInternal 전역객체를 구분한 것으로 보이며,
우리가 현재 확인해야할 client사이드 부분에서는 아래와 같이 정의된다.
**>react/src/ReactSharedInternalsClient**
const ReactSharedInternals: SharedStateClient = ({
**H: null, // Hook 디스패처 (초기값 null)**
A: null, // 비동기 캐시 (초기값 null)
T: null, // 트랜지션 설정 (초기값 null)
S: null, // 트랜지션 콜백 (초기값 null)
}: any);
ReactSharedInternals 객체를 확인했다면 useState
훅 정의를 살펴보자.
useState훅은 아래의 패키지에서 export하고 있고, resolveDispatcher()가 반환한 dispatcher의 useState프로퍼티를 반환한다. (우리가 호출할 때 넣어준 initialState인자를 전달하며)
***>React(Core) 패키지의 ReactHooks.js***
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
useState내부의 resolveDispatcher는 다음과 같다.
***>React Core 패키지의 ReactHooks.js***
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
if (__DEV__) {
if (dispatcher === null) {
console.error(
"Invalid hook call. Hooks can only be called inside of the body of a function component..."
);
}
}
return dispatcher;
}
자 이제 실마리가 풀렸다.
useState훅의 resolveDispatcher 함수는 ReactSharedInternals 객체의 H라는 프로퍼티를 반환한다.
즉 useState훅 반환값은 ReactSharedInternals.H.useState인 것이다.
**ReactSharedInternals.H =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;**
renderWithHooks 내부의 위 코드에서 확인할 수 있었듯이,
ReactSharedInternals.H는 Fiber객체 생성 프로세스에서
Fiber객체가 mount 컨텍스트인지, update 컨텍스트인지에 따라 동적으로 결정된다.
- Fiber가 mount 될 때(current Fiber가 없을 때) : HooksDispatcherOnMount
- Fiber가 update될 때(current Fiber가 존재할 때): HooksDispatcherOnUpdate
그렇다면 Fiber가 mount될 때의 경우인 HooksDispatcherOnMount를 확인해보자.
**> react-reconciler > src > ReactFiberHooks.js**
const HooksDispatcherOnMount: Dispatcher = {
readContext,
use,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
};
위 선언으로부터 HooksDispatcherOnMount는 Dispatcher 타입의 객체를 말하며,
useState는 mountState라는 함수가 할당됨을 알 수 있다.
즉, Fiber가 mount될때 (current fiber를 생성할 때) useState는 mountState함수로 동적으로 결정된다.
mountState함수 내부 구현을 확인해보자
**> react-reconciler > src > ReactFiberHooks.js**
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
**const hook = mountStateImpl(initialState);**
const queue = hook.queue;
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
dispatchSetState
함수는 리액트의useState
와 같은 Hook에서 상태 변경을 요청하고 처리하는 핵심 역할을 수행한다.- 새롭게 정의하는
dispatch
는 currentlyRenderingFiber(현재렌더링중인 파이버객체)와 queue객체를 를 bind해서 상태변경을 처리하는 dispatch함수가 됨. - useState훅은 우리가 넣어준 initialState(hook.memoizedState)와 위에 언급한 dispatch를 반환함.
**> react-reconciler > src > ReactFiberHooks.js**
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
**const hook = mountWorkInProgressHook();**
if (typeof initialState === 'function') {
const initialStateInitializer = initialState;
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
initialState = initialStateInitializer();
}
**hook.memoizedState = hook.baseState = initialState;**
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
return hook;
}
- useState의 초기값이 함수인지 (lazy initialize) , 함수가 아닌 값인지에 따른 분기처리가 존재
- mountState의 hook.memoizedState값은 우리가 넣어준 initialState가 됨을 알 수 있다.
( 왜 최신버전 리액트 코드에서는 아래 함수 구현체를 찾을 수 없는지 의문 , 구 버전 코드 참조)
let workInProgressHook: Hook | null = null; // 전역스코프 선언 객체
...
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null, // hook의 현재 상태값 저장
baseState: null, // 업데이트 이전의 기본 상태
baseQueue: null, // 처리되지 않은 업데이트들의 큐
queue: null, // 현재 업데이트 큐
next: null // 다음 hook을 가리키는 포인터
}
if (workInProgressHook === null) { // 처음 마운트되는 fiber일때
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook; // 마운트상태일 때 초기화된 hook을 반환한다.
}
// 전역스코프 선언 객체
let currentlyRenderingFiber: Fiber = (null: any);
let workInProgressHook: Hook | null = null;
...
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null, // hook의 현재 상태값 저장
baseState: null, // 업데이트 이전의 기본 상태
baseQueue: null, // 처리되지 않은 업데이트들의 큐
queue: null, // 현재 업데이트 큐
next: null // 다음 hook을 가리키는 포인터
}
if (workInProgressHook === null) { // 처음 마운트되는 fiber일때
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook; // 마운트상태일 때 초기화된 hook을 반환한다.
}
- 새로운 Hook 객체를 생성하여, 현재 렌더링 중인 컴포넌트의 Fiber 구조에 할당 (fiber가 마운트될 때)
- Hook의 순서와 상태를 관리하여, 다음 렌더링 사이클에서도 동일한 순서로 호출될 수 있도록 보장
- workInProgressHook가 존재하면 초기화된 hook객체를 할당하고 이를 그대로 반환한다.
- Fiber는 VDOM 구조와 관련된 트리 구조를 형성하고, Hook 관련 정보를 Fiber 노드의 memoizedState 속성 등을 통해 저장한다.
- 리액트의 실제 상태는 컴포넌트의 Fiber 노드에 포함된 Hook 객체와 Context, Props 등에 의해 관리됩니다. VDOM은 이 상태 정보를 바탕으로 렌더링할 UI의 형태를 계산하여, 이를 통해 변화된 UI가 브라우저에 적용될 수 있도록 합니다.