취소 가능한 가져오기

제이크 아치볼드
제이크 아치볼드

'가져오기 취소'와 관련된 원래의 GitHub 문제는 2015년에 발표되었습니다. 2017년 (올해)에서 2015년을 빼버리면 2점을 얻습니다. 2015년은 사실 '영원'하기 전이었기 때문에 이는 수학의 버그임을 보여줍니다.

2015년에는 진행 중인 가져오기 취소를 처음으로 연구하기 시작했고, GitHub 댓글 780개, 2개의 잘못된 시작, 5개의 pull 요청 후 마침내 브라우저에서 취소 가능한 가져오기 랜딩을 수행했으며 첫 번째는 Firefox 57입니다.

업데이트: 아니요, 제가 틀렸어요. Edge 16이 먼저 취소 지원 서비스와 함께 착륙했습니다. Edge팀에 축하의 말씀을 전합니다!

나중에 이 기록에 대해서 알아보겠습니다. 먼저 API는 다음과 같습니다.

컨트롤러 + 신호 조작

AbortControllerAbortSignal 소개:

const controller = new AbortController();
const signal = controller.signal;

컨트롤러에는 다음 한 가지 메서드만 있습니다.

controller.abort();

이렇게 하면 다음과 같은 신호를 보냅니다.

signal.addEventListener('abort', () => {
    // Logs true:
    console.log(signal.aborted);
});

이 API는 DOM 표준에 의해 제공되며, 이것이 전체 API입니다. 이 API는 의도적으로 일반적이므로 다른 웹 표준 및 JavaScript 라이브러리에서 사용할 수 있습니다.

신호 취소 및 가져오기

가져오기에는 AbortSignal이 걸릴 수 있습니다. 예를 들어 5초 후에 가져오기 제한 시간을 설정하는 방법은 다음과 같습니다.

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort(), 5000);

fetch(url, { signal }).then(response => {
    return response.text();
}).then(text => {
    console.log(text);
});

가져오기를 취소하면 요청과 응답이 모두 취소되므로 응답 본문(예: response.text()) 읽기도 모두 취소됩니다.

데모 – 현재 이 기능을 지원하는 브라우저는 Firefox 57뿐입니다. 또한 디자인 기술이 있는 사람이 데모를 만드는 데 관여하지 않았으므로 만반의 준비를 갖추세요.

또는 요청 객체에 신호를 제공한 후 나중에 가져오기 위해 전달할 수 있습니다.

const controller = new AbortController();
const signal = controller.signal;
const request = new Request(url, { signal });

fetch(request);

request.signalAbortSignal이기 때문에 작동합니다.

중단된 가져오기에 반응

비동기 작업을 취소하면 프로미스가 AbortError라는 DOMException와 함께 거부됩니다.

fetch(url, { signal }).then(response => {
    return response.text();
}).then(text => {
    console.log(text);
}).catch(err => {
    if (err.name === 'AbortError') {
    console.log('Fetch aborted');
    } else {
    console.error('Uh oh, an error!', err);
    }
});

사용자가 작업을 취소해도 오류 메시지를 표시하지 않는 것이 좋습니다. 사용자가 요청한 작업을 성공적으로 수행하면 '오류'가 되지 않기 때문입니다. 이를 방지하려면 위와 같은 if 문을 사용하여 취소 오류를 구체적으로 처리하세요.

다음은 사용자에게 콘텐츠를 로드하는 버튼과 취소할 수 있는 버튼을 제공하는 예입니다. 가져오기 오류가 발생하면 취소 오류가 아닌 다음과 같은 오류가 표시됩니다.

// This will allow us to abort the fetch.
let controller;

// Abort if the user clicks:
abortBtn.addEventListener('click', () => {
    if (controller) controller.abort();
});

// Load the content:
loadBtn.addEventListener('click', async () => {
    controller = new AbortController();
    const signal = controller.signal;

    // Prevent another click until this fetch is done
    loadBtn.disabled = true;
    abortBtn.disabled = false;

    try {
    // Fetch the content & use the signal for aborting
    const response = await fetch(contentUrl, { signal });
    // Add the content to the page
    output.innerHTML = await response.text();
    }
    catch (err) {
    // Avoid showing an error message if the fetch was aborted
    if (err.name !== 'AbortError') {
        output.textContent = "Oh no! Fetching failed.";
    }
    }

    // These actions happen no matter how the fetch ends
    loadBtn.disabled = false;
    abortBtn.disabled = true;
});

데모 – 현재 이 기능을 지원하는 브라우저는 Edge 16과 Firefox 57입니다.

하나의 신호, 많은 가져오기

단일 신호를 사용하여 한 번에 여러 가져오기를 취소할 수 있습니다.

async function fetchStory({ signal } = {}) {
    const storyResponse = await fetch('/story.json', { signal });
    const data = await storyResponse.json();

    const chapterFetches = data.chapterUrls.map(async url => {
    const response = await fetch(url, { signal });
    return response.text();
    });

    return Promise.all(chapterFetches);
}

위의 예에서는 최초 가져오기와 병렬 챕터 가져오기에 동일한 신호가 사용됩니다. fetchStory 사용 방법은 다음과 같습니다.

const controller = new AbortController();
const signal = controller.signal;

fetchStory({ signal }).then(chapters => {
    console.log(chapters);
});

이 경우 controller.abort()를 호출하면 진행 중인 가져오기가 취소됩니다.

앞으로

기타 브라우저

