- Jser.dev의 React Internals Deep Dive 시리즈 중 The Overview of React internals를 바탕으로 작성한 글입니다.
- react 18.2.0 버전 기준입니다.
- 🚨 이번 글엔 많은 겉핥기식 추측이 포함되어 있습니다.
React를 실무에서 사용하며 React로 코딩하는 것에 점점 익숙해지지만, 그만큼 이해도가 높아지는 것 같지는 않습니다. 당연한 말이지만, React도 코드 조각으로 이루어진 프로그램인데, 내부 로직에는 눈을 가린채 일종의 '마법'처럼 사용하고 있는 것 같기도 합니다.
마구 짠 코드들의 비효율이 모여 꽤 커져버린 성능 저하, 복잡한 버그 등을 해결할 땐 리액트에 대한 보다 깊은 이해에 대한 갈증이 생깁니다.
동아리에서 알게된 Jser.dev 블로그의 React Internals Deep Dive에 React 내부 코드를 살펴보는 포스팅이 잘 정리되어 있어, 이 시리즈를 바탕으로 React 내부를 파헤치며 이해를 심화시키고자 합니다. 오늘은 그 첫번째로, 큰 틀에서 훑어보며 내부 로직에 대한 상상을 하는 것으로 시작합니다.
바로 React 코드를 하나하나 열어보기보다, 실제 런타임에서 어떤 함수들이 호출되는지를 보며 큰 맥락을 파악해보면 좋을 것 같습니다.
call stack을 보며 React의 실행 흐름을 훑어보기 위한 간단한 예시 코드에 debuuger
를 추가합니다.
function App() {
const [count, setCount] = useState(1);
debugger;
useEffect(() => {
debugger;
setCount((count) => count + 1);
}, []);
return <button>{count}</button>;
ReactDOM.createRoot(document.getElementById("container")).render(<App />);
DOM이 조작될 때의 call stack도 보기 위해, DOM Breakpoint도 추가합니다.
call stack에서 몇가지 주요 함수들을 살펴보겠습니다.
ReactDOMRoot.render
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function (children) {
var root = this._internalRoot;
// 에러처리 생략
updateContainer(children, root, null, null);
};
updateContainer
// 임의로 많이 생략된 코드입니다.
function updateContainer(element, container, parentComponent, callback) {
var current$1 = container.current;
var eventTime = requestEventTime();
var lane = requestUpdateLane(current$1);
var update = createUpdate(eventTime, lane);
update.payload = {
element: element
};
var root = enqueueUpdate(current$1, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, current$1, lane, eventTime);
}
return lane;
}
scheduleUpdateOnFiber
ensureRootIsScheduled
// 임의로 많이 생략된 코드입니다.
function ensureRootIsScheduled(root, currentTime) {
var newCallbackPriority = getHighestPriorityLane(nextLanes); // Check if there's an existing task. We may be able to reuse it.
var existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
// 에러처리 생략
// The priority hasn't changed. We can reuse the existing task. Exit.
return;
}
var newCallbackNode;
if (newCallbackPriority === SyncLane) {
// 스케줄 로직 생략
}
newCallbackNode = null;
} else {
var schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediatePriority;
break;
// 우선순위 레벨 결정 케이스 생략
default:
schedulerPriorityLevel = NormalPriority;
break;
}
newCallbackNode = scheduleCallback$1(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
performConcurrentWorkOnRoot
가 예약 됩니다.function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
var startTime;
if (typeof options === "object" && options !== null) {
// 여러 조건(options, delay 등)에 따라 startTime 설정하는 로직 생략
}
var timeout;
switch (priorityLevel) {
// prioirityLevel에 따라 timeout 설정하는 로직 생략
}
var expirationTime = startTime + timeout;
var newTask = {
id: taskIdCounter++,
callback: callback,
priorityLevel: priorityLevel,
startTime: startTime,
expirationTime: expirationTime,
sortIndex: -1,
};
if (startTime > currentTime) {
// delayed task 처리하는 로직 생략
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}
postMessage
를 통해 비동기적으로 스케줄된 작업을 시작합니다.// 이 함수는 짧고, 주석에 React 개발자들의 의도가 많이 담겨있어 전체를 가져왔습니다.
var performWorkUntilDeadline = function () {
if (scheduledHostCallback !== null) {
var currentTime = getCurrentTime(); // Keep track of the start time so we can measure how long the main thread
// has been blocked.
startTime = currentTime;
var hasTimeRemaining = true; // If a scheduler task throws, exit the current browser task so the
// error can be observed.
//
// Intentionally not using a try-catch, since that makes some debugging
// techniques harder. Instead, if `scheduledHostCallback` errors, then
// `hasMoreWork` will remain true, and we'll continue the work loop.
var hasMoreWork = true;
try {
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
if (hasMoreWork) {
// If there's more work, schedule the next message event at the end
// of the preceding one.
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
} else {
isMessageLoopRunning = false;
} // Yielding to the browser will give it a chance to paint, so we can
};
hasMoreWork
가 true이면, schedulePerformWorkUntilDeadline
를 다시 호출합니다. 즉, 다음 메시지 이벤트를 예약합니다.ensureRootIsScheduled
함수에서 등록했던 함수가 실행됩니다.mountIndeterminateComponent
)performConcurrentWorkRoot
에서 render가 끝나고 나면, commit 단계로 넘어옵니다.function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
// 생략
switch (finishedWork.tag) {
// ...
case HostRoot: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
// ...
}
recursivelyTraverseMutationEffects
를 호출하고, 그 안에선 child를 넘기며 commitMutationEffectsOnFiber
를 다시 호출합니다.useEffect
에 의해 생성된 passive effects를 flush 한다는데, 정확히 어떤 역할인지는 잘 모르겠습니다.useEffect
안에서 setState
를 하여 유발된 re-render 시점입니다.mountIndeterminateComponent
를 호출했던 것 대신 updateFunctionComponent
함수가 호출되는 것 정도가 다릅니다.(출처: https://jser.dev/2023-07-11-overall-of-react-internals)
위 예시에서 debugger와 breakpoint를 통해 살펴본 리액트 내부의 흐름을 단계별로 정리해봅시다.
scheduleUpdateOnFiber
)를 react에 알려줍니다.ensureRootIsScheduled
가 작업을 결정하고 예약하는 마지막 단계입니다.scheduleCallback
을 통해 스케줄 단계로 넘어갑니다.workLoop
는 실질적인 작업을 실행합니다.flushPassiveEffects
, commitLayoutEffects
등)오늘은, 간단한 예제코드를 통해 큰 틀에서의 리액트 흐름을 살펴보았습니다.
call stack에 어떤 함수들이 등록되는지, 어떤 인자들을 주고 받으며 이어지는지를 보며 역할을 추측했고, 자세한 디테일은 다루지 않았습니다.
이후, 오늘 글의 소스가 된 Jser.dev의 React Internals Deep Dive 시리즈를 바탕으로 리액트의 더 깊숙한 부분으로 파고들어볼 예정입니다.