−1. 결과를 표현하는 Promise
JavaScript의 ECMAScript 2015 표준은 Promise를 언어의 구성 요소로 정리했다. Promise는 아직 정해지지 않은 결과와, 그 결과가 성공 또는 실패로 정해졌을 때 이어서 실행할 동작을 표현한다. pending에서 fulfilled나 rejected로 이동하는 결과의 모델이다.[각주 1]
Promise 객체가 결과를 나타낸다는 것과 실제 작업의 중단 권한을 가진다는 것은 다른 문제다. 하나의 결과를 여러 코드가 기다릴 수도 있고, 그 결과를 만드는 동작이 통신인지 계산인지도 Promise만으로는 알 수 없다. await하던 코드를 그만 기다리게 했다고 해서 결과의 생산자가 자동으로 멈추는 관계는 성립하지 않는다.
이 구분은 타임아웃에서도 드러난다. 타이머와 요청 중 먼저 완료한 Promise를 선택하면 대기 시간을 제한할 수 있지만, 뒤에 남은 요청의 통신이나 서버 작업은 별도로 수명을 관리해야 한다.
2. 웹 플랫폼에서 AbortController로
Jake Archibald의 2017년 「Abortable fetch」는 2015년에 시작된 fetch 취소 논의와 브라우저 도입 과정을 설명한다. 문서의 정정에는 Edge 16의 지원이 언급된다. 이 기록은 웹의 fetch에 취소를 연결한 과정이며, 비동기 취소 자체가 그때 처음 생겼다는 연혁은 아니다.[각주 2]
DOM 표준의 AbortController와 AbortSignal은 요청하는 쪽과 관찰하는 쪽을 분리한다. controller를 소유한 코드는 중단을 요청하고, 작업에 전달한 signal은 중단 여부와 이유를 나타낸다. 이 구조를 사용하면 하나의 중단 요청을 여러 협력 작업에 전달할 수도 있다.[각주 3]
fetch는 이 신호를 받는 API 중 하나다. 같은 signal을 받는 다른 API도 있을 수 있으나, 동작의 취소 방식과 정리는 각각의 API가 정의한다. 신호를 전달할 수 있다는 사실과 해당 작업이 실제로 그 신호에 반응한다는 사실은 구분된다.
3. 협력적 취소의 실행 과정
협력적 취소는 보통 요청, 관찰, 정리, 완료의 순서로 이해할 수 있다. 호출자가 중단을 요청하면 작업은 적절한 시점에 신호를 확인하고, 더 진행할 일을 멈춘 뒤 자신이 가진 자원을 정리한다. 오래 걸리는 동기 계산이라면 신호를 확인할 지점과 다른 코드가 실행될 기회도 마련해야 한다.
취소 요청과 정상 완료가 비슷한 시점에 일어나면 작업의 종료 규칙이 중요해진다. 이미 완료한 결과를 취소 신호가 뒤늦게 되돌리는지, 완료 이후 신호를 무시하는지에 따라 계약이 달라진다. 이벤트 리스너와 타이머도 작업 수명에 맞춰 정리해야 다음 실행에 남지 않는다.
4. 가장 작은 신호 예제
다음 코드는 작업을 아직 시작하지 않은 상태에서 중단 신호가 이미 전달돼 있을 때를 보여준다. 통신이나 타이머를 추가하지 않고, controller와 signal의 역할부터 확인하려는 예제다.
+실행을 끊는 일에서 중단을 요청하는 일로 (2009년의 회고)
Mike Liddell은 2009년 .NET 4 취소 프레임워크를 소개하면서 당시의 두 접근을 비교했다. 실행 중인 코드를 강제로 끊으면 공유 데이터나 자원이 정리되지 않은 지점에서 멈출 수 있다. 협력적 취소는 작업이 중단 요청을 관찰하고 자신이 맡은 정리를 수행하게 한다.[각주 1]
그 글이 지적한 기존 API의 어려움은 중단 방법이 객체마다 달랐다는 점이다. 작업 객체의 취소 메서드나 플래그에 의존하면 하나의 요청을 여러 작업에 전달하거나, 여러 작업 가운데 특정 실행만 구별하기가 어려워진다. 작업을 수행하는 객체와 실행 한 번의 수명을 분리할 필요가 생긴다.
이 기록은 .NET 설계자가 2009년에 설명한 문제와 설계 선택을 보여준다. 협력적 취소 전체의 발명 시점을 뜻하지는 않는다.
요청자와 관찰자를 별도 값으로 나누다 (2009)
.NET의 설계는 중단을 요청하는 CancellationTokenSource와 작업에 전달하는 CancellationToken을 나눈다. 호출자는 source로 중단 의사를 전달하고, 작업은 받은 token을 확인하거나 콜백을 등록한다. 여러 작업이 같은 요청을 관찰할 수도 있다.[각주 1]
이 분리는 취소 권한을 가진 쪽과 그 요청에 반응하는 쪽을 드러낸다. 현재 .NET 문서도 요청, 관찰, 작업별 정리를 협력적 취소의 절차로 설명한다. 요청을 보낸 시각과 작업이 실제로 끝난 시각 사이에는 간격이 있다.[각주 2]
결과의 모델로 표준화된 Promise (2015)
ECMAScript 2015는 Promise를 JavaScript 표준에 포함했다. Promise는 아직 정해지지 않은 결과와 이후의 성공·실패 반응을 표현하며, pending에서 fulfilled 또는 rejected로 정해진다.[각주 3]
하나의 결과를 여러 코드가 기다릴 수 있고 결과의 생산자가 통신을 하는지 계산을 하는지도 Promise만으로는 정해지지 않는다. 결과를 기다리는 인터페이스와 생산자의 실행 수명을 제어하는 인터페이스가 별도로 필요한 이유다.
타이머와 요청 중 먼저 정해진 Promise를 선택하면 기다림의 기한을 둘 수 있다. 남은 요청의 자원 사용이나 원격 서버의 처리를 끝내려면 그 작업이 지원하는 중단 수단도 연결해야 한다.
fetch에 취소를 연결한 웹 플랫폼의 논의 (2015~2017)
Jake Archibald의 「Abortable fetch」는 2015년에 시작된 논의가 2017년 브라우저 지원으로 이어진 과정을 기록한다. 글에 붙은 정정은 Edge 16이 먼저 지원했다는 점을 명시한다. 이 연혁의 대상은 웹 fetch의 취소 인터페이스다.[각주 4]
DOM의 AbortController와 AbortSignal도 요청자와 관찰자를 분리한다. controller가 중단을 요청하면 signal에 중단 상태와 이유가 남는다. API는 그 signal을 받아 자신의 실행을 어떻게 중단할지 정한다. 앞서 본 .NET과 비교할 수 있는 역할 분리이며, 두 인터페이스의 직접적인 계승 관계를 전제할 필요는 없다.[각주 5]
신호가 여러 API의 수명 계약으로 쓰이다
현재 DOM 표준은 진행 중인 활동의 중단을 위한 공통 신호를 정의하고, Fetch 표준은 이를 요청과 응답 본문에 연결한다. 응답 객체를 받은 뒤에도 본문 읽기가 진행될 수 있으므로, 응답 도착과 본문 처리 완료는 서로 다른 시점이다.[각주 5][각주 6]
협력적 취소의 흐름은 요청, 관찰, 정리, 완료로 이어진다. 긴 동기 계산은 신호를 관찰할 지점과 다른 코드가 실행될 기회가 필요하다. 신호 객체를 전달하는 것만으로 계산에 중단 지점이 생기지는 않는다.
외부 쓰기는 또 다른 수명을 가진다. 클라이언트가 통신을 중단했을 때 서버가 이미 커밋했다면, 그 변경의 처리 결과는 서버 조회나 별도의 취소 명령 같은 애플리케이션 계약으로 확인한다.
화면의 경쟁 상태에서 결과 적용 권한으로
검색 A 다음에 B를 시작해도 A가 늦게 끝날 수 있다. 완료 순서대로 화면에 적용하면 오래된 A가 최종 화면에 남는다. React 공식 문서의 Effect 정리 예제는 오래된 응답을 무시하는 방식으로 이 경쟁을 설명한다.[각주 7]
이 문제에서는 작업을 줄이는 중단 요청과 현재 화면을 바꿀 권한을 함께 관리한다. 요청 번호나 세대로 최신 요청을 구별하면 성공 결과뿐 아니라 오류 표시와 로딩 해제에도 같은 규칙을 적용할 수 있다. 부모 기능이 끝날 때 어느 하위 작업까지 끝내는지도 수명 계약에 포함된다.
중단 신호의 상태를 관찰하는 작은 예제
다음 코드는 controller와 signal의 역할을 먼저 확인한다. 통신이나 비동기 계산은 실행하지 않고, 중단 요청 전후의 상태만 읽는다.