requestIdleCallback 사용

많은 사이트와 앱에는 실행할 스크립트가 많습니다. JavaScript는 최대한 빨리 실행되어야 하지만 동시에 사용자의 작업을 방해해서는 안 됩니다. 사용자가 페이지를 스크롤할 때 애널리틱스 데이터를 전송하거나 사용자가 버튼을 탭하는 동안 DOM에 요소를 추가하면 웹 앱이 응답하지 않아 사용자 환경이 저하될 수 있습니다.

requestIdleCallback을 사용하여 비필수 작업 예약

다행히 이제 이를 도와줄 API(requestIdleCallback)가 있습니다. requestAnimationFrame를 채택하여 애니메이션을 적절하게 예약하고 60fps를 달성할 가능성을 극대화할 수 있었던 것처럼 requestIdleCallback는 프레임 끝에 여유 시간이 있거나 사용자가 비활성 상태일 때 작업을 예약합니다. 즉, 사용자의 작업을 방해하지 않고 작업을 할 수 있습니다. Chrome 47부터 사용할 수 있으므로 지금 바로 Chrome Canary를 사용하여 사용해 보세요. 이 기능은 실험용이며 사양은 아직 변경될 수 있으므로 향후 변경될 수 있습니다.

requestIdleCallback을 사용해야 하는 이유는 무엇인가요?

중요하지 않은 작업을 직접 예약하는 것은 매우 어렵습니다. requestAnimationFrame 콜백이 실행된 후에는 스타일 계산, 레이아웃, 페인트, 기타 브라우저 내부 작업을 실행해야 하므로 남은 프레임 시간을 정확하게 파악할 수 없습니다. 자체 제작 솔루션은 이러한 요소를 고려할 수 없습니다. 사용자가 어떤 식으로든 상호작용하지 않도록 하려면 모든 종류의 상호작용 이벤트 (scroll, touch, click)에도 리스너를 연결해야 합니다. 이는 리스너가 기능상 필요하지 않더라도 사용자가 상호작용하고 있지 않다는 것을 확신할 수 있도록 필요한 것입니다. 반면 브라우저는 프레임 종료 시 사용 가능한 시간을 정확히 알고 있으며 사용자가 상호작용하는지 여부도 알고 있으므로 requestIdleCallback를 통해 여유 시간을 최대한 효율적으로 활용할 수 있는 API를 얻을 수 있습니다.

좀 더 자세히 살펴보고 이를 활용하는 방법을 알아보겠습니다.

requestIdleCallback 확인

requestIdleCallback은 아직 초기 단계이므로 사용하기 전에 사용 가능한지 확인해야 합니다.

if ('requestIdleCallback' in window) {
    // Use requestIdleCallback to schedule work.
} else {
    // Do what you’d do today.
}

shim을 실행할 수도 있으며, 이를 위해서는 setTimeout로 돌아가야 합니다.

window.requestIdleCallback =
    window.requestIdleCallback ||
    function (cb) {
    var start = Date.now();
    return setTimeout(function () {
        cb({
        didTimeout: false,
        timeRemaining: function () {
            return Math.max(0, 50 - (Date.now() - start));
        }
        });
    }, 1);
    }

window.cancelIdleCallback =
    window.cancelIdleCallback ||
    function (id) {
    clearTimeout(id);
    }

setTimeoutrequestIdleCallback처럼 유휴 시간을 알 수 없으므로 사용하기에는 좋지 않지만 requestIdleCallback를 사용할 수 없는 경우 함수를 직접 호출하므로 이 방식으로 쉬밍해도 나쁘지 않습니다. 쉬름을 사용하면 requestIdleCallback를 사용할 수 있는 경우 호출이 자동으로 리디렉션됩니다.

하지만 지금은 존재한다고 가정해 보겠습니다.

requestIdleCallback 사용

requestIdleCallback를 호출하는 것은 콜백 함수를 첫 번째 매개변수로 사용한다는 점에서 requestAnimationFrame와 매우 유사합니다.

requestIdleCallback(myNonEssentialWork);

