취소 가능한 가져오기

Jake Archibald
Jake Archibald

'가져오기 중단'에 관한 원래 GitHub 문제는 2015년에 열렸습니다. 이제 2017년 (현재 연도)에서 2015년을 빼면 2가 됩니다. 이는 수학의 버그를 보여줍니다. 2015년은 실제로 '오래 전'이니까요.

2015년에 진행 중인 가져오기 중단을 처음으로 살펴보기 시작했으며, GitHub 주석 780개, 몇 번의 잘못된 시작, 5개의 풀 리퀘스트 후에 마침내 브라우저에 중단 가능한 가져오기가 도착했습니다. 첫 번째 브라우저는 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 토론은 상당히 길습니다. 이 대화목록에는 많은 뉘앙스가 있지만, 주요 불일치는 한 그룹은 fetch()에서 반환된 객체에 abort 메서드가 있어야 한다고 주장하는 반면 다른 그룹은 응답을 가져오는 것과 응답에 영향을 미치는 것을 분리해야 한다고 주장하는 것입니다.

이러한 요구사항은 호환되지 않으므로 한 그룹은 원하는 결과를 얻지 못했습니다. 이 경우 죄송합니다. 위안이 되시길 바라며 저도 그 그룹에 있었습니다. 하지만 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 내에서 이를 지정하는 것은 적절하지 않습니다. JavaScript에서 필요한 모든 것이 이미 있으므로 웹 플랫폼 내에서 인터페이스, 특히 DOM 표준을 정의했습니다. 결정을 내린 후 나머지는 비교적 빠르게 진행되었습니다.

사양 대폭 변경

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

이번에는 제대로 처리하고자 했지만, 그 결과 많은 검토가 필요하고 (제 과실이며, 이 과정을 도와준 앤 반 케스터렌님과 도메닉 데니콜라님께 감사드립니다) 적절한 테스트 세트가 필요한 대규모 사양 변경이 발생했습니다.

이제 다 왔습니다. 비동기 작업을 중단하기 위한 새로운 웹 원시 유형이 있으며 여러 가져오기를 한 번에 제어할 수 있습니다. 나중에 가져오기 전체 기간 동안 우선순위 변경을 사용 설정하고 가져오기 진행 상황을 관찰하는 상위 수준 API를 살펴봅니다.