JavaScript로 작성된 Windows 스토어 앱을 위한 promise의 모든 것

JavaScript로 Windows 스토어 앱을 개발할 경우 비동기 API와 관련된 작업을 할 때 'promise'라는 구조를 자주 사용하게 됩니다. 또한 얼마 지나지 않아 순차적인 비동기 작업에도 기본적으로 promise 체인을 사용하게 될 것입니다.

하지만 개발 작업 과정에서 진행 상황을 확실하게 파악할 수 없는 경우 promise를 다른 방법으로 사용하게 됩니다. 이에 대한 좋은 예는 HTML ListView 성능 최적화 샘플에 나오는 ListView 컨트롤의 항목 렌더링 기능 최적화입니다. 전편에 이어 이번 글에서도 이 내용을 살펴보겠습니다. Josh Williams가 //build 2012의 Deep Dive into WinJS 세션에서 언급한 코드를 살펴보는 것도 좋습니다. 내용은 약간 수정되었습니다.

 list.reduce(function callback (prev, item, i) {
    var result = doOperationAsync(item);
    return WinJS.Promise.join({ prev: prev, result: result}).then(function (v) {
        console.log(i + ", item: " + item+ ", " + v.result);
    });
})

이 코드에서는 병렬 비동기 작업에 promise를 사용하여 'list'의 순서에 따라 연속적으로 결과를 제공합니다. 이 코드를 보고 그 동작을 즉시 이해할 수 있는 분들은 이 글을 읽지 않아도 됩니다! 그렇지 않은 분들은 promise의 작동 방식과 WinJS(JavaScript용 Windows 라이브러리)에서 사용되는 식을 자세히 살펴보고 패턴을 이해하시기 바랍니다.

promise란 정확하게 무엇일까요? promise 관계

가장 기본적인 내용부터 살펴보겠습니다. promise는 코드 구조 또는 호출 규칙일 뿐입니다. 그렇기 때문에 promise는 기본적으로 비동기 작업과 관계가 없고 그래서 매우 '유용'합니다! promise는 나중에 사용할 수 있게 되거나 이미 사용 가능한 값을 나타내는 개체일 뿐입니다. 이러한 맥락에서 인간 관계를 나타내는 용어인 promise를 사용하게 되었습니다. 제가 만약 “도너츠 12개를 보내 드리겠다고 약속합니다”라고 말한다면 지금 당장은 도너츠가 없지만 나중에 언젠가는 확실하게 도너츠가 생긴다는 의미입니다. 그리고 도너츠가 생겼을 때 보내면 됩니다.

이러한 맥락에서 promise는 두 에이전트 간의 관계를 의미합니다. 한 에이전트는 어떤 물건을 공급하기로 약속하는 '주관자'이고 다른 에이전트는 약속과 물건을 받는 '소비자'입니다. 주관자가 해당 물건을 취득하는 방법은 전적으로 주관자의 재량에 달렸습니다. 마찬가지로 소비자는 약속 그 자체와 받은 물건을 원하는 대로 사용할 수 있습니다. 심지어 약속을 다른 소비자와 공유할 수도 있습니다.

또한 주관자와 소비자 사이에는 '생성'과 '처리'라는 두 가지 단계가 있습니다. 이 모든 관계가 다음 다이어그램에 정리되어 있습니다.

promise_diagram

다이어그램의 흐름을 관찰해 보면 promise가 비동기 작업과 잘 맞는 이유가 두 단계로 구성된 관계 때문이라는 사실을 알 수 있습니다. 요청이 접수(promise)된 것을 확인한 소비자가 계속 기다리지(동기식) 않고 다른 일을 할 수 있다는(비동기식) 것이 가장 중요합니다. 즉, 소비자는 약속이 처리되기를 기다리면서 다른 요청에 응답하는 등의 다른 일을 할 수 있습니다. 이것이 바로 비동기 API의 가장 중요한 목적입니다. 그렇다면 물건이 이미 제공되는 경우에는 어떨까요? 약속이 즉시 처리되어 모든 것이 동기 호출 규칙이 됩니다.

물론 이 관계에도 몇 가지 고려해야 할 사항이 있습니다. 누구나 살면서 약속을 하고 또 약속을 받기도 합니다. 약속이 지켜지는 경우도 많지만 그렇지 못한 경우도 실제로 많습니다. 예를 들어 피자 배달 직원이 배달 도중에 사고를 당할 수 있습니다! 약속 불이행은 어쩔 수 없는 현실입니다. 개인 일상에서나 비동기 프로그래밍에서나 이를 받아들여야 합니다.

