❓ 주의!
해당 글은 아직 react-reconciler 내 구현체에 대한 세밀한 분석이 끝나지 않았습니다.
useEffect 가 Message Channel API 를 기반으로 실행되는 과정을 중점으로 서술합니다.
만약 틀린 점이 있다면 부디 React Fiber 장인 분들의 많은 의견과 질책 부탁드립니다.
setTimeout
을 둬서 렌더링 이후의 실행을 보장한다고 하는데… 과연 그 말이 맞는지에 대한 의문이 들었다.setTimeout
이 유효한지에 대한 의문이 들어서 개인적으로 useEffect
의 구현체에 대한 탐구를 시작했다.useEffect
는 Commit Phase 에 실행되므로 Render Phase 가 종료되는 시점에 실행되는 내부 코드를 조사해보자.// Render Phase 의 Work 가 종료될 경우 실행되는 함수 performSyncWorkOnRoot
function performSyncWorkOnRoot(root) {
// ... 이전 코드 생략
// Commit Phase 에 진입하는 함수 commitRoot 실행
commitRoot(
root,
workInProgressRootRecoverableErrors,
workInProgressTransitions
);
return null;
}
commitRoot()
함수 내부에서는 실제 Commit Phase 에서 실행되는 작업들을 모아둔 구현체는 commitRootImpl
함수를 실행한다.function commitRoot(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null
) {
// ... 중략
try {
ReactCurrentBatchConfig.transition = null;
setCurrentUpdatePriority(DiscreteEventPriority);
// commitRoot 의 실제 구현체인 commitRootImpl 를 실행시킨다.
commitRootImpl(
root,
recoverableErrors,
transitions,
previousUpdateLanePriority
);
} catch (error) {
// ... 생략
}
// ... 중략
return null;
}
// commitRoot 의 실제 구현체 commitRootImpl
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
renderPriorityLevel: EventPriority
) {
// ... 중략
// 만약 Pending 상태의 Passive Effect 가 존재한다면, Passive Effect Scheduler 에 적재시킨다.
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
pndingPassiveTransitions = transitions;
scheduleCallback(NormalSchedulerPriority, () => {
// Passive Effect 내 부수 효과를 Trigger 시키는 함수 flushPassiveEffects
flushPassiveEffects();
return null;
});
}
}
scheduleCallback
함수의 구현체를 살펴보면 Scheduler 모듈의 unstable_scheduleCallback 을 사용함을 알 수 있다.// packages/react-reconciler/src/ReactFiberWorkLoop.new.js (3088 Line)
const fakeActCallbackNode = {};
function scheduleCallback(priorityLevel, callback) {
if (__DEV__) {
const actQueue = ReactCurrentActQueue.current;
if (actQueue !== null) {
actQueue.push(callback);
return fakeActCallbackNode;
} else {
// DEV 모드에서도 Scheduler_scheduleCallback 함수를 실행시킨다.
return Scheduler_scheduleCallback(priorityLevel, callback);
}
} else {
// PRODUCTION 모드에서는 항상 Scheduler_scheduleCallback 함수를 실행시킨다.
return Scheduler_scheduleCallback(priorityLevel, callback);
}
}
// packages/react-reconciler/src/Scheduler.js
export const scheduleCallback = Scheduler.unstable_scheduleCallback; // 이 친구가 실제 ScheduleCallback 이다.
unstable_scheduleCallback
의 구현체는 아래와 같다.unstable_scheduleCallback
에서는 인자로 받은 callback 을 포함하여 내부적으로 쓰이는 Task 객체를 만든다.// packages/scheduler/src/forks/Scheduler.js
// Scheduler 최소 힙 구현 (우선 순위에 따른 Task 정렬)
import { push, pop, peek } from '../SchedulerMinHeap';
function unstable_scheduleCallback(
priorityLevel: PriorityLevel,
callback: Callback,
options?: {delay: number},
): Task {
var currentTime = getCurrentTime();
var startTime;
// ... startTime 구하는 로직, 중략
var timeout;
var expirationTime = startTime + timeout;
// TaskQueue 에 추가할 새로운 Task 객체를 생성하고, callback 함수를 인계한다.
var newTask: Task = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (startTime > currentTime) {
// ... 지연된 Task 에 대한 처리, 생략
} else {
newTask.sortIndex = expirationTime;
// TaskQueue 힙에 새로운 Task 추가.
push(taskQueue, newTask);
// 스케줄링된 작업이 없다면 (isHostCallbackScheduled flag 가 false 라면), requestHostCallback 호출
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true
// taskQueue 내부의 작업을 순차적으로 꺼내어 실행시키는 flushWork 를 requestHostCallback 에 인계
requestHostCallback(flushWork);
}
}
return newTask;
}
function requestHostCallback(callback) {
scheduledHostCallback = callback; // scheduledHostCallback 에 인자로 받은 callback 을 인계한다.
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
requestHostCallback
함수를 호출하여 스케줄링을 시작했다.requestHostCallback
내부에서는 스케줄러가 동작하지 않을 경우 schedulePerformWorkUntilDeadline
함수를 실행시킨다.schedulePerformWorkUntilDeadline
함수를 다르게 설정했다.// IE 혹은 Node.js 인 경우에는 localSetImmediate 에 setImmediate 를 추가한다.
const localSetImmediate =
typeof setImmediate !== 'undefined' ? setImmediate : null;
let schedulePerformWorkUntilDeadline;
// Node.JS 혹은 IE 인 경우 MessageChannel API 를 사용하지 않고 setImmediate 를 사용한다.
if (typeof localSetImmediate === 'function') {
// schedulePerformWorkUntilDeadline 식별자에 localSetImmediate(performWorkUntilDeadline) 을 할당시킨다.
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
}
// 그 외 브라우저에서는 MessageChannel API 를 사용하여 onMessage 콜백에 performWorkUntilDeadline 를 실행시킨다.
else if (typeof MessageChannel !== 'undefined') {
const channel = new MessageChannel();
const port = channel.port2;
// port2 에서 postMessage 실행 시 port1 의 onmessage 콜백이 실행된다.
// 이를 기반으로 스케줄러가 schedulePerformWorkUntilDeadline 함수를 실행하면 performWorkUntilDeadline 가 실행되도록 설계했다.
channel.port1.onmessage = performWorkUntilDeadline;
// schedulePerformWorkUntilDeadline 에 postMessage 함수 인계
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
// 브라우저가 아닌 환경에 대해서는 setTimeout 을 사용한다.
schedulePerformWorkUntilDeadline = () => {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
performWorkUntilDeadline
함수를 의도한 타이밍에 실행시키기 위한 장치다.performWorkUntilDeadline
함수는 Task Queue 에서 작업을 하나 꺼내와 Task 객체 내부의 callback 을 실행시키는 기능을 한다.const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
startTime = currentTime;
const hasTimeRemaining = true;
let hasMoreWork = true;
try {
// 아직 Task Queue 에 작업이 더 남았는지를 체크한다.
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
// 만약 처리해야 할 작업이 아직 남았다면 다음 태스크를 실행하도록 schedulePerformWorkUntilDeadline 를 호출한다.
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
} else {
isMessageLoopRunning = false;
}
needsPaint = false;
};
❓ 왜 Paint 이후에 Schedule 된 작업을 실행하기 위해서 MessageChannel API 를 사용했을까?
postMessage
를 실행하면 다른 포스트의 onmessage 핸들러가 동작하여 비동기로 callback 호출되는 동작 방식을 가진다. const channel = new MessageChannel();
const port = channel.port2;
// port2 에서 postMessage 실행 시 port1 의 onmessage 콜백이 실행된다.
// 이를 기반으로 스케줄러가 schedulePerformWorkUntilDeadline 함수를 실행하면 performWorkUntilDeadline 가 실행되도록 설계했다.
channel.port1.onmessage = performWorkUntilDeadline;
// schedulePerformWorkUntilDeadline 에 postMessage 함수 인계
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
onmessage
핸들러에 performWorkUntilDeadline 함수를 등록했다.port2.postMessage(null)
을 실행하는 콜백을 할당했다.port2.postMessage(null)
가 실행되는 것이고, 그 결과로 port1 의 onmessage
핸들러에 등록된 performWorkUntilDeadline 가 실행된다.이때,
onmessage
핸들러에 등록된 콜백은 비동기로 동작하기에 Task Queue 에 들어간다.
onmessage
핸들러에 등록되어 실행되었던 performWorkUntilDeadline 이 바로 실행된다.setImmediate
나 다른 방식으로 구현한 모습을 볼 수 있다.[MessagePort](https://html.spec.whatwg.org/multipage/web-messaging.html#messageport)
object also has a task source called the port message queue하지만 이 방식도 만능은 아니라는 걸 알아야 한다.