Scheduling API란 - The-Next-Web-Research-Lab/the-next-web-research-lab.github.io GitHub Wiki

Scheduling API란

출처

https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API

우선순위 작업 스케줄링 API

우선순위 작업 스케줄링 API는 웹 사이트 개발자 코드 또는 third-party 라이브러리 및 프레임워크에 정의된 작업이든 응용 프로그램에 속한 모든 작업의 우선순위를 지정하는 표준화된 방법을 제공합니다.

작업 우선 순위는 매우 세분화되어 있으며 작업이 사용자 상호 작용을 차단하는지 또는 사용자 경험에 영향을 미치는지 또는 백그라운드에서 실행할 수 있는지 여부를 기반으로 합니다. 개발자와 프레임워크는 API에서 정의한 광범위한 범주 내에서 보다 세분화된 우선 순위 지정 체계를 구현할 수 있습니다.

API는 Promise 기반이며 작업 우선 순위를 설정 및 변경하고, 스케줄러에 추가되는 작업을 지연하고, 작업을 중단하고, 우선 순위 변경 및 중단 이벤트를 모니터링하는 기능을 지원합니다.

이 페이지에는 다른 API 사양에 정의되었지만 작업 예약과 매우 밀접하게 관련된 navigator.scheduling.isInputPending() 메서드에 대한 정보도 포함되어 있습니다. 이 방법을 사용하면 이벤트 대기열에 대기 중인 입력 이벤트가 있는지 확인할 수 있으므로 작업 대기열을 효율적으로 처리하고 필요할 때만 기본 스레드에 양보합니다.

컨셉과 사용법

우선 순위가 지정된 작업 일정

Prioritized Task Scheduling API는 전역 개체의 scheduler 속성을 사용하여 window및 worker threads 모두에서 사용할 수 있습니다.

주요 API 메서드는 Scheduler.postTask()로, 콜백 함수("태스크")를 받아 함수의 반환 값으로 resolve하거나 오류와 함께 reject하는 promise을 반환합니다.

API의 가장 간단한 형태는 아래와 같습니다. 그러면 우선 순위가 고정되어 있고 중단할 수 없는 기본 우선 순위 user-visible이 생성됩니다.

const promise = scheduler.postTask(myTask);

메소드는 then을 사용하여 비동기적으로 해결을 기다릴 수 있는 promise를 반환하고, catch를 사용하여 태스크 콜백 함수에 의해(또는 태스크가 중단되었을 때) 발생한 오류를 캐치할 수 있습니다. 콜백 함수는 모든 종류의 함수일 수 있습니다.(아래에서는 화살표 함수를 시연합니다.)

scheduler
  .postTask(() => "Task executing")
  // Promise resolved: log task result when promise resolves
  .then((taskResult) => console.log(`${taskResult}`))
  // Promise rejected: log AbortError or errors thrown by task
  .catch((error) => console.error(`Error: ${error}`));

다음과 같이 await/async를 사용하여 동일한 작업이 대기될 수 있습니다.(참고: 이 작업은 즉시 호출 함수식(IIFE)에서 실행됩니다):

(async () => {
  try {
    const result = await scheduler.postTask(() => "Task executing");
    console.log(result);
  } catch (error) {
    // Log AbortError or error thrown in task function
    console.error(`Error: ${error}`);
  }
})();

기본 동작을 변경하려면 postTask() 메서드에 옵션 객체를 지정할 수도 있습니다. 옵션은 다음과 같습니다:

  • priority 변경 불가능한 특정 우선 순위를 지정할 수 있습니다. 한 번 설정하면 우선 순위를 변경할 수 없습니다.
  • signal 이를 통해 TaskSignal 또는 AbortSignal 중 하나인 신호를 지정할 수 있습니다. 신호는 컨트롤러와 연결되어 있으며, 이는 작업을 중단하는 데 사용될 수 있습니다. 작업이 변경 가능한 경우 TaskSignal를 사용하여 작업 우선 순위를 설정하고 변경할 수도 있습니다.
  • delay 이렇게 하면 작업이 스케줄링에 추가되기 전의 지연 시간(밀리초)을 지정할 수 있습니다.

우선 순위 옵션이 있는 위와 같은 예는 다음과 같습니다:

scheduler
  .postTask(() => "Task executing", { priority: "user-blocking" })
  .then((taskResult) => console.log(`${taskResult}`)) // Log the task result
  .catch((error) => console.error(`Error: ${error}`)); // Log any errors

작업 우선 순위

예약된 작업은 스케줄러 대기열에 추가된 순서에 따라 우선 순위로 실행됩니다.

아래에 나열된 세 가지 우선 순위(가장 높은 순서에서 가장 낮은 순서로 나열됨)는 다음과 같습니다:

user-blocking
사용자는 페이지와 상호 작용하는 것을 중지하는 작업. 여기에는 페이지를 사용할 수 있는 지점까지 렌더링하거나 사용자 입력에 응답하는 작업이 포함됩니다.

user-visible
사용자가 볼 수 있지만 사용자 작업을 반드시 차단할 필요는 없는 작업입니다. 여기에는 페이지의 비필수적인 부분(예: 비필수적인 이미지 또는 애니메이션)을 렌더링하는 것이 포함될 수 있습니다.

이것이 기본 우선 순위입니다.

background
시간에 중요하지 않는 작업입니다. 여기에는 렌더링에 필요하지 않는 로그 처리 또는 third-party 라이브러리 초기화가 포함될 수 있습니다.

가변 및 불변 작업 우선 순위

작업 우선순위를 변경할 필요가 없는 많은 사용 사례가 있지만 다른 경우에는 변경해야 합니다. 예를 들어, carousel이 뷰 영역으로 스크롤되면 이미지 가져오기가 background 태스크에서 user-visible로 변경될 수 있습니다.

태스크 우선순위는 Scheduler.postTask()로 전달되는 인수에 따라 스태틱(불변) 또는 다이내믹(변경 가능)으로 설정할 수 있습니다.

options.priority 인수에 값이 지정되어 있는 경우 작업 우선순위는 불변입니다. 지정된 값은 작업 우선순위에 사용되며 변경할 수 없습니다.

우선순위는 options.signal 인수에 options.priority가 전달되고 TaskSignal이 설정되지 않는 경우에만 변경 가능합니다. 이 경우 작업은 signal priority로부터 초기 priority를 취득한 다음 신호와 관련된 컨트롤러상에서 TaskController.setPriority()을 호출하여 우선순위를 변경할 수 있습니다.

우선순위가 options.priority로 설정되어 있지 않은 경우 또는 TaskSignaloptions.signal에 전달함으로써 우선순위가 설정되어 있지 않은 경우는 기본적으로 user-visible이 됩니다(정의상 불변합니다).

중단할 필요가 있는 작업에서는 options.signalTaskSignal 또는 AbortSignal 중 하나로 설정해야 한다는 점에 주의하십시오. 단 불변의 우선순위를 가진 작업의 경우 AbortSignal은 신호를 사용하여 작업 우선순위를 변경할 수 없음을 보다 명확하게 보여줍니다.

isInputPending()

isInputPending() API는 작업 실행을 돕기 위한 것으로, 사용자가 임의의 간격으로 앱과 상호 작용하려고 할 때만 메인 스레드를 양보하여 작업 실행자를 더 효율적으로 만들 수 있습니다.

예를 들어 이것이 무엇을 의미하는지 설명해 보겠습니다. 우선순위가 거의 같은 여러 작업이 있을 경우 유지 보수, 디버깅 및 기타 여러 가지 이유를 지원하기 위해 별도의 기능으로 분해하는 것이 좋습니다.

예:

function main() {
  a();
  b();
  c();
  d();
  e();
}

그러나 이러한 구조는 메인 스레드 차단에 도움이 되지 않습니다. 5개의 작업을 모두 하나의 main function 내부에서 실행하기 때문에 브라우저는 이를 모두 하나의 작업으로 실행합니다.

이를 처리하기 위해 주기적으로 함수를 실행하여 코드가 메인 스레드에 양보하도록 도움 줄 수 있습니다. 이는 코드가 여러 작업으로 분할되고 브라우저가 UI 업데이트와 같은 우선 순위가 높은 작업을 처리할 수 있는 기회가 제공됩니다. 이 함수의 일반적인 패턴은 setTimeout()을 사용하여 실행을 별도의 작업으로 연기합니다.