promise 관계 내에서 이를 살펴보자면, 주관자는 "죄송하지만 이 약속을 지킬 수 없게 되었습니다"라고 말할 수 있는 방법이 필요하고 소비자는 이를 확인할 수 있는 방법이 필요합니다. 두 번째로, 소비자는 주관자의 약속 이행에 대해 인내심이 그리 많지 않습니다. 만약 주관자가 약속 이행 진행 상황을 추적할 수 있다면 소비자가 그 정보를 받아 볼 수 있는 방법이 필요합니다. 세 번째로, 소비자가 주문을 취소하고 주관자에게 더 이상 그 물건이 필요 없다고 말하는 경우가 있습니다.

이러한 요구 사항을 다이어그램에 추가하면 완전한 관계가 완성됩니다.

promise_diagram_2

이러한 관계가 코드에서 어떻게 나타나는지 살펴보겠습니다.

promise 구조 및 promise 체인

실제로 promise에 대한 다양한 제안 또는 사양이 있습니다. Windows 및 WinJS에 사용된 것은 Common JS/Promises A이며, promise(나중에 제공할 가치를 나타내기 위해 주관자가 반환하는 것)를 then이라는 함수를 갖는 개체로 정의합니다. 소비자는 then을 호출하여 promise의 처리를 신청합니다. Windows의 promise도 promise 체인에 사용되는 done이라는 비슷한 함수를 지원하는데, 이 부분에 대해서는 잠시 후에 다루겠습니다.

이 함수에 대해 소비자는 최대 3개의 선택적 함수를 인수로 전달할 수 있으며 순서는 다음과 같습니다.

  1. 'completed handler'. 이 약속된 값이 사용 가능하게 될 때 주관자가 이 함수를 호출하고, 해당 값이 이미 사용 가능한 경우에는 then 내에서 즉시 completed handler가 호출됩니다.
  2. 약속된 값을 얻지 못할 경우 호출되는 선택적 함수 'error handler'. 어떠한 promise든 error handler가 호출되면 절대로 completed handler가 호출되지 않습니다.
  3. 작업에서 지원할 경우 중간 결과와 함께 주기적으로 호출되는 선택적 함수 'progress handler'. WinRT의 경우 API가 IAsync[Action | Operation]WithProgress의 반환 값을 갖고 IAsync[Action | Operation] 는 그렇지 않다는 의미입니다.

error handler만 연결하고 completed handler는 연결하지 않으려는 경우 이러한 인수 중 아무 인수에나 'null'을 전달할 수 있습니다.

관계의 반대편을 살펴보면, 소비자는 then을 여러 번 호출하여 같은 promise에 handler를 원하는 만큼 신청할 수 있습니다. 또한 중심 콘텐츠에 then을 호출할 수 있는 다른 소비자와 promise를 공유할 수도 있습니다. 이러한 기능이 완벽하게 지원됩니다.

즉, promise는 수신하는 모든 handler 목록을 관리하고 적절한 시기에 호출해야 합니다. 또한 전체 관계에서 살펴본 것처럼 promise는 취소를 허용해야 합니다.

'Promises A' 사양의 다른 요구 사항은 then 메서드 자체가 promise를 반환하는 것입니다. 두 번째 promise는 첫 번째 promise.then에 제공된 completed handler가 돌아올 때 처리되며 두 번째 promise의 결과로 반환 값이 제공됩니다. 다음 코드를 살펴보시기 바랍니다.

 var promise1 = someOperationAsync();
var promise2 = promise1.then(function completedHandler1 (result1) { return 7103; } );
promise2.then(function completedHandler2 (result2) { });

someOperationAsync가 시작되면 'promise1'을 반환하는 것이 이 코드의 실행 체인입니다. 이 작업이 진행 중일 때 'promise2'를 즉시 반환하는 promise1.then을 호출합니다. 비동기 작업이 가능하지 않으면 completedHandler1이 호출되지 않는다는 점을 확실히 이해하시기 바랍니다. 여전히 기다리고 있는 상황을 가정해 보겠습니다. 이 경우 바로 promise2.then 함수를 호출합니다. 이번에도 역시 completedHandler2가 호출되지 '않습니다'.