myNonEssentialWork가 호출되면 남은 작업 시간을 나타내는 숫자를 반환하는 함수가 포함된 deadline 객체가 제공됩니다.

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0)
    doWorkIfNeeded();
}

timeRemaining 함수를 호출하여 최신 값을 가져올 수 있습니다. timeRemaining()가 0을 반환하면 아직 할 일이 더 있는 경우 다른 requestIdleCallback를 예약할 수 있습니다.

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0 && tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

함수 보장

일이 정말 바쁘면 어떻게 하나요? 콜백이 호출되지 않을까 우려될 수 있습니다. requestIdleCallbackrequestAnimationFrame와 유사하지만 두 번째 매개변수(시간 초과 속성이 있는 옵션 객체)를 선택적으로 사용한다는 점에서 다릅니다. 이 제한 시간은 설정된 경우 브라우저에 콜백을 실행해야 하는 시간을 밀리초로 제공합니다.

// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });

시간 초과 실행으로 인해 콜백이 실행되면 다음 두 가지 사항을 확인할 수 있습니다.

  • timeRemaining()는 0을 반환합니다.
  • deadline 객체의 didTimeout 속성이 true가 됩니다.

didTimeout가 true인 경우 작업을 실행하고 완료하는 것이 가장 좋습니다.

function myNonEssentialWork (deadline) {

    // Use any remaining time, or, if timed out, just run through the tasks.
    while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
            tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

이 시간 제한으로 인해 사용자에게 중단이 발생할 수 있으므로 (작업으로 인해 앱이 응답하지 않거나 버벅거림이 될 수 있음) 이 매개변수를 설정하는 데 신중을 기해야 합니다. 가능한 경우 브라우저가 콜백을 호출할 시점을 결정하도록 합니다.

애널리틱스 데이터 전송에 requestIdleCallback 사용

requestIdleCallback를 사용하여 애널리틱스 데이터를 전송하는 방법을 살펴보겠습니다. 이 경우 탐색 메뉴를 탭하는 등의 이벤트를 추적하는 것이 좋습니다. 하지만 일반적으로 화면에 애니메이션으로 표시되므로 이 이벤트를 Google 애널리틱스로 즉시 전송하지 않는 것이 좋습니다. 전송할 이벤트 배열을 생성하여 향후 특정 시점에 전송하도록 요청합니다.

var eventsToSend = [];

function onNavOpenClick () {

    // Animate the menu.
    menu.classList.add('open');

    // Store the event for later.
    eventsToSend.push(
    {
        category: 'button',
        action: 'click',
        label: 'nav',
        value: 'open'
    });

    schedulePendingEvents();
}

이제 requestIdleCallback를 사용하여 대기 중인 이벤트를 처리해야 합니다.

function schedulePendingEvents() {

    // Only schedule the rIC if one has not already been set.
    if (isRequestIdleCallbackScheduled)
    return;

    isRequestIdleCallbackScheduled = true;

    if ('requestIdleCallback' in window) {
    // Wait at most two seconds before processing events.
    requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
    } else {
    processPendingAnalyticsEvents();
    }
}

시간 제한을 2초로 설정했지만 이 값은 애플리케이션에 따라 다릅니다. 분석 데이터의 경우 데이터가 특정 시점에만 보고되는 것이 아니라 적절한 기간 내에 보고되도록 제한 시간을 사용하는 것이 좋습니다.

마지막으로 requestIdleCallback가 실행할 함수를 작성해야 합니다.

function processPendingAnalyticsEvents (deadline) {

    // Reset the boolean so future rICs can be set.
    isRequestIdleCallbackScheduled = false;

    // If there is no deadline, just run as long as necessary.
    // This will be the case if requestIdleCallback doesn’t exist.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) {
    var evt = eventsToSend.pop();

    ga('send', 'event',
        evt.category,
        evt.action,
        evt.label,
        evt.value);
    }

    // Check if there are more events still to send.
    if (eventsToSend.length > 0)
    schedulePendingEvents();
}