Edge는 이를 먼저 출시하기에 매우 좋았고, Firefox는 그 뒤를 잇는 인기입니다. 엔지니어가 사양을 작성하는 동안 테스트 모음에서 구현했습니다. 다른 브라우저의 경우 다음 티켓을 따르세요.

서비스 워커에서

서비스 워커 부품의 사양을 완성해야 하지만 계획은 다음과 같습니다.

앞서 언급했듯이 모든 Request 객체에는 signal 속성이 있습니다. 페이지가 더 이상 응답에 관심이 없으면 fetchEvent.request.signal는 서비스 워커 내에서 취소 신호를 보냅니다. 따라서 다음과 같은 코드가 작동합니다.

addEventListener('fetch', event => {
    event.respondWith(fetch(event.request));
});

페이지에서 가져오기를 취소하면 fetchEvent.request.signal가 취소 신호를 보내므로 서비스 워커 내의 가져오기도 취소됩니다.

event.request 이외의 항목을 가져오는 경우 맞춤 가져오기에 신호를 전달해야 합니다.

addEventListener('fetch', event => {
    const url = new URL(event.request.url);

    if (event.request.method == 'GET' && url.pathname == '/about/') {
    // Modify the URL
    url.searchParams.set('from-service-worker', 'true');
    // Fetch, but pass the signal through
    event.respondWith(
        fetch(url, { signal: event.request.signal })
    );
    }
});

사양에 따라 추적하세요. 구현할 준비가 되면 브라우저 티켓에 링크를 추가하겠습니다.

역사

네, 비교적 간단한 API를 만드는 데 오랜 시간이 걸렸습니다. 이유는 다음과 같습니다.

API 불일치

보시다시피 GitHub 토론은 상당히 깁니다. 이 스레드에는 많은 미묘한 차이가 있지만 (약간의 미묘한 차이) 주요 불일치는 한 그룹은 abort 메서드가 fetch()에서 반환된 객체에 존재하기를 원했지만 다른 그룹은 응답 가져오기와 응답에 영향을 미치는 분리를 원했다는 점입니다.

이러한 요구사항은 호환되지 않으므로 한 그룹은 원하는 결과를 얻지 못할 것입니다. 그렇다면 죄송해요! 기분이 나아진다면 저도 그 그룹에 참여했습니다. 하지만 AbortSignal가 다른 API의 요구사항에 맞는지 확인하는 것이 올바른 선택으로 보입니다. 또한 연계된 프로미스가 취소 가능하게 되도록 허용하는 것은 불가능하지는 않을지라도 매우 복잡해질 것입니다.

응답을 제공하지만 취소할 수도 있는 객체를 반환하려면 간단한 래퍼를 만들면 됩니다.

function abortableFetch(request, opts) {
    const controller = new AbortController();
    const signal = controller.signal;

    return {
    abort: () => controller.abort(),
    ready: fetch(request, { ...opts, signal })
    };
}

TC39에서 잘못된 시작

취소된 작업을 오류와 구분하려고 했습니다. 여기에는 '취소됨'을 나타내는 세 번째 프로미스 상태와 동기화 및 비동기 코드에서 취소를 처리하는 새로운 구문이 포함되었습니다.

금지사항

실제 코드가 아님 — 제안이 철회됨

    try {
      // Start spinner, then:
      await someAction();
    }
    catch cancel (reason) {
      // Maybe do nothing?
    }
    catch (err) {
      // Show error message
    }
    finally {
      // Stop spinner
    }

작업이 취소되었을 때 하는 가장 일반적인 작업은 아무것도 하지 않는 것입니다. 위 제안은 취소를 오류와 분리했으므로 취소 오류를 구체적으로 처리할 필요가 없었습니다. catch cancel를 사용하면 취소된 작업에 대한 알림을 받을 수 있지만 대부분의 경우 이를 수행할 필요가 없습니다.

이는 TC39에서 1단계에 도달했지만 합의에 이르지 못하여 제안이 철회되었습니다.

대체 제안인 AbortController는 새로운 문법이 필요하지 않았으므로 TC39 내에서는 사양을 지정하는 것이 적절하지 않았습니다. 자바스크립트에서 필요한 모든 기능이 이미 존재했기 때문에 웹 플랫폼 내의 인터페이스, 특히 DOM 표준을 정의했습니다. 일단 결정을 내렸을 때 나머지는 비교적 빠르게 통합되었습니다.

대형 사양 변경

XMLHttpRequest는 몇 년 동안 중단될 수 있었지만 사양은 매우 모호했습니다. 기본 네트워크 활동을 피하거나 종료할 수 있는 시점 또는 abort()가 호출되는 후 가져오기 완료 사이에 경합 상태가 발생하면 어떤 일이 발생했는지 명확하지 않았습니다.

이번에는 제대로 하고 싶었지만, 대대적인 사양 변경으로 인해 많은 검토가 필요하고 (제 잘못이며, 앤 반 케스테렌도메닉 데니콜라에게 큰 감사를 표함) 적절한 테스트가 필요했습니다.

하지만 지금은 여기 있습니다! 비동기 작업 취소를 위한 새로운 웹 프리미티브가 있으며 여러 가져오기를 한 번에 제어할 수 있습니다. 앞으로는 가져오기 수명 기간에 우선순위 변경을 사용 설정하는 방법과 가져오기 진행 상황을 관찰하는 상위 수준의 API를 사용 설정하는 방법을 알아봅니다.