function yield() {
  return new Promise((resolve) => {
    setTimeout(resolve, 0);
  });
}

이는 각 작업이 실행된 후 메인 스레드에 양보하기 위해 다음과 같은 task runner pattern 내부에서 사용할 수 있습니다:

async function main() {
  // Create an array of functions to run
  const tasks = [a, b, c, d, e];

  // Loop over the tasks
  while (tasks.length > 0) {
    // Shift the first task off the tasks array
    const task = tasks.shift();

    // Run the task
    task();

    // Yield to the main thread
    await yield();
  }
}

이렇게 하면 메인 스레드 차단 문제에 도움이 되지만 사용자가 페이지와 상호 작용하려고 할 때만 navigator.scheduling.isInputPending()를 사용하여 yield() function를 실행 할 수 있습니다.

async function main() {
  // Create an array of functions to run
  const tasks = [a, b, c, d, e];

  while (tasks.length > 0) {
    // Yield to a pending user input
    if (navigator.scheduling.isInputPending()) {
      await yield();
    } else {
      // Shift the first task off the tasks array
      const task = tasks.shift();

      // Run the task
      task();
    }
  }
}

이를 통해 사용자가 페이지와 적극적으로 상호 작용하고 있을 때 메인 스레드를 차단하는 것을 피할 수 있어 사용자 경험이 보다 원활해질 수 있습니다. 단, 필요한 경우에만 양보함으로써 처리할 사용자 입력이 없는 경우에도 현재 작업을 계속 수행할 수 있습니다. 이렇게 하면 작업이 현재 작업 이후에 예약된 다른 중요하지 않은 브라우저 시작 작업 뒤에 대기열 뒤에 배치되는 것도 방지됩니다.

Interfaces

Scheduler
예약할 우선 순위 작업을 추가하는 postTask() 방법이 포함되어 있습니다. 이 인터페이스의 인스턴스는 Window또는 WorkerGlobalScope 글로벌 객체(this.scheduler)에서 사용할 수 있습니다.

Scheduling
이벤트 큐에 보유 중인 입력 이벤트가 있는지 확인하는 isInputPending() 메서드가 포함되어 있습니다.

TaskController
작업을 중단하고 우선 순위를 변경할 수 있습니다.

TaskSignal
작업을 중단하고 필요한 경우 TaskController 객체를 사용하여 우선 순위를 변경할 수 있는 신호 객체입니다.

TaskPriorityChangeEvent
작업 우선 순위가 병결될 때 전송되는 prioritychange 이벤트의 인터페이스입니다.

참고: 작업 우선 순위를 변경할 필요가 없는 경우 TaskControllerTaskSignal 대신 AbortController 및 관련 AbortSignal를 사용할 수 있습니다.

다른 인터페이스로의 확장

Navigator.scheduling
이 속성은 Scheduling.isInputPending() 메서드를 사용하기 위한 시작점입니다.

Window.scheduler
이 속성을 Scheduler.postTask() 메서드를 사용하기 위한 엔트리 포인트입니다. 이것은 WorkerGlobalScopeWindow에 내장되어 있으며, 대부분의 스코프에서 this를 통해 Scheduler 인스턴스를 사용할 수 있게 되었습니다.

테스크 스케줄링 예제

아래의 예에서는 mylog()를 사용해 텍스트 영역에 적습니다. 로그 영역과 메서드 코드는 일반적으로 더 관련성이 높은 코드에서 주의를 기울지이 않도록 숨깁니다.

// hidden logger code - simplifies example
let log = document.getElementById("log");
function mylog(text) {
  log.textContent += `${text}\n`;
}

기능 체크

현재 스코프에 노출된 global this에서 scheduler 속성을 테스트하여 우선 순위 작업 스케줄링이 지원되는지 확인합니다.

API가 이 브라우저에서 지원되는 경우 아래 코드는 "Feature: Supported"을 출력합니다.

// Check that feature is supported
if ("scheduler" in this) {
  mylog("Feature: Supported");
} else {
  mylog("Feature: NOT Supported");
}

기본 사용법