이 예에서는 requestIdleCallback가 없으면 애널리틱스 데이터를 즉시 전송해야 한다고 가정했습니다. 하지만 프로덕션 애플리케이션에서는 상호작용과 충돌하여 버벅거림을 일으키지 않도록 제한 시간으로 전송을 지연하는 것이 좋습니다.

requestIdleCallback을 사용하여 DOM 변경

requestIdleCallback가 실적에 큰 도움이 되는 또 다른 상황은 점점 늘어나는 지연 로드 목록의 끝에 항목을 추가하는 등 중요하지 않은 DOM 변경사항을 적용해야 하는 경우입니다. requestIdleCallback가 일반적인 프레임에 실제로 어떻게 들어맞는지 살펴보겠습니다.

일반적인 프레임입니다.

브라우저가 특정 프레임에서 콜백을 실행하기에 너무 바쁠 수 있으므로 프레임 끝에 추가 작업을 할 수 있는 여유 시간이 있을 것이라고 예상해서는 안 됩니다. 따라서 프레임마다 실행되는 setImmediate과는 다릅니다.

프레임 끝에서 콜백이 실행되면 실행되며, 현재 프레임이 커밋된 후에 실행되도록 예약됩니다. 즉, 스타일 변경사항이 적용되고 중요한 점은 레이아웃이 계산됩니다. 유휴 콜백 내에서 DOM을 변경하면 이러한 레이아웃 계산이 무효화됩니다. 다음 프레임에 getBoundingClientRect, clientWidth 등의 레이아웃 읽기가 있는 경우 브라우저는 잠재적인 성능 병목 현상인 강제 동기식 레이아웃을 실행해야 합니다.

유휴 콜백에서 DOM 변경을 트리거하지 않는 또 다른 이유는 DOM 변경의 시간적 영향이 예측할 수 없으므로 브라우저에서 제공한 기한을 쉽게 넘길 수 있기 때문입니다.

requestAnimationFrame 콜백 내에서만 DOM을 변경하는 것이 좋습니다. 이러한 유형의 작업을 염두에 두고 브라우저에서 예약하기 때문입니다. 즉, 코드는 문서 프래그먼트를 사용해야 하며, 이 프래그먼트는 다음 requestAnimationFrame 콜백에 추가될 수 있습니다. VDOM 라이브러리를 사용하는 경우 requestIdleCallback를 사용하여 변경사항을 적용하지만 유휴 콜백이 아닌 다음 requestAnimationFrame 콜백에서 DOM 패치를 적용합니다.

이를 염두에 두고 코드를 살펴보겠습니다.

function processPendingElements (deadline) {

    // If there is no deadline, just run as long as necessary.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    if (!documentFragment)
    documentFragment = document.createDocumentFragment();

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && elementsToAdd.length > 0) {

    // Create the element.
    var elToAdd = elementsToAdd.pop();
    var el = document.createElement(elToAdd.tag);
    el.textContent = elToAdd.content;

    // Add it to the fragment.
    documentFragment.appendChild(el);

    // Don't append to the document immediately, wait for the next
    // requestAnimationFrame callback.
    scheduleVisualUpdateIfNeeded();
    }

    // Check if there are more events still to send.
    if (elementsToAdd.length > 0)
    scheduleElementCreation();
}

여기서는 요소를 만들고 textContent 속성을 사용하여 채우지만 요소 생성 코드는 더 복잡할 수 있습니다. 요소를 만든 후 scheduleVisualUpdateIfNeeded이 호출되어 문서 프래그먼트를 본문에 추가하는 단일 requestAnimationFrame 콜백을 설정합니다.

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

function appendDocumentFragment() {
    // Append the fragment and reset.
    document.body.appendChild(documentFragment);
    documentFragment = null;
}

이제 DOM에 항목을 추가할 때 발생하는 버벅거림이 훨씬 줄어듭니다. 멋집니다!

