자바스크립트 Scheduler API

곽지욱·2025년 4월 25일

JS

목록 보기
11/11

웹 혹은 웹앱을 개발할 때 프로젝트의 복잡성이 높아질 수록 메인 스레드의 효율적인 관리는 JS를 사용한다면 성능 최적화의 핵심이 되는 부분입니다.

그 동안 보편적으로 setTimeout(0) 과 같은 기법으로 브라우저에게 작업을 잠시 양보해가며 작업을 쪼개서 실행해왔으나 이러한 방법은 우선순위나 세밀한 제어에 한계가 분명히 존재합니다.

이 한계를 극복하기 위해서 등장한 것이 바로 JS의 Scheduler API 입니다.

Scheduler API란?

Scheduler API는 작업의 우선순위를 명시하고, 메인 스레드에서 실행 타이밍을 보다 정밀하게 조절할 수 있도록 돕는 표준 API입니다.

기존 방식의 한계점

setTimeout(() => {
  processNextBatch();
}, 0);

위와 같은 방식은 브라우저의 Event Loop 에 넘겨 비동기 방식으로 처리하는 것이 가능하지만

  • 작업의 중요도 구분 불가
  • 백그라운드에서 실행할 작업과 사용자 입력 사이의 구분 부족
  • 체계적인 우선순위 불가능

과 같은 단점을 가지고 있습니다.

Scheduler API 의 핵심기능

1. scheduler.yield()

  • 현재 작업 흐름을 일시적으로 중단하고, 브라우저에게 컨트롤을 양보할 수 있습니다. 일종의 비동기 함수의 일종이며, 현재 작업을 중단하고, 브라우저가 사용자 이벤트 처리나 렌더링 같은 작업을 먼저 하도록 잠깐 쉬어주는 역할을 합니다.
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(); // 메인 스레드 양보
    }
  }
}
  • 이렇듯 현재 실행을 중단하고, 다른 중요한 작업이 있으면 먼저 처리하게 한 뒤 다시 돌아옵니다.

2.scheduler.postTask()

  • 작업을 명시적으로 등록하고 우선순위와 취소 신호 등을 함께 설정할 수 있습니다.
await scheduler.postTask(() => {
  processNextBatch();
}, { priority: 'background' });

작업 우선순위 같은 경우는 Scheduler API 에서 세 가지 작업 우선순위를 지원합니다.

  1. user-blocking : 사용자 입력 처리 UI업데이트 등 즉시 처리해야 하는 작업
  2. user-visible : 사용자게에 곧 보여질 데이터 등 중요하지만 긴급하지 않은 작업
  3. background : 분석 프리페칭 등 사용자 경험에 영향 없는 작업

실제 활용 예시

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'로 설정하였기에 브라우저가 다른 중요한 작업(렌더링, 사용자 입력 등) 먼저 처리한 후 실행되도록 유도합니다.

  • 사용자 인터랙션을 막지 않으면서 무거운 작업을 진행하고 싶을 때, 혹은 중간에 작업을 취소할 가능성이 있을 때 사용할 수 있습니다.


작업 중 우선순위 변경

  • 사용자 행동(이벤트)에 따라서 작업 중요도가 변하는 케이스도 존재할 수 있습니다. 이 때 TaskController 를 통해 동적으로 우선순위를 변경할 수 있습니다.
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 작업을 더 빠르게, 사용자에게 잘 보이는 작업으로 인식하게끔 만듭니다.

  • 예를 들면 초기에 리소스를 아끼고 나중에 중요한 데이터를 처리해야할 때 백그라운드로 작업을 시작하다가, 중간에 우선순위를 올리는 방법을 사용할 때 고려할 수 있습니다.

작업 중단(abort)

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 블록이 실행됩니다.

언제 Scheduler API를 사용해야 할까

  • 긴 연산을 메인 스레드를 차단 하지 않고 나눠 처리하고 싶을 때
  • 사용자의 상호작용에 영향을 주지 않도록 작업의 중요도를 조절하고 싶을 때
  • 여러 작업 간의 우선순위 체계를 도입하고 싶을 때

0개의 댓글