태스크는 Scheduler.postTask()를 사용하여 게시되며, 첫 번째 인수에서 콜백 함수(태스크)를 지정하고, 태스크 우선 순위, 지연을 지정하는 데 사용될 수 있는 선택적인 두 번째 인수를 사용합니다. 메소드는 콜백 함수의 반환 값으로 해결되는 Promise를 반환하거나, 중단 오류 또는 함수에 던져진 오류 중 하나로 거부합니다.

promise를 반환하기 때문에 Scheduler.postTask()은 다른 promise과 채인으로 연결될 수 있습니다. 아래에서는 then을 사용하여 해결할 promise을 기다리는 방법을 보여줍니다. 이것은 기본 우선 순위(user-visible)를 사용합니다.

// A function that defines a task
function myTask() {
  return "Task 1: user-visible";
}

if ("scheduler" in this) {
  // Post task with default priority: 'user-visible' (no other options)
  // When the task resolves, Promise.then() logs the result.
  scheduler.postTask(myTask).then((taskResult) => mylog(`${taskResult}`));
}

이 메서드는 async function 내부의 await와 함께 사용할 수도 있습니다. 아래 코드는 이 접근 방식을 사용하여 user-blocking 작업을 기다리는 방법을 보여줍니다.

function myTask2() {
  return "Task 2: user-blocking";
}

async function runTask2() {
  const result = await scheduler.postTask(myTask2, {
    priority: "user-blocking",
  });
  mylog(result); // Logs 'Task 2: user-blocking'.
}
runTask2();

경우에 따라 완료될 때까지 전혀 기다릴 필요가 없을 수도 있습니다. 여기에 있는 많은 예제는 단순하게 작업이 실행될 때 결과를 기록합니다.

// A function that defines a task
function mytask3() {
  mylog("Task 3: user-visible");
}

if ("scheduler" in this) {
  // Post task and log result when it runs
  scheduler.postTask(mytask3);
}

아래 로그의 위의 세 가지 작업의 출력을 보여줍니다. 작업이 실행되는 순서는 우선 순위, 그리고 선언 순서에 따라 달라집니다.

Task 2: user-blocking
Task 1: user-visible
Task 3: user-visible

영구적인 우선순위

두 번째 인수 옵션에서 priority 매개 변수를 사용하여 작업 우선 순위를 설정할 수 있습니다. 이렇게 설정된 우선 순위는 변경할 수 없습니다(변경 할 수 없음).

아래에는 세 가지 작업으로 구성된 두 그룹을 보여줍니다. 마지막 작업은 기본 우선 순위를 가집니다. 실행 시 각 작업은 단순히 예상된 순서를 기록합니다(실행 순서를 표시할 필요가 없기 때문에 결과를 기다리고 있지 않습니다).

if ("scheduler" in this) {
  // three tasks, in reverse order of priority
  scheduler.postTask(() => mylog("background 1"), { priority: "background" });
  scheduler.postTask(() => mylog("user-visible 1"), { priority: "user-visible" });
  scheduler.postTask(() => mylog("user-blocking 1"), { priority: "user-blocking" });

  // three more tasks, in reverse order of priority
  scheduler.postTask(() => mylog("background 2"), { priority: "background" });
  scheduler.postTask(() => mylog("user-visible 2"), { priority: "user-visible" });
  scheduler.postTask(() => mylog("user-blocking 2"), { priority: "user-blocking" });

  // Task with default priority: user-visible
  scheduler.postTask(() => mylog("user-visible 3 (default)"));
}

아래 출력은 태스크가 우선 순위와 선언 순서로 실행되는 것을 보여줍니다.

user-blocking 1
user-blocking 2
user-visible 1
user-visible 2
user-visible 3 (default)
background 1
background 2

작업 우선 순위 변경

태스크 우선 순위는 또한 선택적인 두 번째 인수에서 postTask()로 전달된 TaskSignal로부터 그들의 초기 값을 가져올 수 있습니다. 이러한 방식으로 설정되면, 태스크 우선 순위는 신호와 연관된 컨트롤러를 사용하여 변경될 수 있습니다.

참고: 신호를 사용하여 작업 우선 순위를 설정하고 변경하는 것은 postTask()에 대한 options.priority 인자가 설정되지 않은 경우와 options.signalTaskSignal인 경우 (AbortSignal가 아닌 경우)에만 작동합니다.