FAQ

  • 폴리필이 있나요? 안타깝게도 그렇지 않습니다. 하지만 setTimeout로 투명하게 리디렉션하려면 시임이 있습니다. 이 API가 존재하는 이유는 웹 플랫폼에 실질적인 격차를 줄 수 있기 때문입니다. 활동 부족을 추론하는 것은 어렵지만 프레임 끝의 여유 시간을 결정하는 JavaScript API는 없으므로 기껏해야 추측해야 합니다. setTimeout, setInterval 또는 setImmediate와 같은 API를 사용하여 작업을 예약할 수 있지만 requestIdleCallback와 같은 방식으로 사용자 상호작용을 피하기 위해 시간이 지정되지는 않습니다.
  • 기한을 초과하면 어떻게 되나요? timeRemaining()가 0을 반환하지만 더 오래 실행하도록 선택하면 브라우저에서 작업이 중단될 염려 없이 그렇게 할 수 있습니다. 하지만 브라우저는 사용자에게 원활한 환경을 제공하기 위해 기한을 정해 두므로, 특별한 이유가 없는 한 항상 기한을 준수해야 합니다.
  • timeRemaining()가 반환하는 최대 값이 있나요? 예, 현재 50ms입니다. 응답성 있는 애플리케이션을 유지하려면 사용자 상호작용에 대한 모든 응답을 100ms 미만으로 유지해야 합니다. 사용자가 상호작용하는 경우 대부분의 경우 50밀리초 창을 통해 유휴 콜백이 완료되고 브라우저가 사용자의 상호작용에 응답할 수 있습니다. 브라우저에서 실행할 시간이 충분하다고 판단하는 경우 여러 유휴 콜백이 연달아 예약될 수 있습니다.
  • requestIdleCallback에서 수행하면 안 되는 작업이 있나요? 비교적 예측 가능한 특성을 가진 작은 청크 (마이크로태스크)로 작업을 수행하는 것이 이상적입니다. 예를 들어, 특히 DOM을 변경하면 실행 시간을 예측할 수 없게 됩니다. 스타일 계산, 레이아웃, 페인트, 합성을 트리거하기 때문입니다. 따라서 위에서 제안한 대로 requestAnimationFrame 콜백에서만 DOM을 변경해야 합니다. 주의해야 할 또 다른 사항은 프라미스 해결 (또는 거부)입니다. 유휴 콜백이 완료된 후 더 이상 시간이 남지 않더라도 콜백이 즉시 실행되기 때문입니다.
  • 프레임 끝에 항상 requestIdleCallback가 표시되나요? 아니요, 항상 그런 것은 아닙니다. 브라우저는 프레임 끝에 여유 시간이 있거나 사용자가 비활성 상태일 때마다 콜백을 예약합니다. 콜백이 프레임별로 호출될 것으로 예상해서는 안 되며, 콜백이 특정 기간 내에 실행되어야 하는 경우 제한 시간을 사용해야 합니다.
  • requestIdleCallback 콜백을 여러 개 가질 수 있나요? 예, 여러 개의 requestAnimationFrame 콜백을 가질 수 있는 것처럼 할 수 있습니다. 하지만 첫 번째 콜백이 콜백 중에 남은 시간을 사용하면 다른 콜백에 사용할 시간이 더 이상 남지 않습니다. 그러면 다른 콜백은 브라우저가 다음에 유휴 상태가 될 때까지 기다려야 실행할 수 있습니다. 완료하려는 작업에 따라 유휴 콜백을 하나만 두고 작업을 나누는 것이 더 나을 수 있습니다. 또는 시간 제한을 사용하여 콜백이 시간 부족을 겪지 않도록 할 수 있습니다.
  • 다른 콜백 내에 새 유휴 콜백을 설정하면 어떻게 되나요? 새 유휴 콜백은 현재 프레임이 아닌 다음 프레임에서 시작하여 최대한 빨리 실행되도록 예약됩니다.

자리를 비우세요!

requestIdleCallback는 사용자에게 방해가 되지 않으면서 코드를 실행할 수 있는 멋진 방법입니다. 사용하기 쉽고 매우 유연합니다. 하지만 아직 초기 단계이고 사양이 완전히 확정되지는 않았으므로 언제든지 의견을 보내주시기 바랍니다.

Chrome Canary에서 기능을 확인하고, 프로젝트에 사용해 보고, 어떻게 진행되고 있는지 알려주세요.