나중에 someOperationAsync가 값 14618로 완료되었다고 해 봅시다. 이제 'promise1'이 처리되고 해당 값을 가진 completedHandler1을 호출합니다. 'result1'은 14618이 됩니다. 이제 completedHandler1이 실행되고 값 7103을 반환합니다. 이 시점에서 'promise2'가 처리되고 completedHandler2가 호출되며 'result2'는 7103입니다.

completed handler가 다른 promise를 반환하는 경우는 어떨까요? 이 경우는 약간 다르게 처리됩니다. 위 코드의 completedHandler1이 다음과 같이 promise를 반환한다고 합시다.

 var promise2 = promise1.then(function completedHandler1 (result1) {
    var promise2a = anotherOperationAsync();
    return promise2a;
});

이 경우 completedHandler2의 'result2'는 'promise2a' 자체가 아니라 'promise2a'의 fulfillment 값입니다. completed handler가 promise를 반환하기 때문에 'promise2a'의 결과로 promise1.then에서 반환된 'promise2'가 처리됩니다.

바로 이러한 특징 때문에 순차적인 비동기 작업을 체인으로 엮을 수 있고, 체인의 각 작업의 결과는 다음 작업으로 전달됩니다. 중간 변수 또는 명명된 handler가 없는 경우 promise 체인에서 다음 패턴을 자주 보게 됩니다.

 operation1().then(function (result1) {
    return operation2(result1)
}).then(function (result2) {
    return operation3(result2);
}).then(function (result3) {
    return operation4(result3);
}).then(function (result4) {
    return operation5(result4)
}).then(function (result5) {
    //And so on
});

물론 각 completed handler는 수신하는 결과를 가지고 다양한 일을 하지만, 모든 체인에서 이 핵심 구조를 보게 될 것입니다. 또 하나의 사실은, 이 코드의 모든 then 메서드는 주어진 completed handler를 저장한 후 다른 promise를 반환하기 때문에 하나씩 차례대로 실행된다는 것입니다. 따라서 이 코드의 마지막 부분에 도착하면 'operation1'만 시작하고 completed handler가 호출되지 않습니다. 하지만 then을 호출할 때마다 중간 promise가 생성되고 서로 연결되어서 순차 작업이 진행될 때 체인을 관리합니다.

각 순차 작업을 이전 completed handler에 중첩하여 같은 순서로 배열할 수 있으며 이 경우 'return' 문 중 일부가 제외됩니다. 하지만 이렇게 중첩하려면 들여쓰기 때문에 고생을 많이 하게 되며, 특히 then을 호출할 때마다 진행 상황과 error handler를 추가할 경우 더욱 어렵게 됩니다.

이에 대해 부연 설명을 하자면, WinJS에서 promise의 기능 중 하나는 체인에서 발생하는 모든 오류가 자동으로 체인의 끝부분에 전달된다는 것입니다. 다시 말해, 모든 수준에서 handler를 사용하지 말고 마지막으로 호출되는 then에 error handler를 하나만 연결할 수 있어야 합니다. 하지만 다양한 이유로 인해 체인의 마지막 링크가 then 호출인 경우 이러한 오류가 가려진다는 점에 주의해야 합니다. 이 때문에 WinJS에서는 promise에 done 메서드를 제공합니다. 이 메서드는 then과 같은 인수를 받아들이지만 체인이 완전하다고 표시합니다(또 다른 promise 대신 'undefined'를 반환). 그러면 체인의 오류에 대해 done에 연결된 error handler가 호출됩니다. 뿐만 아니라 error handler가 없을 경우 done 메서드가 앱 수준에서 예외를 발생시키며, 이는 WinJS.Application.onerror 이벤트의 window.onerror로 처리할 수 있습니다. 정리하자면, 예외를 드러내서 적절하게 처리할 수 있도록 모든 체인을 done으로 끝내는 것이 가장 좋습니다.

물론 then 호출의 긴 체인에서 마지막 promise를 반환하는 것이 '목적'인 함수를 작성하는 경우 마지막 부분에 then 을 사용해야 합니다. 오류 처리의 책임은 또 다른 체인에서 해당 promise를 사용하는 호출자에게 있습니다.

promise 생성: WinJS.Promise 클래스