아래 코드는 먼저 TaskController에서 신호의 초기 우선 순위를 user-blocking로 설정하여 TaskController() constructor를 생성하는 방법을 보여줍니다.

그런 다음 코드는 컨트롤러의 신호에 이벤트 수신기를 추가하기 위해 addEventListener()를 사용합니다(이벤트 핸들러를 추가하기 위해 TaskSignal.onprioritychange 속성을 사용할 수 있음). 이벤트 핸들러는 이벤트에 대한 previousPriority를 사용하여 원래 우선 순위를 얻고 이벤트 대상에 대한 TaskSignal.priority를 사용하여 새로운/현재 우선 순위를 얻습니다.

그런 다음 작업이 게시되어 신호를 전달한 다음 컨트롤러에서 TaskController.setPriority()를 호출하여 우선순위를 background로 즉시 변경합니다.

if ("scheduler" in this) {
  // Create a TaskController, setting its signal priority to 'user-blocking'
  const controller = new TaskController({ priority: "user-blocking" });
  
  // Listen for 'prioritychange' events on the controller's signal.
  controller.signal.addEventListener("prioritychange", (event) => {
    const previousPriority = event.previousPriority;
    const newPriority = event.target.priority;
    mylog(`Priority changed from ${previousPriority} to ${newPriority}.`);
  });

  // Post task using the controller's signal.
  // The signal priority sets the initial priority of the task
  scheduler.postTask(() => mylog("Task 1"), { signal: controller.signal });

  // Change the priority to 'background' using the controller
  controller.setPriority("background");
}

아래 출력은 우선 순위가 user-blocking에서 background로 성공적으로 변경되었음을 보여줍니다. 이 경우 우선 순위는 작업이 실행되기 전에 변경되지만 작업이 실행되는 동안에도 동일하게 변경될 수 있습니다.

Priority changed from user-blocking to background.
Task 1

작업 취소

TaskControllerAbortController를 사용하여 정확히 같은 방식으로 작업을 중단할 수 있습니다. 유일한 차이점은 작업 우선 순위도 설정하려면 TaskController를 사용해야 한다는 것입니다.

아래 코드는 컨트롤러를 생성하고 해당 신호를 작업에 전달합니다. 그러면 작업이 즉시 중단됩니다. 그러면 AbortError와 함께 promise가 거부되고 catch 블록에 걸려 기록됩니다. 또한 TaskSignal 또는 AbortSignal에서 발생한 abort 이벤트를 듣고 중단을 기록할 수도 있습니다.

if ("scheduler" in this) {
  // Declare a TaskController with default priority
  const abortTaskController = new TaskController();
  // Post task passing the controller's signal
  scheduler
    .postTask(() => mylog("Task executing"), {
       signal: abortTaskController.signal,
    })
    .then((taskResult) => mylog(`${taskResult}`))
    .catch((error) => mylog(`Error: ${error}`);

  // Abort the task
  abortTaskController.abort();
}

아래 로그에는 중단된 작업이 표시됩니다.

Error: AbortError: signal is aborted without reason.

작업 지연

postTask()options.deplay 파라미터에 정수 밀리초를 지정하여 작업을 지연시킬 수 있습니다. 이렇게 하면 setTimeout()를 사용하여 생성될 수 있는 것처럼 시간 초과 시에 작업이 우선 순위 큐에 효과적으로 추가됩니다. delay은 작업이 스케줄러에 추가되기 전까지의 최소 시간입니다. 시간이 더 길 수도 있습니다.

아래 코드는 지연과 함께 (화살표 함수로) 추가된 두 가지 작업을 보여줍니다.

if ("scheduler" in this) {
  // Post task as arrow function with delay of 2 seconds
  scheduler
    .postTask(() => "Task delayed by 2000ms", { delay: 2000 })
    .then((taskResult) => myLog(`${taskResult}`));
  scheduler
    .postTask(() => "Next task should complete in about 2000ms", { delay: 1 })
    .then((taskResult) => mylog(`${taskResult}`));
}

두 번째 문자열은 약 2초 후에 로그에 표시됩니다.

Next task should complete in about 2000ms
Task delayed by 2000ms

브라우저 지원 범위

⚠️ **GitHub.com Fallback** ⚠️