import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
const rootElement = document.getElementById('root')
const root = ReactDOM.createRoot(rootElement!)
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)
먼저 시작은 ReactDOM.createRoot이다.
리액트로 앱을 만들 때는, HTML 파일 안에 있는 특정 DOM element를 React의 루트로 삼아야 한다. 일반적으로는 <div id="root"></div>처럼 HTML의 최상단에 위치한 element를 사용하지만, 꼭 그럴 필요는 없다.
리액트는 앱 전체를 통째로 구성하는 경우가 많지만 페이지 일부만 리액트로 구현할 수도 있다.
즉, 꼭 HTML의 최상단 태그가 root일 필요는 없으며, 일부 페이지 혹은 특정 섹션의 element도 루트로 지정할 수 있다. 루트가 여러개일 수도 있다.
이렇게 선택한 DOM element를 document.getElementById나 유사한 방식으로 가져온 다음, ReactDOM.createRoot에 넘겨주면 해당 root를 통해 container(rootNode)를 만들며 container render()를 통해 리액트의 work가 시작된다.
// packages/react-dom/src/client/ReactDOMRoot.js
import { markContainerAsRoot } from 'react-dom-bindings/src/client/ReactDOMComponentTree';
import {
createContainer,
} from 'react-reconciler/src/ReactFiberReconciler';
import { ConcurrentRoot } from 'react-reconciler/src/ReactRootTags';
function ReactDOMRoot(internalRoot) {
this._internalRoot = internalRoot;
}
ReactDOMRoot.prototype.render = function (children) {
const root = this._internalRoot;
if (root === null) {
throw new Error('Cannot update an unmounted root.');
}
updateContainer(children, root, null, null);
};
export function createRoot(container, options) {
if (!isValidContainer(container)) {
throw new Error('Target container is not a DOM element.');
}
/*
options 설정 관련 코드들
*/
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
transitionCallbacks,
);
/*
// container (div같은 dom node)에 '__reactContainer$' + randomKey라는 랜덤한 키에
// HostRootFiber를 저장한다.
export function markContainerAsRoot(hostRoot: Fiber, node: Container): void {
node[internalContainerInstanceKey] = hostRoot;
}
*/
markContainerAsRoot(root.current, container);
const rootContainerElement =
!disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE
? container.parentNode
: container;
listenToAllSupportedEvents(rootContainerElement);
return new ReactDOMRoot(root);
}
createroot함수는 크게 두가지 역할을 한다.
1. root element를 통해 container를 만들고, rootFiber를 만든다. rootFiber는 container에 연결되며, container는 다시 root element에 연결된다.
React는 우리가 전달한 DOM 노드와 React의 fiber를 연결해야 한다. 이를 위해 DOM 노드에 특별한 속성을 하나 만들어서 React Fiber 정보를 저장한다.이 속성의 이름은 고유해야 하는데, 다른 라이브러리나 코드와 충돌하지 않도록 랜덤한 문자열을 생성해서 사용한다. 이렇게 연결해두면 나중에 이벤트가 발생했을 때 그 DOM 노드가 어떤 React 컴포넌트와 연결되어 있는지 빠르게 찾을 수 있다. 양방향으로 연결되어 있어서 DOM에서 React로도, React에서 DOM으로도 접근할 수 있게 된다.
2. 이벤트 리스너를 등록한다.
React가 지원하는 모든 네이티브 이벤트들에 대해 이벤트 리스너를 등록한다. 이때 중요한 점은 개별 DOM 요소마다 리스너를 등록하는 것이 아니라 루트 컨테이너 하나에만 모든 이벤트 리스너를 등록한다는 것이다.이벤트 위임의 핵심은 이벤트 버블링과 캡처링을 활용하는 것이다. 캡처 단계와 버블링 단계 모두에 리스너를 등록해서 이벤트가 DOM 트리를 타고 올라오거나 내려갈 때 루트 컨테이너에서 모든 이벤트를 잡아낸다.
이렇게 하면 1000개의 버튼이 있어도 1000개의 리스너가 아닌 루트에 1개만 등록하면 되고, 새로 추가된 DOM 요소도 자동으로 이벤트 처리가 가능하다. 사용자가 버튼을 클릭하면 이벤트가 루트 컨테이너까지 버블링되고, React가 이벤트 타겟을 분석해서 어떤 컴포넌트의 onClick인지 파악해서 해당 컴포넌트의 이벤트 핸들러를 실행한다.
예외적으로 일부 이벤트들은 위임하지 않는다. 스크롤이나 로드 같은 이벤트들은 일관되게 버블링되지 않기 때문에 각 요소에 직접 리스너를 등록해야 한다.selectionchange 이벤트는 특별히 document에서만 발생하므로 별도로 처리한다.
// packages/react-reconciler/src/ReactFiberReconciler.js
export function createContainer(
containerInfo,
tag,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
transitionCallbacks
) {
const hydrate = false;
const initialChildren = null;
return createFiberRoot(
containerInfo,
tag,
hydrate,
initialChildren,
hydrationCallbacks,
isStrictMode,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
transitionCallbacks,
null
);
}
// packages/react-reconciler/src/ReactFiberRoot.js
export function createFiberRoot(
containerInfo,
tag,
hydrate,
initialChildren,
hydrationCallbacks,
isStrictMode,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
transitionCallbacks,
formState
) {
const root = new FiberRootNode(
containerInfo,
tag,
hydrate,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
formState,
);
/*
options 설정 관련 코드들
*/
// Cyclic construction.
const uninitializedFiber = createHostRootFiber(tag, isStrictMode);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
/*
initialCache의 반환값, 자바스크립트 객체이다.
{
controller: new AbortControllerLocal(),
data: new Map(),
refCount: 0,
}
*/
const initialCache = createCache();
retainCache(initialCache); // refCount++
root.pooledCache = initialCache;
retainCache(initialCache);
const initialState = {
element: initialChildren,
isDehydrated: hydrate,
cache: initialCache,
};
uninitializedFiber.memoizedState = initialState;
initializeUpdateQueue(uninitializedFiber);
return root;
}
export function initializeUpdateQueue<State>(fiber: Fiber): void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
lanes: NoLanes,
hiddenCallbacks: null,
},
callbacks: null,
};
fiber.updateQueue = queue;
}
먼저 contaienr와 각종 메타데이터들을 저장하는 rootNode를 만들고 rootFiber를 만든다. rootFiber는 current에 fiber를 저장하고, 그 fiber의 stateNode에 다시 root를 저장하는 식으로 서로 원형 참조를 하고 있다.
FiberRootNode는 container 정보, context, expirationTimes, lanes 등 전체 애플리케이션의 메타데이터를 저장하고, HostRootFiber는 fiber로써 최상단 부모 역할을 담당한다.
HostRoot의 UpdateQueue는 update에 관한 정보를 담고 있는 원형 리스트로, render 혹은 setState 호출들을 연결 리스트로 관리하며, 여러 업데이트를 순차적으로 처리한다. render를 통해 생성된 update에는 initialchildren, 우리가 만든 컴포넌트들,이 담겨있는 것을 볼 수 있다.
rootFiber에 저장하는 fiber는 아래 코드와 같다.
export function createHostRootFiber(
tag: RootTag,
isStrictMode: boolean,
): Fiber {
/*
모드 설정: Concurrent/Strict/Profile 등의 실행 모드를 설정한다
*/
return createFiber(HostRoot, null, null, mode);
}
// enableOjbectFiber의 default는 false.
const createFiber = enableObjectFiber
? createFiberImplObject
: createFiberImplClass;
function createFiberImplClass(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
return new FiberNode(tag, pendingProps, key, mode);
}
function FiberNode(
this,
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.refCleanup = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
}
위 함수들을 거쳐 생성한 fiber를 rootfiber의 current에 저장하고, 저장한 fiber는 FiberNode(tag, null, null, mode) 이다.
이제 우리가 ReactDom.createRoot 실행을 통해 반환되는 값은 ReactDomRoot의 instance이고, 그 instance는 property value로 FiberRootNode를 가지고, 그 FiberRootNode는 current에 HostRootFiber를 가짐을 알 수 있다.
ReactDomRoot의 proptotype method에 render정의 되어 있고 render를 호출하면 다음과 같이 react element(children)과 FiberRootNode임을 알 수 있다.
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
function (children: ReactNodeList): void {
const root = this._internalRoot;
updateContainer(children, root, null, null);
};
위 예시에서는 children은 App컴포넌트가 될 것이다. 그럼 마저 updateContainer 함수를 분석해보자.
// react-reconciler\src\ReactFiberReconciler.js
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
const current = container.current;
const lane = requestUpdateLane(current);
updateContainerImpl(
current,
lane,
element,
container,
parentComponent,
callback,
);
return lane;
}
function updateContainerImpl(
rootFiber: Fiber,
lane: Lane,
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): void {
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
const update = createUpdate(lane);
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
// 반환된 root는 FiberRootNode
const root = enqueueUpdate(rootFiber, update, lane);
if (root !== null) {
startUpdateTimerByLane(lane);
scheduleUpdateOnFiber(root, rootFiber, lane);
entangleTransitions(root, rootFiber, lane);
}
}
lane은 업데이트가 발생했음을 나타내는 지표이자 그 업데이트의 우선순위를 나타낸다.
resolveUpdatePriority 함수는 현재 발생한 업데이트의 우선순위를 결정한다. 이는 createRoot에서 등록한 이벤트 리스너를 통해 이루어지고 이 함수는 두 단계로 동작한다. 먼저 현재 설정된 업데이트 우선순위가 있는지 확인하고, 없다면 현재 진행 중인 DOM 이벤트의 타입을 기반으로 우선순위를 결정한다. 이벤트 리스너는 window.event를 통해 현재 이벤트에 접근하고, getEventPriority 함수를 사용해 이벤트 타입에 따른 우선순위를 반환한다.getEventPriority 함수는 DOM 이벤트 타입을 다음과 같이 분류한다:
- DiscreteEventPriority: 클릭, 키보드 입력 등 사용자의 직접적인 입력 이벤트 (가장 높은 우선순위)
- ContinuousEventPriority: 마우스 이동, 스크롤 등 연속적인 이벤트 (중간 우선순위)
- DefaultEventPriority: 기타 모든 이벤트와 기본값 (낮은 우선순위)
이렇게 결정된 우선순위는 eventPriorityToLane 함수를 통해 lane으로 변환된다.예를 들어 DiscreteEventPriority는 SyncLane에, ContinuousEventPriority는 InputContinuousLane에 대응된다.이렇게 변환된 lane은 React의 스케줄링 시스템에서 어떤 업데이트를 먼저 처리할지 결정하는 기준이 된다.
parentComponent는 rootFiber이기 때문에 null이고, 그에 따라 context는 빈 객체가 된다.
const update: Update<mixed> = {
lane,
tag: UpdateState,
payload: null,
callback: null,
next: null,
};
payload에는 react element가 담긴다. 이때 element는 root로부터의 children들이기 때문에 rootfiber가 될 것이다.
export function enqueueUpdate<State>(
fiber: Fiber,
update: Update<State>,
lane: Lane,
): FiberRoot | null {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
return null;
}
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
if (isUnsafeClassRenderPhaseUpdate(fiber)) {
// Render phase 중 발생한 클래스 컴포넌트 업데이트 처리
// 이미 렌더링 중이어도, 자식 업데이트 예약 정보(childlanes)를 루트까지 전파해서
// 다음 렌더 때 누락되지 않도록 함.
return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
} else {
return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
}
}
export function enqueueConcurrentClassUpdate<State>(
fiber: Fiber,
queue: ClassQueue<State>,
update: ClassUpdate<State>,
lane: Lane,
): FiberRoot | null {
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
return getRootForUpdatedFiber(fiber);
}
function getRootForUpdatedFiber(sourceFiber: Fiber): FiberRoot | null {
// 무한 재렌더링 감지 로직
throwIfInfiniteUpdateLoopDetected();
detectUpdateOnUnmountedFiber(sourceFiber, sourceFiber);
let node = sourceFiber;
let parent = node.return;
while (parent !== null) {
detectUpdateOnUnmountedFiber(sourceFiber, node);
node = parent;
parent = node.return;
}
return node.tag === HostRoot ? (node.stateNode: FiberRoot) : null;
}
enqueueUpdate부터 getRootForUpdatedFiber까지 전달된 fiber는 hostFiber다. getRootForUpdatedFiber에서 node.return을 통해 부모 fiber를 찾아가는데, hostFiber는 이미 최상단 fiber이고 tag는 HostRoot이니 stateNode인 container가 반환된다.
HostRoot의 updateQueue에는 baseState로 initialChildren이 저장되어 있고, 마운트 시에는 updateHostRoot에서 processUpdateQueue를 호출해서 이 baseState만으로 렌더링을 진행한다.
work가 끝나면 finishQueueingConcurrentUpdates에서 concurrentQueue에 쌓인 업데이트들을 fiber의 shared.pending에 추가한다. 이렇게 하는 이유는 렌더링 중에 새로운 업데이트가 들어와도 현재 진행 중인 렌더링의 일관성을 해치지 않기 위함이다.
enqueueConcurrentClassUpdate에서는 전역변수인 concurrentQueues에 업데이트를 위한 정보인 fiber, update, lane, queue 등을 기록하고, 반환된 stateNode(FiberRoot)를 가지고 scheduleUpdateOnFiber(root, rootFiber, lane) 함수가 실행된다.
'React' 카테고리의 다른 글
| React 톺아보기 3. scheduler (1) (0) | 2025.06.29 |
|---|---|
| React 톺아보기 2. reconciler (1) (0) | 2025.06.09 |
| React에서 batch 처리와 렌더링 주기: 무한 재렌더링 (1) | 2025.01.15 |
| React에서 경로 최적화를 통한 성능 향상 (0) | 2025.01.14 |
| React Hook이란 무엇인가 (1) | 2024.12.02 |