개발자는 'Promises A' 사양에 맞게 promise 클래스를 직접 만들 수는 있지만 너무 번거롭기 때문에 라이브러리에 맡겨 두는 것이 가장 좋습니다. 이러한 이유로 WinJS에서는 WinJS.Promise라는 철저한 검증을 마친 강력하고 유연한 promise 클래스를 제공합니다. 이 클래스를 사용하면 주관자/소비자 관계 또는 then 동작을 일일이 관리할 필요 없이 다양한 값과 작업에 대한 promise를 간편하게 생성할 수 있습니다.

필요하다면 new WinJS.Promise 또는 적절한 도우미 함수를 사용하여 비동기 작업과 기존(동기) 값 모두에 대한 promise를 생성할 수 있습니다. 이에 대해서는 다음 단원에서 설명하겠습니다. promise는 단지 코드 구조일 뿐이라는 점을 기억하시기 바랍니다. promise가 비동기 작업을 래핑해야 할 필요는 '전혀' 없습니다. 마찬가지로, promise의 어떤 코드 조각을 단지 래핑한다고 해서 자동으로 비동기식으로 실행되지는 않습니다.비동기식으로 실행하는 것은 여전히 개발자의 몫입니다.

WinJS.Promise를 직접 사용하는 간단한 예를 들어 보겠습니다. 1부터 어떤 숫자까지 추가하면서 긴 계산을 수행하려고 하는데 비동기식으로 계산해야 한다고 가정해 보겠습니다. 일상적 작업에 대한 콜백 메커니즘을 직접 개발할 수 있지만 promise 내에서 래핑할 경우 다른 API의 다른 promise와 체인으로 연결하거나 이어 붙일 수 있습니다. 이러한 줄과 함께 WinJS.xhr 함수가 promise 내에서 JavaScript의 비동기 XmlHttpRequest를 래핑하므로 개발자가 후자의 특정 이벤트 구조를 처리하지 않아도 됩니다.

물론 긴 계산에 JavaScript 작업자를 사용할 수도 있지만 설명을 위해 UI 스레드를 유지하고 setImmediate를 사용하여 작업을 여러 단계로 구분하겠습니다. 다음은 WinJS.Promise를 사용하여 promise 구조 내에서 구현하는 방법입니다.

 function calculateIntegerSum(max, step) {
    //The WinJS.Promise constructor's argument is an initializer function that receives 
    //dispatchers for completed, error, and progress cases.
    return new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) {
        var sum = 0;

        function iterate(args) {
            for (var i = args.start; i < args.end; i++) {
                sum += i;
            };

            if (i >= max) {
                //Complete--dispatch results to completed handlers
                completeDispatch(sum);
            } else {
                //Dispatch intermediate results to progress handlers
                progressDispatch(sum);
                setImmediate(iterate, { start: args.end, end: Math.min(args.end + step, max) });
            }
        }
            
        setImmediate(iterate, { start: 0, end: Math.min(step, max) });
    });
}

new WinJS.Promise를 호출할 때 생성자의 단일 인수는 'initializer' 함수입니다(이 예에서는 익명). initializer가 수행할 작업을 캡슐화하지만 이 함수 자체가 UI 스레드에서 '동기식으로' 실행될 것이 분명합니다. 따라서 여기서 setImmediate를 사용하지 않고 긴 계산을 수행할 경우 그 시점의 모든 UI 스레드를 차단하게 됩니다. 다시 한 번 말씀 드리지만 promise 내에 코드를 배치한다고 해서 자동으로 비동기식으로 실행되지는 않습니다. initializer 함수 자체에서 그렇게 설정해야 합니다.

인수의 경우 initializer 함수는 promise가 지원하는 completed, error 및 progress 사례의 '디스패처' 세 개를 수신합니다. 보시다시피 작업하는 동안 적절한 인수를 사용하여 적절한 시기에 디스패처를 호출하고 있습니다.

저는 이러한 함수를 "디스패처"라고 부릅니다. 소비자가 promise의 then 메서드(또는 done, 설명은 생략)를 요청하는 handler와 다르기 때문입니다. 좀 더 자세히 알아보자면, WinJS는 handler 배치를 관리하며, 몇 명이 되었든 몇 개가 되었든 소비자가 handler를 요청할 수 있습니다. 이러한 디스패처 중 하나를 호출하면 WinJS가 내부 목록을 통해 반복하면서 개발자를 대신하여 모든 handler를 호출합니다. 또한 WinJS.Promisethen이 체인 연결에 필요한 만큼 또 다른 promise를 반복하도록 합니다.

