영문 블로그 글을 번역했습니다. 허가를 받으면 시리즈를 이어갈 예정입니다.
원문링크: https://jser.dev/2023-07-18-how-react-rerenders
ℹ️ React Internals Deep Dive 에피소드 3, 유튜브에서 제가 설명하는 것을 시청해주세요.
⚠ React@18.2.0 기준, 최신 버전에서는 구현이 변경되었을 수 있습니다.
React가 최초 마운트를 수행하고 전체 DOM을 처음부터 생성하는 방법에 대해 설명했습니다. 최초 마운트 후 리-렌더링할 때 React는 재조정(recouncile) 과정을 통해 가능한 한 DOM을 재사용하려고 합니다. 이 에피소드에서는 아래 데모에서 버튼을 클릭한 후 React가 리렌더링할 때 실제로 어떤 일이 일어나는지 알아보겠습니다.
import { useState } from 'react';
function Link() {
return <a href='https://jser.dev'>jser.dev</a>;
}
function Component() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((count) => count + 1)}>
click me - {count}
</button>
({count % 2 === 0 ? <span>even</span> : <b>odd</b>})
</div>
);
}
export default function App() {
return (
<div>
<Link />
<br />
<Component />
</div>
);
}
1. Re-render: Trigger phase
React는 최초 마운트에서 Fiber Tree와 DOM 트리를 구성하며, 완료되면 아래와 같이 두 개의 트리가 생깁니다.

