웹 혹은 웹앱을 개발할 때 프로젝트의 복잡성이 높아질 수록 메인 스레드의 효율적인 관리는 JS를 사용한다면 성능 최적화의 핵심이 되는 부분입니다.
그 동안 보편적으로 setTimeout(0) 과 같은 기법으로 브라우저에게 작업을 잠시 양보해가며 작업을 쪼개서 실행해왔으나 이러한 방법은 우선순위나 세밀한 제어에 한계가 분명히 존재합니다.
이 한계를 극복하기 위해서 등장한 것이 바로 JS의 Scheduler API 입니다.
Scheduler API는 작업의 우선순위를 명시하고, 메인 스레드에서 실행 타이밍을 보다 정밀하게 조절할 수 있도록 돕는 표준 API입니다.
setTimeout(() => {
processNextBatch();
}, 0);
위와 같은 방식은 브라우저의 Event Loop 에 넘겨 비동기 방식으로 처리하는 것이 가능하지만
과 같은 단점을 가지고 있습니다.
function heavyTask() {
doWorkChunk1();
setTimeout(() => {
doWorkChunk2();
}, 0); // 잠깐 쉬었다 다시 실행
}
async function processLargeData() {
for (let i = 0; i < 1000; i++) {
processItem(i);
if (i % 100 === 0) {
await scheduler.yield(); // 메인 스레드 양보
}
}
}
await scheduler.postTask(() => {
processNextBatch();
}, { priority: 'background' });
작업 우선순위 같은 경우는 Scheduler API 에서 세 가지 작업 우선순위를 지원합니다.
async function processData(data) {
const controller = new TaskController({ priority: 'background' });
try {
await scheduler.postTask(() => heavyComputation(data), {
signal: controller.signal,
priority: 'background'
});
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Processing failed:', error);
}
}
}
heavyComputation 작업 순위를 'background'로 설정하였기에 브라우저가 다른 중요한 작업(렌더링, 사용자 입력 등) 먼저 처리한 후 실행되도록 유도합니다.
사용자 인터랙션을 막지 않으면서 무거운 작업을 진행하고 싶을 때, 혹은 중간에 작업을 취소할 가능성이 있을 때 사용할 수 있습니다.
const controller = new TaskController({ priority: 'background' });
controller.signal.addEventListener('prioritychange', (e) => {
console.log(`Changed: ${e.previousPriority} -> ${e.target.priority}`);
});
scheduler.postTask(async () => {
await processInitialData();
controller.setPriority('user-visible');
await processCriticalData();
}, { signal: controller.signal });
작업 도중에 controller.setPriority() 로 우선순위를 background -> user-visible로 동적으로 변경하는 코드입니다.
초기에는 우선순위가 background로 설정되었으나, 우선순위가 바뀌었을 때 감지하는 이벤트 리스너인 controller.signal.addEventListener 를 설정해두었기에
processInitialData()가 실행되고 우선순위가 변경됩니다. 이 변경이 이루어지면서 브라우저가 controller 작업을 더 빠르게, 사용자에게 잘 보이는 작업으로 인식하게끔 만듭니다.
예를 들면 초기에 리소스를 아끼고 나중에 중요한 데이터를 처리해야할 때 백그라운드로 작업을 시작하다가, 중간에 우선순위를 올리는 방법을 사용할 때 고려할 수 있습니다.
const controller = new TaskController();
scheduler.postTask(async () => {
const res = await fetch('/api/data');
return res.json();
}, { signal: controller.signal }).catch((e) => {
if (e.name === 'AbortError') {
console.log('작업이 중단되었습니다.');
}
});
controller.abort(); // 언제든 중단 가능
이 코드의 핵심은 예약된 작업을 중간에 중단할 수 있고, 중단된 경우에 AbortError 를 이용해서 적절한 에러처리를 수행할 수 있다는 점이에요
브라우저의 스케줄러 큐에 작업을 예약하고 내부에서 fetch를 진행합니다. 이때 signal : controller.signal 을 전달해서 작업이 controller.abort() 를 호출할때 중단될 수 있도록 세팅합니다.
만약 abort()에 의해 중단된다면 AbortError가 발생하고 catch 블록이 실행됩니다.
차단 하지 않고 나눠 처리하고 싶을 때