정리하자면, WinJS.Promise는 promise를 둘러싼 모든 것을 제공합니다. 따라서 개발자는 promise가 나타내는 핵심 작업에만 집중할 수 있으며, 이러한 것들은 initializer 함수에 포함되어 있습니다.

promise 생성을 돕는 도우미

promise를 생성하는 기본 도우미 함수는 정적 메서드인 WinJS.Promise.as입니다. 이 메서드는 promise의 '모든' 값을 래핑합니다. 기존 값의 래퍼는 정상적으로 작동하고 then으로 전달되는 모든 completed handler를 호출합니다. 따라서 알고 있는 임의의 값을 promise로 처리할 수 있기 때문에 연결 또는 체인 연결을 통해 다른 promise와 혼합하여 구성할 수 있습니다. 기존 promise에 as를 사용하면 해당 promise만 반환합니다.

다른 정적 도우미 함수는 WinJS.Promise.timeout입니다. 이 함수는 setTimeoutsetImmediate와 관련된 편리한 래퍼를 제공합니다. 두 번째 promise가 특정 시간(밀리초) 내에 처리되지 않을 경우 두 번째 promise를 취소하는 promise를 만들 수도 있습니다.

setTimeoutsetImmediate와 관련된 timeout promise가 'undefined'를 사용하여 스스로 처리되는 것을 주의 깊게 살펴보시기 바랍니다. 이와 관련하여 “이 promise를 사용하여 시간 제한 이후에 다른 결과를 제공하려면 어떻게 해야 합니까?”라는 질문을 자주 받습니다. 답변을 드리자면, then이 completed handler의 반환 값을 통해 처리되는 또 다른 promise를 반환합니다. 이 코드 줄에서 예를 들자면 다음과 같습니다.

 var p = WinJS.Promise.timeout(1000).then(function () { return 12345; });

이 코드는 1초 후 값 12345로 처리되는 promise 'p'를 생성합니다. 다시 말해, WinJS.Promise.timeout(…).then(function () { return <value>} ) 는 지정된 시간 제한 이후 '<value>'를 제공하는 패턴입니다. '<value>' 자체가 또 다른 promise인 경우 시간 제한 이후 특정 시점에 해당 promise의 처리 값을 제공합니다.

취소 및 promise 오류 생성

다들 보셨겠지만 조금 전에 본 코드는 두 가지 결함이 있습니다. 첫째, 시작 후 작업을 취소하는 방법이 없고 둘째, 작업 처리 오류를 표시하는 방법이 없습니다.

두 가지 문제를 해결하는 비결을 소개해 드리자면, calculateIntegerSum 함수처럼 promise를 생산하는 함수는 항상 promise를 반환해야 합니다. 어떤 작업이 완료되지 않거나 아예 시작되지 않을 경우 해당 promise는 소위 말하는 '오류 상태'에 있는 것입니다. 즉, 해당 promise는 completed handler에 전달할 수 있는 값을 갖고 있지 않으며 앞으로도 가질 수 없습니다. 단지 error handler만 호출할 수 있습니다. 사실, 소비자가 한 promise에서 then을 호출했는데 그 promise가 이미 오류 상태인 경우 해당 promise는 즉시(동기식으로) then에 전달되는 error handler를 호출합니다.

WinJS.Promise가 오류 상태로 진입하는 두 가지 이유가 있습니다. 첫 번째는 소비자가 cancel 메서드를 호출하는 경우이고 두 번째는 initializer 함수 내의 코드가 error 디스패처를 호출하는 경우입니다. 이러한 상황이 발생하면 해당 promise에 전달된 오류 값이 무엇이든 error handler에 전달됩니다. WinJS.Promise 내에 작업을 생성할 경우 WinJS.ErrorFromName 인스턴스를 사용할 수도 있습니다. 이 인스턴스는 JavaScript 개체이며, 오류를 식별하는 'name' 속성과 자세한 정보가 들어 있는 'message' 속성을 갖고 있습니다. 예를 들어 한 promise가 취소되면 error handler는 'name' 및 'message' 이름이 “Canceled”로 설정된 오류 개체를 받습니다.