1.1 lanes and childLanes
Lane은 보류 중인 작업의 우선순위입니다. Fiber Node의 경우, 이렇습니다:
lanes => 스스로에 대한 보류 중인 작업
childLanes => 서브트리에 대한 보류 중인 작업
ℹ️ Lane 에 대한 자세한 이야기는, EP21 - 리액트 소스 코드에서 Lane이란? 을 참조 해주세요.
버튼이 클릭 됐을 때, setState()가 호출됩니다:
루트에서 대상 Fiber 까지의 경로에는 다음 렌더링에서 확인해야 할 위치를 표시하기 위해 lanes 와 childLanes가 표시됩니다.
업데이트는 scheduleUpdateOnFiber()에 의해 스케줄링되며, 이 스케줄링은 결국 ensureRootIsScheduled() 를 호출하고 "Scheduler"에서 performConcurrentWorkOnRoot() 를 스케쥴링합니다. 이는 최초 마운트와 매우 유사합니다.
명심해야 할 한 가지 중요한 점은 이벤트의 우선순위에 따라 업데이트의 우선순위가 결정된다는 것입니다. click 이벤트의 경우 우선순위가 높은 SyncLane에 매핑되는 DiscreteEventPriority입니다.
ℹ️ `useState()가 어떻게 동작하는지 더 자세히 알고 싶으면, EP5 - useState()는 어떻게 동작하나요? 를 참조 하세요.
자세한 내용은 여기서는 생략하겠지만 결국에는 Fiber Tree를 따라 작업하게 됩니다.

2. Re-render : Render phase
2.1 기본 렌더링 로직은 최초 마운트와 동일합니다.
click 이벤트에서 렌더링 lane은 차단 lane인 SyncLane이기 때문에 최초 마운트와 마찬가지로 performConcurrentWorkOnRoot() 내부에서는 여전히 동시 모드(concurrent mode)가 활성화되지 않습니다.
ℹ️ 동시 모드가 켜져 있는 경우, EP8 - 리액트에서 useTransition()은 어떻게 동작하나요? 를 참고하세요.
아래는 전체 프로세스를 요약한 코드입니다.
💬 역자 주석: Jser의 코멘트는 ❗❗로 표시 해뒀습니다.
그 외 주석은 리액트 소스 코드 자체의 주석입니다.
... 은 생략된 코드입니다.
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
next = beginWork(current, unitOfWork, subtreeRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
더 자세한 설명은 이전 에피소드를 참고하세요. 여기서는 React가 Fiber Tree를 순회하고 필요한 경우 Fiber를 업데이트한다는 점만 기억하세요.
2.2 React는 새로운 Fiber Node를 생성하기 전에 중복된 Fiber Node를 재사용합니다.
최초 마운트에서 우리는 Fiber가 처음부터 생성되는 것을 볼 수 있습니다. 하지만 실제로 React는 먼저 Fiber Node를 재사용하려고 시도합니다.
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
...
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
workInProgress.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
}
workInProgress.flags = current.flags & StaticMask;
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
...
return workInProgress;
}
FiberRootNode는 current 를 통해 현재 Fiber Tree를 가리키기 때문에 현재 트리에 없는 모든 Fiber Node를 재사용할 수 있습니다.
리-렌더링 프로세스에서, 중복 HostRoot는 prepareFreshStack()에서 재사용됩니다.
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
root.finishedWork = null;
root.finishedLanes = NoLanes;
...
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
finishQueueingConcurrentUpdates();
return rootWorkInProgress;
}
따라서 다음 head로 리-렌더링을 시작합니다.

색을 입혀 보겠습니다.

2.3 beginWork() 내의 업데이트 브랜치
beginWork()에는 업데이트를 처리하는 중요한 브랜치가 있는데, 최초 마운트 에피소드에서는 다루지 않았습니다.
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else {
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
(workInProgress.flags & DidCapture) === NoFlags
) {
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
...
}
} else {
didReceiveUpdate = false;
...
}
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
...
}
}
2.4 attemptEarlyBailoutIfNoScheduledUpdate() 내의 Bailout 로직
이름에서 알 수 있듯, 이 함수는 불필요한 경우 렌더링을 더 빨리 중지하려고 시도합니다.
function attemptEarlyBailoutIfNoScheduledUpdate(
current: Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
const root: FiberRoot = workInProgress.stateNode;
pushRootTransition(workInProgress, root, renderLanes);
if (enableCache) {
const cache: Cache = current.memoizedState.cache;
pushCacheProvider(workInProgress, cache);
}
resetHydrationState();
break;
case HostComponent:
pushHostContext(workInProgress);
break;
...
}
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
workInProgress.dependencies = current.dependencies;
}
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
if (enableLazyContextPropagation && current !== null) {
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null;
}
} else {
return null;
}
}
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
export function cloneChildFibers(
current: Fiber | null,
workInProgress: Fiber,
): void {
if (current !== null && workInProgress.child !== current.child) {
throw new Error('Resuming work not yet implemented.');
}
if (workInProgress.child === null) {
return;
}
let currentChild = workInProgress.child;
let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
workInProgress.child = newChild;
newChild.return = workInProgress;
while (currentChild.sibling !== null) {
currentChild = currentChild.sibling;
newChild = newChild.sibling = createWorkInProgress(
currentChild,
currentChild.pendingProps,
);
newChild.return = workInProgress;
}
newChild.sibling = null;
}
bailout 절차를 요약해 보겠습니다.
Fiber에 프로퍼티/컨텍스트 변경이 없고 보류 중인 작업(비어있는 lane)이 없는 경우
자식에게 보류 중인 작업(비어있는 childLanes)이 없는 경우, bailout이 발생하고 React는 트리에서 더 깊게 이동하지 않습니다.
그렇지 않으면 React는 이 Fiber를 리-렌더링하지 않고 바로 자식에게 이동합니다.
그렇지 않으면 React가 먼저 다시 렌더링을 시도한 후 자식에게 전달합니다.
ℹ bailout에 대한 자세한 정보는, EP13 - 리액트 reconciliation에서 bailout이 작동하는 방법을 참고해주세요
2.5 memoizedProps vs pendingProps
beginWork()에서 workInProgress는 current 와 비교됩니다. props의 경우, workInProgress.pendingProps가 current.memoizedProps와 비교됩니다. momoizedProps는 현재 props로, pendingProps는 다음 버전으로 생각할 수 있습니다.
React는 "Render" 단계에서 새로운 Fiber Tree를 생성한 다음 현재 Fiber Tree와 비교(diffing)합니다. pendingProps가 실제로는 workInProgress 생성을 위한 매개변수임을 알 수 있습니다.
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
...
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
workInProgress.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
}
workInProgress.flags = current.flags & StaticMask;
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
workInProgress.refCleanup = current.refCleanup;
return workInProgress;
}
실제로, 루트 FiberNode 생성자에는 pendingProps가 매개변수로 있습니다.
function createFiber(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
return new FiberNode(tag, pendingProps, key, mode);
}
function FiberNode(
this: $FlowFixMe,
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
....
}
따라서 Fiber Node를 만드는 것이 첫 번째 단계입니다. 이 Fiber Node는 나중에 동작하게 됩니다.
그리고 Fiber에 대한 리-렌더링이 완료되면 memoizedProps가 pendingProps에 설정되며, 이는 performUnitOfWork() 내부에 있습니다.
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
이제 demo를 살펴보겠습니다.
React는 HostRoot(lanes: 0, childLanes: 1)에서 작동합니다. HostRoot에는 props가 없고 memoizedProps 와 pendingProps 가 모두 null이므로 React는 복제된 App인 자식으로 바로 이동합니다.
React는 <App/>(lanes: 0, childLanes: 1)에서 작동합니다. App 컴포넌트는 리-렌더링되지 않으므로 memoizedProps 와 pendingProps 는 동일하므로 React는 그 자식인 복제된 div로 바로 이동합니다.
React는 <div/>(lanes: 0, childLanes: 1)에서 작동합니다. App에서 자식들을 가져오지만 App이 다시 실행되지 않으므로 자식(<Link>,<br/> 및 <Component/>)이 변경되지 않으므로 다시 React는 <Link/> 로 바로 이동합니다.
React는 <Link/>(lanes: 0, childLanes: 0)에서 작동합니다. 이번에는 React가 더 깊이 들어갈 필요도 없으므로 여기서 멈추고 형제인 <br/>로 이동합니다.
React가 <br/>(lanes: 0, childLanes: 0)에서 작동하고, bailout이 다시 발생하고, React가 <Component/> 로 이동합니다.
이제 조금 다른 점이 있습니다. <Component/> 에는 1 의 lanes가 있어 React가 그 자식들을 리-렌더링하고 조정해야 하는데, 이는 updateFunctionComponent(current, workInProgress)로 수행됩니다.
지금까지 다음과 같은 상태를 얻었습니다.

2.6 updateFunctionComponent() 는 함수 컴포넌트를 리-렌더링하고 자식을 조정합니다.
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes,
) {
let context;
if (!disableLegacyContext) {
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
context = getMaskedContext(workInProgress, unmaskedContext);
}
let nextChildren;
let hasId;
prepareToReadContext(workInProgress, renderLanes);
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
우리는 React가 최초 마운트를 수행하는 방법에서 reconcileChildren()을 만났습니다. 내부적으로 자식 유형에 따라 몇 가지 변형이 있습니다. 그중 3가지에 집중하겠습니다.
새로운 하위 fiber를 생성할 뿐 아니라 기존 fiber를 재사용하려고 시도한다는 점을 기억하세요.
function reconcileChildFibersImpl(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
...
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_PORTAL_TYPE:
...
case REACT_LAZY_TYPE:
...
}
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
...
}
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
<Component/>의 경우 단일 div를 반환합니다. 따라서 reconcileSingleElement()로 이동하겠습니다.
2.7 reconcileSingleElement()
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
...
} else {
if (
child.elementType === elementType ||
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
}
}
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
if (element.type === REACT_FRAGMENT_TYPE) {
...
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
그리고 useFiber에서 React는 이전 버전을 생성하거나 재사용합니다. 앞서 언급했듯이 pendingProps(자식을 포함하는)가 설정됩니다.
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
const clone = createWorkInProgress(fiber, pendingProps);
clone.index = 0;
clone.sibling = null;
return clone;
}
따라서 컴포넌트가 다시 렌더링된 후 React는 새로운 <div/>인 자식으로 이동하며, 현재 버전은 빈 lanes 와 childLanes 를 모두 가지고 있습니다.
2.8 컴포넌트가 리-렌더링되면 기본적으로 해당 하위 트리가 리-렌더링됩니다.
<div/>와 그 자식들은 예정된 작업이 없으므로 bailout이 발생한다고 생각할 수 있지만 그렇지 않습니다.
beginWork()에 memoizedProps 및 pendingProps 검사가 있다는 것을 기억하세요.
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
}
컴포넌트가 렌더링될 때마다 React 엘리먼트가 포함된 새로운 객체를 생성하므로 매번 pendingProps가 새로 생성되는 반면, props를 비교할 때는 얕은 동등(느슨한 동등)이 사용되지 않는다는 점에 유의하세요.
<div/>의 경우 Component() 가 실행되면 항상 새 프로퍼티를 가져오기 때문에 bailout이 전혀 일어나지 않습니다.
따라서 React는 업데이트 브랜치 - updateHostComponent()로 이동합니다.
2.9 updateHostComponent()
function updateHostComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
pushHostContext(workInProgress);
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}
const type = workInProgress.type;
const nextProps = workInProgress.pendingProps;
const prevProps = current !== null ? current.memoizedProps : null;
let nextChildren = nextProps.children;
const isDirectTextChild = shouldSetTextContent(type, nextProps);
if (isDirectTextChild) {
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
workInProgress.flags |= ContentReset;
}
markRef(current, workInProgress);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
여기있는 nextChildren은 아래와 같습니다:
[
{$$typeof: Symbol(react.element), type: 'button'},
" (",
{$$typeof: Symbol(react.element), type: 'b'},
")"
]
따라서 내부적으로 React는 reconcileChildrenArray()로 이를 조정합니다.
그리고 memoizedProps의current는 아래와 같습니다.
[
{$$typeof: Symbol(react.element), type: 'button'},
" (",
{$$typeof: Symbol(react.element), type: 'span'},
")"
]
2.10 reconcileChildrenArray() 는 필요에 따라 Fiber를 생성하고 삭제합니다.
reconcileChildrenArray() 는 약간 복잡합니다. 엘리먼트의 재정렬(re-order)이 있는지 확인하고 key 가 있는 경우 Fiber를 재사용하려고 시도하여 추가적인 최적화를 수행합니다.
ℹ key의 경우, 이 주제에 대한 별도의 에피소드 - EP19 - key는 어떻게 동작하나? 리액트의 리스트 diffing
하지만 데모에서는 key가 없으므로 그냥 기본 지점으로 이동하겠습니다.
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<any>,
lanes: Lanes,
): Fiber | null {
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
...
return resultingFirstChild;
}
updateSlot() 은 기본적으로 key를 고려하여 새로운 props로 Fiber를 생성하거나 재사용할 뿐입니다.
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
const key = oldFiber !== null ? oldFiber.key : null;
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
if (key !== null) {
return null;
}
return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
return updateElement(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
}
}
...
}
}
return null;
}
function updateElement(
returnFiber: Fiber,
current: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
return updateFragment(
returnFiber,
current,
element.props.children,
lanes,
element.key,
);
}
if (current !== null) {
if (
current.elementType === elementType ||
(__DEV__
? isCompatibleFamilyForHotReloading(current, element)
: false) ||
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === current.type)
) {
const existing = useFiber(current, element.props);
existing.ref = coerceRef(returnFiber, current, element);
existing.return = returnFiber;
return existing;
}
}
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, current, element);
created.return = returnFiber;
return created;
}
따라서 <div/>에서, updateSlot()은 3개의 자식을 성공적으로 재사용하고, 4번째는 예외인데 왜냐하면 current가 span이지만 우리는 b를 원하기 때문에, span의 Fiber는 처음부터 생성되고 deleteChild()를 통해 b의 Fiber는 삭제됩니다. 새로 생성된 span은 placeChild()로 표시됩니다.
2.11 placeChild() 및 deleteChild() 는 플래그로 Fiber를 표시합니다.
자, Comonent 아래 <div>의 자식에는 파이버 노드를 표시하는 이 두 가지 함수가 있습니다.
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
newFiber.flags |= Forked;
return lastPlacedIndex;
}
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
return oldIndex;
}
} else {
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
if (!shouldTrackSideEffects) {
return;
}
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete];
returnFiber.flags |= ChildDeletion;
} else {
deletions.push(childToDelete);
}
}
삭제해야 하는 Fiber는 부모 Fiber의 배열에 임시로 저장됩니다. 이는 삭제 후 새 Fiber Tree에 더 이상 존재하지 않지만 "Commit" 단계에서 처리해야 하므로 어딘가에 저장해야 하기 때문에 저장이 필요합니다.
이제 <div>가 완성되었습니다.

다음 React는 버튼으로 이동합니다. 다시 말하지만, 스케줄이 작동하지 않는다고 생각했지만, prop이 ["click me-", "1"] 에서 ["click me-", "2"]로 변경되었기 때문에 React는 여전히 updateHostComponent() 를 사용하여 작동합니다.
HostText의 경우 프로퍼티가 문자열이므로 첫 번째 "click me -" 는 중단됩니다. 그리고 React는 updateHostText()로 텍스트를 조정하려고 시도합니다.
2.12 updateHostText()
function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}
return null;
}
다시 말하지만 이건 아무것도 안합니다, 왜냐하면 업데이트는 완료 단계인 completeWork() 에서 표시됩니다. 이는 최초 마운트에서도 설명 했습니다.
2.13 completeWork() 는 HostComponent의 업데이트를 표시하고 필요한 경우 DOM 노드를 생성합니다.
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
...
}
bubbleProperties(workInProgress);
return null;
}
case HostText: {
const newText = newProps;
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
updateHostText(current, workInProgress, oldText, newText);
} else {
...
if (wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
}
}
bubbleProperties(workInProgress);
return null;
}
...
}
}
updateHostText = function(
current: Fiber,
workInProgress: Fiber,
oldText: string,
newText: string,
) {
if (oldText !== newText) {
markUpdate(workInProgress);
}
};
updateHostComponent = function(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
return;
}
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext,
);
workInProgress.updateQueue = (updatePayload: any);
if (updatePayload) {
markUpdate(workInProgress);
}
};
function markUpdate(workInProgress: Fiber) {
workInProgress.flags |= Update;
}
렌더링 단계가 끝나면 다음과 같은 결과가 나타납니다.
b에 삽입(Insertion)
span에서 삭제(Deletion)
HostText의 업데이트
button 의 업데이트(후드 아래 비어 있음)
한 가지 강조하고 싶은 것은 button과 그 부모 div 모두에 대해 prepareUpdate() 가 실행되지만, div에 대해서는 null을 생성하지만 button에 대해서는 [] 을 생성한다는 점입니다. 여기서는 다루지 않을 까다로운 에지 케이스입니다.

이제 커밋 단계에서 이러한 업데이트를 커밋할 차례입니다.
3. Re-render: Commit Phase
3.1 commitMutationEffectsOnFiber() 는 Insertion/Deletion/Update의 커밋을 시작합니다.
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
try {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(
HookInsertion | HookHasEffect,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
...
}
return;
}
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (supportsMutation) {
if (finishedWork.flags & ContentReset) {
const instance: Instance = finishedWork.stateNode;
try {
resetTextContent(instance);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
if (flags & Update) {
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
const newProps = finishedWork.memoizedProps;
const oldProps =
current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
try {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(
finishedWork,
finishedWork.return,
error,
);
}
}
}
}
}
return;
}
case HostText: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
if (supportsMutation) {
if (finishedWork.stateNode === null) {
throw new Error(
'This should have a text node initialized. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
}
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
const oldText: string =
current !== null ? current.memoizedProps : newText;
try {
commitTextUpdate(textInstance, oldText, newText);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
}
}
우리는 이것이 재귀적 프로세스임을 알 수 있으며 각 변형(mutation) 유형을 자세히 살펴 보겠습니다.
3.2 삭제가 먼저 처리된 후, 자식 및 self를 처리합니다.
function recursivelyTraverseMutationEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes,
) {
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
commitDeletionEffects(root, parentFiber, childToDelete);
} catch (error) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
}
const prevDebugFiber = getCurrentDebugFiberInDEV();
if (parentFiber.subtreeFlags & MutationMask) {
let child = parentFiber.child;
while (child !== null) {
setCurrentDebugFiberInDEV(child);
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}
자식을 처리하기 전임에도, 삭제는 우선적으로 처리됩니다.
function commitDeletionEffects(
root: FiberRoot,
returnFiber: Fiber,
deletedFiber: Fiber,
) {
if (supportsMutation) {
let parent = returnFiber;
findParent: while (parent !== null) {
switch (parent.tag) {
case HostComponent: {
hostParent = parent.stateNode;
hostParentIsContainer = false;
break findParent;
}
case HostRoot: {
hostParent = parent.stateNode.containerInfo;
hostParentIsContainer = true;
break findParent;
}
case HostPortal: {
hostParent = parent.stateNode.containerInfo;
hostParentIsContainer = true;
break findParent;
}
}
parent = parent.return;
}
if (hostParent === null) {
throw new Error(
'Expected to find a host parent. This error is likely caused by ' +
'a bug in React. Please file an issue.',
);
}
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
hostParent = null;
hostParentIsContainer = false;
} else {
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
}
detachFiberMutation(deletedFiber);
}
function commitDeletionEffectsOnFiber(
finishedRoot: FiberRoot,
nearestMountedAncestor: Fiber,
deletedFiber: Fiber,
) {
onCommitUnmount(deletedFiber);
switch (deletedFiber.tag) {
case HostComponent: {
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
}
case HostText: {
if (supportsMutation) {
const prevHostParent = hostParent;
const prevHostParentIsContainer = hostParentIsContainer;
hostParent = null;
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
hostParent = prevHostParent;
hostParentIsContainer = prevHostParentIsContainer;
if (hostParent !== null) {
if (hostParentIsContainer) {
removeChildFromContainer(
((hostParent: any): Container),
(deletedFiber.stateNode: Instance | TextInstance),
);
} else {
removeChild(
((hostParent: any): Instance),
(deletedFiber.stateNode: Instance | TextInstance),
);
}
}
} else {
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
}
return;
}
...
default: {
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
}
}

3.3 삽입은 다음으로 처리됩니다.
이는 새로 생성된 노드를 트리 구조로 설정할 수 있도록 하기 위한 것입니다.
function commitReconciliationEffects(finishedWork: Fiber) {
const flags = finishedWork.flags;
if (flags & Placement) {
try {
commitPlacement(finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
finishedWork.flags &= ~Placement;
}
if (flags & Hydrating) {
finishedWork.flags &= ~Hydrating;
}
}
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
const parentFiber = getHostParentFiber(finishedWork);
switch (parentFiber.tag) {
case HostComponent: {
const parent: Instance = parentFiber.stateNode;
if (parentFiber.flags & ContentReset) {
resetTextContent(parent);
parentFiber.flags &= ~ContentReset;
}
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
case HostRoot:
case HostPortal: {
const parent: Container = parentFiber.stateNode.containerInfo;
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
break;
}
default:
throw new Error(
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
}
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
const {tag} = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
} else if (tag === HostPortal) {
} else {
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
function insertOrAppendPlacementNode(
node: Fiber,
before: ?Instance,
parent: Instance,
): void {
const {tag} = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
if (before) {
insertBefore(parent, stateNode, before);
} else {
appendChild(parent, stateNode);
}
} else if (tag === HostPortal) {
} else {
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNode(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNode(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}

3.4 업데이트는 마지막에 처리됩니다.
업데이트 브랜치는 commitMutationEffectsOnFiber() 안에 있습니다.
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
try {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(
HookInsertion | HookHasEffect,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
if (supportsMutation) {
if (finishedWork.flags & ContentReset) {
const instance: Instance = finishedWork.stateNode;
try {
resetTextContent(instance);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
if (flags & Update) {
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
const newProps = finishedWork.memoizedProps;
const oldProps =
current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
try {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(
finishedWork,
finishedWork.return,
error,
);
}
}
}
}
}
return;
}
case HostText: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
if (supportsMutation) {
if (finishedWork.stateNode === null) {
throw new Error(
'This should have a text node initialized. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
}
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
const oldText: string =
current !== null ? current.memoizedProps : newText;
try {
commitTextUpdate(textInstance, oldText, newText);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
...
default: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
return;
}
}
}
export function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
updateProperties(domElement, updatePayload, type, oldProps, newProps);
updateFiberProps(domElement, newProps);
}
export function commitTextUpdate(
textInstance: TextInstance,
oldText: string,
newText: string,
): void {
textInstance.nodeValue = newText;
}
데모에서는 트리 구조 때문에 변형(mutation)이 아래 순서대로 처리됩니다.
span에서 삭제
HostText 업데이트
button의 업데이트 (후드 아래 비어 있음)
b 삽입
4. 요약
휴, 정말 많네요. 리-렌더링 프로세스를 대략적으로 요약하면 다음과 같습니다.
상태가 변경되면 대상 Fiber Node의 경로가 lanes 및 childLanes으로 표시되어 해당 노드 또는 하위 트리를 리-렌더링해야 하는지 여부를 나타냅니다.
불필요한 리-렌더링을 피하기 위해 bailout을 최적화하여 전체 Fiber Tree를 리-렌더링합니다.
컴포넌트가 리-렌더링되면 새로운 React 엘리먼트가 생성되고, 그 자식들은 모두 동일하더라도 새로운 프로퍼티를 가지므로 React는 기본적으로 전체 Fiber Tree를 리-렌더링합니다. useMemo() 함수가 필요한 이유는 다음과 같습니다.
"리-렌더링"을 통해 React는 현재 트리에서 새로운 Fiber Tree를 생성하고, 필요한 경우 Fiber Node에 PlacementChildDeletion 및 Update 플래그를 표시합니다.
새 Fiber Tree가 완료되면 React는 위의 플래그로 Fiber Node를 처리하고 "Commit" 단계에서 호스트 DOM에 변경 사항을 적용합니다.
그러면 새 Fiber Tree가 현재 Fiber Tree를 가리키게 됩니다. 이전 Fiber Tree의 노드는 다음 렌더링에 재사용 할 수 있습니다.
아래 슬라이드에 단계를 정리해 두었으니 도움이 되길 바랍니다.
JSer의 슬라이드 링크
원글 작성일: 2023-07-23