작업을 아예 시작할 수 없는 경우는 어떨까요? 예를 들어 0, 0처럼 잘못된 인수로 calculateIntegerSum을 호출할 경우 카운트를 시도조차 하지 않고 그 대신 오류 상태의 promise를 반환합니다. 이것이 바로 정적 메서드 WinJS.Promise.wrapError의 목적입니다. WinJS.ErrorFromName의 인스턴스를 가져와서 오류 상태의 promise를 반환합니다. 우리가 예로 든 코드에서도 새로운 WinJS.Promise 인스턴스를 사용하는 대신 오류 상태의 promise를 반환합니다.

또 다른 문제는 promise의 cancel 메서드를 호출하면 promise 자체가 오류 상태에 빠지는 것입니다. 진행 중인 비동기 작업을 중지하려면 어떻게 해야 할까요? 앞에서 구현한 calculateIntegerSum은 생성된 promise의 상태에 관계없이 작업이 완료될 때까지 계속해서 setImmediate를 호출할 것입니다. 사실, promise가 취소된 후 작업에서 complete 디스패처를 호출할 경우 promise가 해당 완료를 무시합니다.

이 문제를 해결하려면 promise가 해당 작업에 더 이상 계속할 필요가 없다고 알릴 수 있는 방법이 필요합니다. 이러한 이유로 WinJS.Promise 생성자는 promise가 취소될 때 호출되는 두 번째 함수 인수를 사용합니다. 이 글의 예에서는 이 함수를 호출할 때 setImmediate가 이어서 호출되지 않도록 하면 계산이 중지됩니다. 다음은 오류를 적절하게 처리한 코드입니다.

 function calculateIntegerSum(max, step) {
    //Return a promise in the error state for bad arguments
    if (max < 1 || step < 1) {
        var err = new WinJS.ErrorFromName("calculateIntegerSum", "max and step must be 1 or greater");
        return WinJS.Promise.wrapError(err);
    }

    var _cancel = false;

    //The WinJS.Promise constructor's argument is an initializer function that receives 
    //dispatchers for completed, error, and progress cases.
    return new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) {
        var sum = 0;

        function iterate(args) {
            for (var i = args.start; i < args.end; i++) {
                sum += i;
            };

            //If for some reason there was an error, create the error with WinJS.ErrorFromName
            //and pass to errorDispatch
            if (false /* replace with any necessary error check -- we don’t have any here */) {
                errorDispatch(new WinJS.ErrorFromName("calculateIntegerSum (scenario 7)", "error occurred"));
            }

            if (i >= max) {
                //Complete--dispatch results to completed handlers
                completeDispatch(sum); 
            } else {
                //Dispatch intermediate results to progress handlers
                progressDispatch(sum);

                //Interrupt the operation if canceled
                if (!_cancel) {
                    setImmediate(iterate, { start: args.end, end: Math.min(args.end + step, max) });
                }
            }
        }
            
        setImmediate(iterate, { start: 0, end: Math.min(step, max) });
    },
    //Cancellation function for the WinJS.Promise constructor
    function () {
        _cancel = true;
    });
}

대체적으로 WinJS.Promise 인스턴스를 생성하여 다양한 용도에 이용할 수 있습니다. 예를 들어 다른 비동기 메서드를 통해 웹 서비스에 정보를 전달하는 라이브러리를 갖고 있다면 promise 내에서 작업을 래핑할 수 있습니다. 새 promise를 사용하여 다양한 소스의 여러 비동기 작업(또는 다른 promise)을 모든 관계를 제어하고 싶은 하나의 promise로 결합할 수도 있습니다. WinJS.Promise의 initializer 코드 내에서 다른 비동기 작업과 그 promise에 대한 개발자 고유의 처리기를 사용할 수 있습니다. 이러한 처리기를 사용하여 네트워크 시간 제한에 대한 자동 재시도 메커니즘을 캡슐화하고, 일반적인 진행 상황 업데이트 UI를 연결하고, 자세한 로깅 또는 분석을 추가할 수 있습니다. 어떤 방법을 사용하든 나머지 코드에서 사소한 부분은 신경 쓰지 않고 소비자 측에서 promise를 처리할 수 있습니다.

다음 코드 줄을 사용하면 JavaScript 작업자를 매우 간단하게 promise로 래핑할 수 있습니다. WinRT의 다른 비동기 작업과 모양 및 동작이 비슷합니다. 잘 아시겠지만 작업자는 앱의 작업자 개체에 message 이벤트를 발생시키는 postMessage 호출을 통해 결과를 전달합니다. 다음 코드는 message에 전달되는 결과를 통해 처리되는 promise에 해당 이벤트를 연결합니다.

 // This is the function variable we're wiring up.
var workerCompleteDispatch = null;

var promiseJS = new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) {
    workerCompleteDispatch = completeDispatch;
});

// Worker is created here and stored in the 'worker' variable

// Listen for worker events
worker.onmessage = function (e) {
    if (workerCompleteDispatch != null) {
        workerCompleteDispatch(e.data.results); /* event args depends on the worker */
    }
}

promiseJS.done(function (result) {
    // Output for JS worker
});

이 코드를 확장하여 작업자의 오류를 처리하려면 error 디스패처를 다른 변수에 저장하고 message 이벤트 처리기가 이벤트 인수의 오류 정보를 확인하도록 하고, complete 디스패처 대신 error 디스패처를 적절하게 호출해야 합니다.

병렬 promise 연결

promise는 비동기 작업을 래핑하는 데 자주 사용되기 때문에 개발자는 여러 작업을 병렬로 진행할 수 있습니다. 그렇다면 그룹의 한 promise가 처리되는 시기 또는 그룹의 모든 promise가 처리되는 시기가 언제인지 궁금할 것입니다. 이러한 용도로 사용되는 것이 정적 함수 WinJS.Promise.anyWinJS.Promise.join입니다.

두 함수는 여러 값을 수락하거나 값 속성이 있는 개체를 수락합니다. 두 값은 promise일 수 있으며 promise가 아닌 값은 WinJS.Promise.as를 통해 래핑되고 결국 전체 배열 또는 개체가 promise로 구성됩니다.

다음은 any의 특징입니다.

  • any는 다른 promise 중 하나가 처리되거나 오류(논리적 OR)와 함께 실패할 때 처리되는 단일 promise를 생성합니다. 기본적으로 any는 모든 promise에 completed handler를 연결하며, 한 completed handler가 호출되면 anypromise가 수신한 모든 completed handler를 호출합니다.
  • any promise가 처리된 후, 즉 목록의 첫 번째 promise가 처리된 후 목록의 다른 작업이 계속 실행되어 해당 promise에 개별적으로 할당된 모든 completed, error 또는 progress handler를 호출합니다.
  • any에서 promise를 취소할 경우 목록의 모든 promise가 취소됩니다.

join의 경우

  • join은 다른 '모든' promise가 처리되거나 오류(논리적 AND)와 함께 실패할 때 처리되는 단일 promise를 생성합니다. 기본적으로 join은 모든 promise에 completed 및 error handler를 연결하며, 모든 handler가 호출될 때까지 기다렸다가 수신하는 모든 completed handler를 호출합니다.
  • 또한 join promise는 개발자가 제공하는 모든 progress handler에 진행 상황을 보고합니다. 이 예의 중간 결과는 지금까지 처리된 개별 promise에서 얻은 결과입니다.
  • join에서 promise를 취소할 경우 아직 보류 중인 모든 promise가 취소됩니다.

anyjoin 외에도 알아두면 편리하게 사용할 수 있는 정적 WinJS.Promise 메서드가 두 개 더 있습니다.

  • is는 임의 값이 promise인지 확인하여 부울을 반환합니다. “then” 함수를 사용하는 개체인지 확인하는 것이 기본 역할이며, “done”은 테스트하지 않습니다.
  • theneachthen을 사용하여 promise 그룹에 completed, error 및 progress handler를 적용하고, 그 결과를 promise 내의 또 다른 값 그룹으로 반환합니다. 모든 handler는 'null'일 수 있습니다.

연속 결과를 갖는 병렬 promise

WinJS.Promise.joinWinJS.Promise.any를 사용하여 병렬 promise, 즉 병렬 비동기 작업을 처리할 수 있습니다. 다시 한 번 말씀 드리지만 join이 반환하는 promise는 배열의 모든 promise가 처리될 때 처리됩니다. 하지만 promise는 무작위 순서로 완료됩니다. 이 방법으로 실행할 수 있는 일련의 작업이 있지만 그 결과를 잘 정의된 순서로, 다시 말해서 promise가 배열에 나타나는 순서대로 처리하려면 어떻게 해야 할까요?

이렇게 하려면 각 후속 promise를 앞에 있는 모든 promise의 join에 연결해야 합니다. 이 글을 시작할 때 소개한 코드가 정확하게 이 방법을 사용합니다. 그 코드를 다시 한 번 보여 드리겠습니다. 하지만 이번에는 promise가 잘 보이도록 약간 수정했습니다. 'list'는 가상의 promise를 생산하는 비동기 호출 doOperationAsync의 인수로 사용되는 값의 배열이라고 가정합니다.

 list.reduce(function callback (prev, item, i) {
    var opPromise = doOperationAsync(item);
    var join = WinJS.Promise.join({ prev: prev, result: opPromise});

    return join.then(function completed (v) {
        console.log(i + ", item: " + item+ ", " + v.result);
    });
})

이 코드를 이해하려면 배열의 reduce 메서드가 작동하는 방식을 먼저 이해해야 합니다. 배열의 각 항목에서 reduce는 함수 인수를 호출합니다. 여기서는 callback인데 인수 네 개를 수신합니다. 이 코드에서는 네 개 중 세 개만 사용하고 있습니다.

  • 'prev' 'previous' 호출에서 callback으로 반환된 값입니다(첫 번째 항목에서는 'null').
  • 'item' 배열의 현재 값입니다.
  • 'i' 목록의 항목 인덱스입니다.
  • 'source' 원래 배열입니다.

목록의 첫 번째 항목에는 **opPromise1**이라고 하는 promise를 사용합니다. prev가 'null'이기 때문에 [WinJS.Promise.as(null), opPromise1] 을 연결합니다. 하지만 join 자체를 반환하는 것은 아니라는 점에 주의하시기 바랍니다. 그 대신 completed라고 이름을 붙인 completed handler를 join에 연결하고 then으로부터 promise를 반환할 것입니다.

then에서 반환되는 promise는 completed handler가 반환될 때 처리된다는 것을 기억하시기 바랍니다. 즉, callback에서 반환하는 것은 첫 번째 항목의 completed handler가 **opPromise1**의 결과를 처리할 때까지 완료되지 않은 promise입니다. join의 결과를 되돌아보면 원래 목록의 promise에서 얻은 결과가 들어 있는 개체를 통해 처리됩니다. 즉, 처리 값 'v'가 'prev' 속성과 'result' 속성을 모두 포함하며, 후자는 **opPromise1**의 결과입니다.

'list'의 다음 항목을 사용하여 callback이 이전 join.then의 promise가 들어 있는 'prev'를 수신합니다. 그런 다음 opPromise1.then 및 **opPromise2**의 새로운 join을 생성하게 됩니다. 결과적으로 이 join은 **opPromise2**가 처리되고 **opPromise1**의 completed handler가 반환될 때까지 완료되지 않습니다. 놀랍지 않습니까? 이 join에 연결하는 completed2 handler는 **completed1**이 반환될 때까지 호출되지 않습니다.

목록의 각 항목마다 동일한 종속성이 계속 구축됩니다. 항목 'n'에 대한 join.then의 promise는 **completedn**이 반환될 때까지 처리되지 않습니다. 따라서 completed handler가 'list'와 같은 순서대로 호출됩니다.

결론

이 글에서 promise 그 자체로는 미래의 어느 시점에 제공할 값을 갖고 있는 주관자와 그 값을 언제 받을 수 있는지 알고 싶어하는 소비자 사이의 관계를 나타내는 코드 구조 또는 호출 규칙에 불과하다는 것을 살펴보았습니다. 그렇기 때문에 promise는 비동기 작업의 결과를 매우 잘 나타내며 JavaScript로 작성된 Windows 스토어 앱 내에서 광범위하게 사용됩니다. 또한 promise의 사양에서는 순차적인 비동기 작업을 체인으로 연결하는 것을 허용하며, 각 중간 결과는 한 링크에서 다음 링크로 전달됩니다.

WinJS(JavaScript용 Windows 라이브러리)는 모든 작업을 개발자 혼자 래핑할 수 있도록 견고하게 구축된 promise를 제공합니다. 또한 promise를 병렬 작업에 연결하는 등의 일반 시나리오를 위한 도우미도 제공합니다. 따라서 WinJS를 사용하면 비동기 작업을 매우 효율적이고 효과적으로 처리할 수 있습니다.

Windows 에코시스템 팀 프로그램 관리자

Kraig Brockschmidt

작성자, 'HTML, CSS 및 JavaScript로 Windows 8 앱 프로그래밍하기'