ECMAScript 6 Promise와 비동기 프로그램밍

ECMASCript 6 Promise와 비동기 프로그램밍

JavaScript의 가장 강력한 부분중 하나는 비동기 프로그래밍을 쉽게 처리할 수 있다는 것입니다. 웹용으로 작성된 언어로서 JavaScript는 처음부터 클릭및 키누름과 같은 비동기 사용자 상호 작용에 응답할 수 있어야 했습니다. Node.js는 Event 대신에 Callback을 사용하여 JavaScript에서 비동기 프로그래밍을 대중화했습니다. 비동기 프로그래밍을 사용하는 프로그램이 점점 더 많아짐에 따라 EventCallback은 더 이상 개발자가 원하는 모든 것을 지원할만큼 강력하지 않았습니다. Promise는 이러한 문제에 대한 해결책입니다.

Promise는 비동기 프로그래밍의 또 다른 옵션이며 다른 언어의 FutureDeferred와 같이 작동합니다. Promise는 나중에 (EventCallback과 같이) 실행될 코드를 지정하고 작업에서 코드의 성공 또는 실패 여부를 명시적으로 나타냅니다. 코드를 이해하고 디버그하기 쉽게 만드는 방법으로 성공 또는 실패를 기반으로 Promise를 연결할 수 있습니다.

Promise가 어떻게 작동하는지 잘 이해하려면 처음 생성되었을 때의 기본 개념을 이해하는 것이 매우 중요합니다.

비동기 프로그래밍 배경

JavaScript 엔진은 단일 스레드 Event Loop 개념을 기반으로 합니다. 단일 스레드는 한번에 하나의 코드만 실행한다는 것을 의미합니다. 이것을 스레드가 여러 다른 코드를 동시에 실행할 수 있는 Java 또는 C++과 같은 언어와 비교됩니다. 여러 코드가 특정 State를 액세스하여 변경할 수 있을때 State를 유지 및 보호하는 것은 어려운 문제이며 스레드 기반 소프트웨어의 빈번한 버그의 원인이 됩니다.

JavaScript 엔진은 한번에 하나의 코드만 실행할 수 있으므로 실행할 코드를 추적할 수 있어야 합니다. 이 코드는 Job Queue(작업 대기열)에 보관됩니다. 코드 조각은 실행할 준비가되면 Job Queue에 추가됩니다. JavaScript 엔진이 코드 실행을 완료하면 Event LoopQueue의 다음 작업을 실행합니다. Event Loop는 코드 실행을 모니터링하고 Job Queue를 관리하는 JavaScript 엔진 내부의 프로세스입니다. Queue의 작업 실행은 Queue의 첫 번째 작업에서 마지막 작업까지 실행됩니다

Event 모델

사용자가 버튼을 클릭하거나 키보드의 키를 누르면 onclick과 같은 Event가 발생됩니다. 이 EventJob Queue 뒤쪽에 새 작업을 추가하여 상호 작용에 응답할 수 있습니다. 이것이 JavaScript의 가장 기본적인 비동기 프로그래밍 형식입니다. Event 핸들러 코드는 Event가 발생할 때까지 실행되지 않으며 작업이 실행될 때 적절한 컨텍스트를 갖습니다.

1
2
3
4
let button = document.getElementById("my-btn");
button.onclick = function(event) {
console.log("Clicked");
};

이 코드에서,console.log("Clicked")button이 클릭될 때까지 실행되지 않습니다. button을 클릭하면 onclick에 할당된 기능이 Job Queue의 뒤쪽에 추가되고 이전 모든 작업이 완료되면 실행됩니다.

Event는 간단한 상호 작용에는 잘 작동하지만 여러 개의 개별 비동기 호출을 함께 연결해야 할때, 각 Event에 대한 Event 대상 (앞의 예제에서 button)을 추적해야하기 때문에 더 복잡합니다. 또한 Event가 처음 발생하기 전에 모든 적절한 Event 핸들러를 추가 해야합니다. 예를 들어 onclick이 지정되기 전에 button이 클릭되면 아무 일도 일어나지 않을 것입니다. Event가 사용자 상호 작용 및 이와 유사한 기능에 대한 응답에 유용하기는 하지만 보다 복잡한 요구에 대해서는 매우 유연하지 않습니다.

Callback 패턴

Node.js가 만들어지고 Callback 패턴 프로프래밍이 대중화하여 비동기 프로그래밍 모델을 발전 시킬수 있었습니다. 비동기 코드는 나중 시점까지 실행되지 않기 때문에 Callback 패턴은 Event 모델과 유사합니다. Callback 패턴은 다음과 같이 호출할 함수가 파라미터로 전달되기 때문에 Event 모델과 다릅니다.

1
2
3
4
5
6
7
8
readFile("example.txt", function(err, contents) {
if (err) {
throw err;
}
console.log(contents);
});
console.log("Hi!");

이 예제는 기존 Node.js Error-first Callback 스타일을 사용합니다. readFile() 함수는 디스크의 파일 (첫 번째 파라미터로 지정된 파일)을 읽은 다음 완료될 때 Callback(두 번째 파라미터)을 실행하기 위한것입니다. 오류가 있으면 콜백의 err 파라미터는 오류 객체입니다. 그렇지 않은 경우, contents 파라미터는 파일 내용을 문자열로 포함합니다.

Callback 패턴을 사용하여 readFile()은 즉시 실행을 시작하고 디스크에서 읽기를 시작할 때 일시 중지합니다. 즉, console.log(contents)가 어떤 것도 출력하기 전 readFile()이 호출된 직후에 console.log( "Hi!")가 출력됩니다. readFile()이 끝나면 Callback 함수와 파라미터는 Job Queue의 끝에 추가됩니다. 그리고 그 앞에있는 다른 모든 작업이 완료되면 실행됩니다.

Callback 패턴은 Event보다 융통성이 있습니다. Callback을 통해 여러개의 호출을 함께 연결하는 것이 쉬워지기 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
readFile("example.txt", function(err, contents) {
if (err) {
throw err;
}
writeFile("example.txt", function(err) {
if (err) {
throw err;
}
console.log("File was written!");
});
});

이 코드에서 readFile()을 성공적으로 호출하면 또 다른 비동기 호출이 발생하며 이번에는 writeFile() 함수를 호출합니다. err을 검사하는 것과 같은 기본 패턴이 두 함수 모두에 존재한다는 것에 유의하십시오. readFile()이 완료되면 호출되는 Job QueuewriteFile()을 호출하는 작업을 추가합니다 (오류가 없다고 가정). 그런 다음 writeFile()이 끝날때 Job Queue에 작업을 추가합니다.

이 패턴은 꽤 잘 작동하지만 여러분은 바로 Callback hell에 빠졌다는 것을 알게됩니다. Callback hell은 다음과 같이 너무 많은 Callback을 중첩할 때 발생합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
method1(function(err, result) {
if (err) {
throw err;
}
method2(function(err, result) {
if (err) {
throw err;
}
method3(function(err, result) {
if (err) {
throw err;
}
method4(function(err, result) {
if (err) {
throw err;
}
method5(result);
});
});
});
});

이 예제는 여러개의 메서드 호출을 중첩하여 복잡하고, 이해 및 디버그가 어려운 웹을 만듭니다. Callback은 복잡한 기능을 구현하려는 경우에도 문제가 발생합니다. 두 개의 비동기 작업을 병렬로 실행하고 두 비동기 작업이 모두 완료될 때 알려주려면 어떻게 해야할까요? 한번에 두개의 비동기 작업을 시작하고 첫 번째 작업의 결과만 가져 오려면 어떻게 해야할까요?

이러한 경우 여러 CallbackCleanup 작업을 추적해야 합니다. Promise는 이러한 상황을 크게 향상시킬 수 있습니다.

Promise 기본

Promise는 비동기 연산의 결과를 위한 Placeholder입니다. Event를 구독하거나 함수에 Callback을 전달하는 대신 함수는 다음과 같이 Promise을 리턴할 수 있습니다.

1
2
// readFile은 미래의 어떤 시점에서 완료할 것을 약속합니다.
let promise = readFile("example.txt");

이 코드에서 readFile()은 실제로 파일 읽기를 즉시 시작하지 않습니다. 그것은 나중에 일어날 일입니다. 대신이 함수는 비동기 읽기 작업을 나타내는 Promise 객체를 리턴하므로 향후 이 작업을 수행할 수 있습니다. 그 결과로 정확하게 일할 수 있는 때는 전적으로 PromiseLifecycle이 어떻게 진행되는지에 달려 있습니다.

Promise 수명주기

PromisePending State에서 시작하는 짧은 Lifecycle을 거치며 비동기 작업이 아직 완료되지 않았음을 나타냅니다. Pending StatePromise불안정(unsettled)한 것으로 간주됩니다. 마지막 예제의 PromisereadFile() 함수가 리턴하는 즉시 Pending State입니다. 비동기 작업이 완료되면 Promise확정(settled)된 것으로 간주되어 두가지 상태중 하나가 됩니다.

  1. Fulfilled(완료됨) : Promise의 비동기 작업이 성공적으로 완료되었습니다.
  2. Rejected(거절됨) : Promise의 비동기 작업이 오류 또는 다른 원인으로 인해 성공적으로 완료되지 않았습니다.

Promise State를 반영하기 위해 내부 [[PromiseState]] 프로퍼티가 "pending", "fulfilled" 또는 "rejected"로 설정됩니다. 이 프로퍼티는 Promise 개체에 노출되지 않으므로 프로그래밍 방식으로 Promise State를 확인할 수 없습니다. 그러나 `then() 메서드를 사용하여 PromiseState가 바꿀때 특정 동작을 취할 수 있습니다.

then() 메서드는 모든 Promise에 존재하며 두개의 파라미터를 받습니다. 첫 번째 파라미터는 Promise가 수행될 때 호출할 함수입니다. 비동기 작업과 관련된 추가 데이터가 이 처리 함수에 전달됩니다. 두 번째 파라미터는 Promise가 거부될 때 호출할 함수입니다. 수행 기능과 마찬가지로 거부 기능에는 거부와 관련된 추가 데이터가 전달됩니다.

이런 식으로 then() 메서드를 구현하는 객체를 모두 Thenable이라고 합니다. 모든 PromiseThenable이지만, 모든 ThenablePromise인 것은 아닙니다.

then()에 대한 두 파라미터는 모두 선택 사항이므로 수행 및 거부의 조합을 만들수 있습니다. 예를 들어, 다음의 then() 호출 집합을 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let promise = readFile("example.txt");
promise.then(function(contents) {
// fulfillment
console.log(contents);
}, function(err) {
// rejection
console.error(err.message);
});
promise.then(function(contents) {
// fulfillment
console.log(contents);
});
promise.then(null, function(err) {
// rejection
console.error(err.message);
});

세번 모두 then() 호출은 같은 Promise에 작동합니다. 첫 번째 호출은 수행과 거절을 모두 받습니다. 두 번째는 수행만 받고 오류는 보고되지 않습니다. 세 번째는 거절을 받고 수행을 보고하지 않습니다.

또한 Promise는 거절 처리만 전달할 때 then()과 동일한 동작을하는 catch() 메서드가 있습니다. 예를 들어 다음과 같은 catch()then() 호출은 기능적으로 동일합니다.

1
2
3
4
5
6
7
8
9
10
11
promise.catch(function(err) {
// rejection
console.error(err.message);
});
// is the same as:
promise.then(null, function(err) {
// rejection
console.error(err.message);
});

then()catch()의 목적은 비동기 작업의 결과를 적절하게 처리하기 위해 이들을 조합하여 사용하는 것입니다. 이 시스템은 작업이 성공했는지 또는 실패했는지를 확인하기 때문에 EventCallback보다 낫습니다. (Event는 오류가있을 때 실행되지 않는 경향이 있고 Callback은 오류 파라미터를 항상 확인해야합니다.) Promise에 거절를 추가하지 않으면 모든 실패가 조용히 발생합니다. 핸들러가 단지 실패만을 기록하는 경우라도 항상 거절 처리가 필요합니다.

수행 또는 거절 핸들러는 Promise가 이미 완료된 후에 Job Queue에 추가 되더라도 여전히 실행됩니다. 이를 통해 언제든지 새로운 수행 처리 및 거절 처리를 추가하고 호출을 보장 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
let promise = readFile("example.txt");
// 원래의 수행 처리
promise.then(function(contents) {
console.log(contents);
// 새로운 처리를 추가
promise.then(function(contents) {
console.log(contents);
});
});

이 코드에서, 수행 핸들러는 동일한 수행 Promise에 또 다른 수행 핸들러를 추가합니다. Promise는 이 시점에서 이미 완료되었으므로 새 수행 핸들러가 작업 대기열에 추가되고 준비가되면 호출됩니다. 거절 핸들러도 같은 방식으로 작동합니다.

then() 또는 catch()를 호출할 때마다 Promise가 해결될 때 실행될 새 Job이 만들어집니다. 그러나 이러한 JobPromise을 위해 엄격하게 예약된 별도의 Job Queue에서 끝납니다. 이 두 번째 Job Queue의 정확한 세부 사항은 일반적인 Job Queue가 작동하는 방식을 이해하는 정도면 됩니다. 이 사항은 Promise 사용 방법을 이해하는 데 중요하지 않습니다.

Unsettled(불확실한) Promise 생성하기

새로운 PromisePromise 생성자를 사용하여 생성됩니다. 이 생성자는 Promise를 초기화하는 코드를 포함하는 Executor라는 함수를 파라미터로 받아들입니다. Executor에는 resolve()reject()라는 두개의 함수가 파라미터로 전달됩니다. resolve() 함수는 Executor가 성공적으로 완료되면 Promise가 해결될 준비가되었음을 알리기 위해 호출되고 reject() 함수는 Executor가 실패했음을 나타냅니다.

이 장의 앞 부분에서 보여주었던 readFile() 함수를 Node.js의 Promise을 사용하여 구현하는 예제를 살펴 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Node.js example
let fs = require("fs");
function readFile(filename) {
return new Promise(function(resolve, reject) {
// 비동기 동작 트리거
fs.readFile(filename, { encoding: "utf8" }, function(err, contents) {
// check for errors
if (err) {
reject(err);
return;
}
// the read succeeded
resolve(contents);
});
});
}
let promise = readFile("example.txt");
// 수행과 거절 모두를 처리한다.
promise.then(function(contents) {
// 수행
console.log(contents);
}, function(err) {
// 거절
console.error(err.message);
});

이 예제는 Node.js의 기본 fs.readFile() 비동기 호출이 Promise로 래핑됩니다. Executor는 error 오브젝트를 reject() 함수에 전달하거나 파일 내용을 resolve() 함수에 전달합니다.

readFile()이 호출되면 Executor가 즉시 실행된다는 점에 유의하십시오. Executor resolve() 또는 reject()가 호출되면 Promise을 해결하기 위해 작업이 Job Queue에 추가됩니다. 이를 Job Scheduling이라고하며, setTimeout() 또는 setInterval() 함수를 사용한 적이 있다면 이미 익숙한 것입니다. Job Scheduling에서 새로운 작업을 Job Queue에 추가하여 “지금 당장 실행하지 말고 나중에 실행하십시오.”라고 말합니다. 예를 들어 setTimeout() 함수를 사용하면 작업이 대기열에 추가되기 전에 지연을 지정할 수 있습니다.

1
2
3
4
5
6
// 500ms가 지난 후에 이 함수를 작업 대기열에 추가하십시오.
setTimeout(function() {
console.log("Timeout");
}, 500);
console.log("Hi!");

이 코드는 500ms 후에 Job Queue에 추가될 작업을 예약합니다. 두개의 console.log() 호출은 다음과 같은 내용을 출력합니다.

1
2
Hi!
Timeout

500ms 지연 덕분에 console.log( "Hi!") 호출의 출력 후에 setTimeout()에 전달된 함수가 표시되었습니다.

Promise도 비슷하게 작동합니다. Promise Executor는 소스 코드에서 그 이후에 나타나는 것보다 먼저 실행됩니다.

1
2
3
4
5
6
let promise = new Promise(function(resolve, reject) {
console.log("Promise");
resolve();
});
console.log("Hi!");

이 코드의 출력은 다음과 같습니다.

1
2
Promise
Hi!

resolve()를 호출하면 비동기 작업이 트리거됩니다. then()catch()에 전달된 함수는 비동기적으로 실행됩니다. 이러한 함수도 Job Queue에 추가됩니다. 다음은 그 예입니다.

1
2
3
4
5
6
7
8
9
10
let promise = new Promise(function(resolve, reject) {
console.log("Promise");
resolve();
});
promise.then(function() {
console.log("Resolved.");
});
console.log("Hi!")

이 예제의 결과는 다음과 같습니다.

1
2
3
Promise
Hi!
Resolved

then()에 대한 호출이 console.log( "Hi!") 행 앞에 나타나더라도 실제로 먼저(Executor와 달리) 실행되지는 않습니다. 이는 Executor가 완료된 후 수행 및 거절 핸들러가 Job Queue의 끝에 추가되기 때문입니다.

Settled(확정된) Promise 생성하기

Promise 생성자는 Promise Executor의 역동적인 특성 때문에 불확실한 Promise를 만드는 가장 좋은 방법입니다. 그러나 알려진 단일 값을 나타내는 Promise를 원한다면 resolve() 함수에 값을 전달하는 Job을 스케쥴하는 것은 의미가 없습니다. 대신, 구체적인 가치를 부여한 Settled Promise를 만드는 두가지 방법이 있습니다.

Promise.resolve() 사용하기

Promise.resolve() 메서드는 단일 파라미터를 받아들이고 Fulfilled StatePromise를 리턴합니다. 이는 Job Scheduling이 발생하지 않으며, 값을 검색하기 위한 Promise에 하나 이상의 수행 핸들러를 추가해야 함을 의미합니다.

1
2
3
4
5
let promise = Promise.resolve(42);
promise.then(function(value) {
console.log(value); // 42
});

이 코드는 수행 핸들러가 value로 42를 설정 하도록 수행 Promise를 작성합니다. 이 Promise는 절대 Rejected State가 되지 않기
때문에 거절 핸들러가 추가되어도 호출되지 않습니다.

Promise.reject() 사용하기

Promise.reject() 메서드를 사용하여 거절된 Promise를 만들수도 있습니다. Promise.resolve()와 같이 작동합니다. 단, 생성된 Promise는 다음과 같이 Rejected State입니다.

1
2
3
4
5
let promise = Promise.reject(42);
promise.catch(function(value) {
console.log(value); // 42
});

Promise에 추가된 모든 거절 핸들러는 호출되지만 수행 핸들러는 호출되지 않습니다.

PromisePromise.resolve() 또는 Promise.reject() 메서드에 전달하면 변경 내용없이 Promise가 리턴됩니다.

Promise가 아닌 Thenable

Promise.resolve()Promise.reject()는 Non-promise Thenable을 파라미터로 받을수 있습니다. Non-promise Thenable을 전달하면 이 메서드는 then() 함수 다음에 호출되는 새로운 Promise를 만듭니다.

Non-promise Thenable은 객체가 다음과 같이 resolvereject 파라미터를 받아들이는 then() 메서드를 가질 때 생성됩니다.

1
2
3
4
5
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};

이 예제에서 thenable 객체는 then() 메서드 이외의 Promise와 관련된 특징이 없습니다. 여러분은 Promise.resolve()를 호출하여 다음과 같이 thenable을 수행된 Promise로 변환할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});

이 예에서, Promise.resolve()Promise State를 결정할 수 있도록 thenable.then()을 호출합니다. then()메서드 내에서 resolve(42)가 호출되기 때문에 thenable에 대한 Promise State가 Fulfilled입니다. 새로운 p1 Promisethenable(즉, 42)에서 전달된 값으로 Fulfilled state에서 생성되고 p1의 수행 핸들러는 값으로 42를 받습니다.

같은 프로세스를 Promise.resolve()와 함께 사용하여 다음과 같은 Promise을 거절할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
let thenable = {
then: function(resolve, reject) {
reject(42);
}
};
let p1 = Promise.resolve(thenable);
p1.catch(function(value) {
console.log(value); // 42
});

이 예제는 thenable이 거부된다는 것을 제외하고는 이전 예제와 유사합니다. thenable.then()이 실행되면, 값이 42 인 Rejected State에서 새로운 Promise가 생성됩니다. 그 값은 p1의 거절 처리기로 전달됩니다.

Promise.resolve()Promise.reject()는 이와 같이 동작하여 Non-promise Thenable에 대해 쉽게 작업할 수 있도록 합니다. 많은 라이브러리는 Promise가 ECMAScript 6에 도입되기 전에 Promise을 구현하기 위해 Thenable을 사용 했으므로, Thenable을 공식 Promise로 변환할 수 있는 이 기능은 기존 라이브러리와의 하위 호환성을 위해 중요합니다. 오브젝트가 Promise인지 확신 할 수 없을 때 Promise가 변경되지 않고 통과하기 하는 Promise.resolve() 또는 Promise.reject()(예상 결과에 따라 다름)를 통해 오브젝트를 전달하는 것이 가장 좋은 방법입니다.

Executor 오류

Executor 내에서 오류가 발생하면 Promise의 거절 핸들러가 호출됩니다.

1
2
3
4
5
6
7
let promise = new Promise(function(resolve, reject) {
throw new Error("Explosion!");
});
promise.catch(function(error) {
console.log(error.message); // "Explosion!"
});

이 코드에서, Executor는 의도적으로 오류를 던집니다. 모든 Executor 내부에는 암묵적인 try-catch가 있어 오류가 포착된 다음 거절 처리기로 전달됩니다. 이전 예는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
let promise = new Promise(function(resolve, reject) {
try {
throw new Error("Explosion!");
} catch (ex) {
reject(ex);
}
});
promise.catch(function(error) {
console.log(error.message); // "Explosion!"
});

Executor는 던져진 오류를 잡아서 이러한 일반적인 유스 케이스를 간소화하고, Executor에서 발생한 오류는 거절 처리기가 있을때만 보고됩니다. 그렇지 않으면 오류가 표시되지 않습니다. 이것은 개발자가 Promise를 사용하는 초기 단계에서 문제가되었고, JavaScript environment는 거절된 Promise를 잡아 내기 위해 Hook을 제공함으로써 문제를 해결합니다.

Global Promise 거절 처리

Promise의 가장 논란이되는 측면중 하나는 거절 핸들러 없이 Promise가 거절될 때 발생하는 침묵의 실패입니다. 어떤 이들은 JavaScript 언어에서 오류를 명백히 드러내지 않는 유일한 부분이기 때문에 이것이 사양의 가장 큰 결함이라고 생각하는 사람도 있습니다.

Promise 거절이 처리되었는지 여부를 결정하는 것은 Promise의 본질 때문에 간단하지 않습니다. 예를 들어 다음 예제를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
let rejected = Promise.reject(42);
// 이 시점에서 거절은 처리되지 않습니다.
// 잠시 후...
rejected.catch(function(value) {
// 이제 거절을 처리합니다.
console.log(value);
});

어떤 시점에서 then() 또는 catch()를 호출하여 Promise가 확정(settled) 되었는지 여부에 관계없이 올바르게 작동하게 함으로써 Promise를 처리할 때를 정확하게 알기가 어렵습니다. 이 경우 Promise는 즉시 거절되지만 나중에 처리될 때까지 처리되지 않습니다.

ECMAScript의 다음 버전이 이 문제를 해결할 가능성은 있지만, 브라우저와 Node.js는 이 문제점을 해결하기 위해 변경 사항을 구현했습니다. 이는 ECMAScript 6 사양의 일부는 아니지만 Promise를 사용할 때 유용한 도구가 됩니다.

Node.js의 거절 핸들링

Node.js에는 Promise 거절 핸들링과 관련된 process 객체에 두개의 Event가 있습니다.

  • unhandledRejection : Promise가 거절되고 한회의 Event Loop에서 거절 핸들러가 호출되지 않은 경우에 발생합니다.
  • rejectionHandled : Promise가 거절되고 한회의 Event Loop 후 거절 핸들러가 호출되면 발생합니다.

이러한 Event는 거절되고 처리되지 않는 Promise를 식별하는데 도움이되도록 설계되었습니다.

unhandledRejection Event 핸들러는 거절 이유 (흔히 오류 객체)와 파라미터로 거절된 Promise가 전달됩니다. 다음 코드는 실행중인 unhandledRejection을 보여줍니다.

1
2
3
4
5
6
7
8
let rejected;
process.on("unhandledRejection", function(reason, promise) {
console.log(reason.message); // "Explosion!"
console.log(rejected === promise); // true
});
rejected = Promise.reject(new Error("Explosion!"));

이 예에서 오류 객체와 함께 거절된 Promise를 만들고 unhandledRejection Event를 수신합니다. Event 핸들러는 첫 번째 파라미터로 오류 객체를 받고 두 번째 파라미터로 Promise를 받습니다.

rejectionHandled Event 핸들러에는 거절된 Promise인 하나의 파라미터만 받습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let rejected;
process.on("rejectionHandled", function(promise) {
console.log(rejected === promise); // true
});
rejected = Promise.reject(new Error("Explosion!"));
// 거절 핸들러를 추가 할 때까지 기다립니다.
setTimeout(function() {
rejected.catch(function(value) {
console.log(value.message); // "Explosion!"
});
}, 1000);

여기서 rejectionHandled Event는 거절 핸들러가 최종적으로 호출될 때 생성됩니다. 거절 핸들러가 rejected가 생성된 후
rejected에 바로 첨부된 경우 Event가 발생하지 않습니다. 거절 핸들러가 rejected를 생성한 동일한 Event Loop에서
호출 되는 것은 유용하지 않습니다.

잠재적으로 처리되지 않은 거절을 올바르게 추적하려면 rejectionHandledunhandledRejection Event를 사용하여 처리되지 않은 거절 가능성이 있는 목록을 유지합니다.그리고 일정 기간 동안 목록을 검사합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let possiblyUnhandledRejections = new Map();
// 거절이 처리되지 않으면 Map에 추가합니다.
process.on("unhandledRejection", function(reason, promise) {
possiblyUnhandledRejections.set(promise, reason);
});
process.on("rejectionHandled", function(promise) {
possiblyUnhandledRejections.delete(promise);
});
setInterval(function() {
possiblyUnhandledRejections.forEach(function(reason, promise) {
console.log(reason.message ? reason.message : reason);
// 이 거절을 처리하기 위해 뭔가를 합니다.
handleRejection(promise, reason);
});
possiblyUnhandledRejections.clear();
}, 60000);

이코드는 처리되지 않는 거절을 추적하는 간단한 추적기입니다. 코드에서 Map을 사용하여 Promise와 거절 사유를 저장합니다. 각 Promise는 키이며, Promise의 이유는 연관된 값입니다. unhandledRejection이 발생할 때마다 Promise와 거절 이유가 Map에 추가됩니다. 그리고 rejectionHandled가 발생될 때마다 처리된 PromiseMap에서 제거됩니다. 따라서 Event가 호출될 때 possibleUnhandledRejections가 커지고 축소됩니다. setInterval() 호출은 처리되지 않은 거절 목록을 주기적으로 검사하여 정보를 console에 출력합니다.(실제로는 로그를 남기거나 다른 방식으로 거절을 처리하기 위해 뭔가 다른 작업을 수행하려고합니다). 이 예제에서
Weak Map대신 Map을 사용합니다. Map을 주기적으로 검사하여 어떤 Promise가 있는지 확인해야 하지만 Weak Map은 가능하지 않습니다.

이 예제는 Node.js에만 해당 하고, 브라우저도 처리되지 않은 거절에 대해 개발자에게 알리는 비슷한 메커니즘을 구현했습니다.

브라우저의 거절 핸들러

브라우저는 처리되지 않은 거절을 식별하는데 도움이되는 두가지 Event를 발생 시킵니다. 이러한 Eventwindow 객체에 의해 발생되고 Node.js와 동일합니다.

  • unhandledrejection : Promise가 거절되고 한회의 Event Loop에서 거절 핸들러가 호출되지 않은 경우에 발생합니다.
  • rejectionhandled : Promise가 거절되고 한회의 Event Loop가 실행된 후 거절 핸들러가 호출되면 발생합니다.

Node.js의 구현은 개별 파라미터를 Event 핸들러에 전달하는 하지만, 브라우저의 Event 핸들러는 다음과 같은 프로퍼티를 갖는 Event 객체를 받습니다.

  • type : 이벤트 이름 ( "unhandledrejection" 또는 "rejectionhandled").
  • promise : 거절된 Promise 객체.
  • reason : Promise의 거절된 값

브라우저 구현의 다른점은 두 Event 모두에서 거절값(reason)을 사용할 수 있다는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let rejected;
window.onunhandledrejection = function(event) {
console.log(event.type); // "unhandledrejection"
console.log(event.reason.message); // "Explosion!"
console.log(rejected === event.promise); // true
};
window.onrejectionhandled = function(event) {
console.log(event.type); // "rejectionhandled"
console.log(event.reason.message); // "Explosion!"
console.log(rejected === event.promise); // true
};
rejected = Promise.reject(new Error("Explosion!"));

이 코드는 onunhandledrejectiononrejectionhandled의 DOM Level 0 표현식을 사용하여 두 Event 핸들러를 할당합니다. 원하는 경우 addEventListener( "unhandledrejection")addEventListener("rejectionhandled")를 사용할 수도 있습니다. 각 Event 핸들러는 거절된 Promise에 대한 정보를 포함하는 Event 객체를 받습니다. type, promisereason 프로퍼티는 모두 두 Event 핸들러에서 사용할 수 있습니다.

브라우저에서 처리되지 않은 거절을 추적하는 코드는 Node.js 코드와 매우 유사합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let possiblyUnhandledRejections = new Map();
// 거절이 처리되지 않으면 Map에 추가합니다.
window.onunhandledrejection = function(event) {
possiblyUnhandledRejections.set(event.promise, event.reason);
};
window.onrejectionhandled = function(event) {
possiblyUnhandledRejections.delete(event.promise);
};
setInterval(function() {
possiblyUnhandledRejections.forEach(function(reason, promise) {
console.log(reason.message ? reason.message : reason);
// 이 거절을 처리하기 위해 뭔가를 합니다.
handleRejection(promise, reason);
});
possiblyUnhandledRejections.clear();
}, 60000);

이 구현은 Node.js 구현과 거의 동일합니다. Promise와 거절 값을 Map에 저장한 다음 나중에 검사하는 것과 같은 접근 방식을 사용합니다. 유일한 실제 차이점은 Event 핸드러에서 정보를 검색하는 위치입니다.

Promise 거절을 처리하는 것은 까다로울 수 있지만, 실제로 얼마나 강력한 Promise가 될 수 있는지를 이제 알기 시작했습니다. 이제 다음 단계로 몇개의 Promise를 연결해서 처리할 때입니다.

Promise 연결하기

이 시점에서 PromiseCallbacksetTimeout() 함수를 이용한 조합보다 조금더 개선된 것으로 보이지만, 눈에 보이는 것보다 훨씬더 많은 Promise가 있습니다. 보다 구체적으로, 보다 복잡한 비동기 동작을 수행하기 위해 Promise를 서로 연결하는 여러 가지 방법이 있습니다.

then() 또는 catch()를 호출할 때마다 실제로 또 다른 Promise가 만들어지고 리턴됩니다. 이 두 번째 Promise는 첫 번째 Promise가 수행되거나 거절된 후에만 처리됩니다. 다음 예제를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
console.log(value);
}).then(function() {
console.log("Finished");
});

위 코드의 결과는 다음과 같습니다.

1
2
42
Finished

p1.then()에 대한 호출은 두 번째 Promise를 리턴하고 then()이 호출됩니다. 두 번째 then() 수행 핸들러는 첫 번째 Promise가 해결된 후에만 호출됩니다. 이 예제의 연결을 해제하면 다음과 같이 보입니다.

1
2
3
4
5
6
7
8
9
10
11
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = p1.then(function(value) {
console.log(value);
})
p2.then(function() {
console.log("Finished");
});

이 연결되지 않은 코드 버전에서 p1.then()의 결과는 p2에 저장되고 p2.then()은 최종 수행 핸들러를 추가하기 위해 호출됩니다. 여러분이 짐작 했겠지만, p2.then() 호출도 Promise를 리턴하지만 그 Promise는 사용하지 않습니다.

Error 포착하기

Promise 체인을 사용하면 이전 Promise에서 수행 또는 거절 핸들러에서 발생할 수 있는 에러를 포착 할 수 있습니다.

1
2
3
4
5
6
7
8
9
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
throw new Error("Boom!");
}).catch(function(error) {
console.log(error.message); // "Boom!"
});

이 코드에서 p1의 수행 핸들러는 에러를 던집니다. 두 번째 Promisecatch() 메서드에 대한 연결된 호출은 거절 핸들러를 통해 해당 에러를 수신할 수 있습니다. 거절 핸들러가 에러를 throw 하는 경우에도 마찬가지입니다.

1
2
3
4
5
6
7
8
9
10
let p1 = new Promise(function(resolve, reject) {
throw new Error("Explosion!");
});
p1.catch(function(error) {
console.log(error.message); // "Explosion!"
throw new Error("Boom!");
}).catch(function(error) {
console.log(error.message); // "Boom!"
});

여기에서 Executor가 오류를 발생시켜 다음 p1 Promise의 거절 핸들러를 트리거합니다. 그 핸들러는 두 번째 Promise의 거절 핸들러가 포착할 또 다른 에러를 발생시킵니다. 연결된 Promise 호출은 체인의 다른 Promise의 에러를 인식합니다.

에러가 발생할 때 올바르게 처리할 수 있도록 Promise 체인의 끝부분에는 항상 거절 핸들러가 있어야합니다.

Promise 체인의 리턴 값

Promise 체인의 또 다른 중요한 부분은 하나의 Promise에서 다음 Promise로 데이터를 전달할 수 있다는 것입니다. Executor 내에서 resolve() 핸들러로 전달된 값이 해당 Promise에 대한 수행 핸들러로 전달되는 것을 이미 보았습니다. 수행 핸들러에서 리턴값을 지정하여 체인을 따라 데이터를 계속 전달할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
console.log(value); // "42"
return value + 1;
}).then(function(value) {
console.log(value); // "43"
});

p1에 대한 수행 핸들러는 실행될 때 value + 1을 리턴합니다. value는 Executor로부터 42를 입력 받고, 수행 핸들러는 43을 리턴합니다. 이 값은 두 번째 Promise의 수행 핸들러로 전달되어 콘솔로 출력됩니다.

거절 핸들러로도 똑같이 할 수 있습니다. 거절 핸들러가 호출되면 값을 리턴할 수 있습니다. 만약 그렇다면, 그 값은 다음과 같이 체인의 다음 Promise를 수행하는데 사용될수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
let p1 = new Promise(function(resolve, reject) {
reject(42);
});
p1.catch(function(value) {
// first fulfillment handler
console.log(value); // "42"
return value + 1;
}).then(function(value) {
// second fulfillment handler
console.log(value); // "43"
});

여기서 Executor는 42로 reject()를 호출합니다. 이 값은 Promise의 거절 핸들러로 전달되며 여기서 value + 1이 리턴됩니다. 이 리턴 값은 거절 핸들러에서 가져오지만 체인에서 다음 Promise의 수행 핸들러에서 계속 사용됩니다. 만약 필요하다면 한 Promise의 실패는
전체 체인에서 복구할 수 있습니다.

Promise 체인에서 Promise 리턴하기

수행 및 거절 핸들러에서 Primitive 값을 리턴하면 Promise 사이에 데이터를 전달할 수 있지만 객체를 리턴하려면 어떻게 해야 할까요? 객체가 Promise라면 진행 방법을 결정하기위한 추가 단계가 필요합니다. 다음 예제를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
p1.then(function(value) {
// first fulfillment handler
console.log(value); // 42
return p2;
}).then(function(value) {
// second fulfillment handler
console.log(value); // 43
});

이 코드에서 p1은 42로 해결(Resolve)되는 작업을 스케쥴합니다. p1의 수행 핸들러는 이미 해결된 상태인 Promisep2를 리턴합니다. p2가 완료되었으므로 두 번째 수행 핸들러가 호출됩니다. p2가 거부되면 두 번째 수행 핸들러 대신 거절 핸들러(있는 경우)가 호출됩니다.

이 패턴에 대해 알아야 할 중요한 점은 두 번째 수행 핸들러가 p2에 추가되지 않고 오히려 세 번째 Promise에 추가된다는 것입니다. 따라서 두 번째 수행 핸들러는 세 번째 Promise에 첨부되며, 앞의 예제를 다음과 같이 만들수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = p1.then(function(value) {
// first fulfillment handler
console.log(value); // 42
return p2;
});
p3.then(function(value) {
// second fulfillment handler
console.log(value); // 43
});

여기에서 두 번째 수행 처리기가 p2보다는 p3에 연결되어 있음이 명확합니다. p2가 거절되는 경우 두 번째 수행 핸들러가 호출되지 않으므로 이것은 미묘하지만 중요한 차이점입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
reject(43);
});
p1.then(function(value) {
// first fulfillment handler
console.log(value); // 42
return p2;
}).then(function(value) {
// second fulfillment handler
console.log(value); // 절대 호출되지 않음
});

이 예제에서는 p2가 거절되므로 두 번째 이행 핸들러가 호출되지 않습니다. 하지만 대신 거절 핸들러를 연결할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
reject(43);
});
p1.then(function(value) {
// first fulfillment handler
console.log(value); // 42
return p2;
}).catch(function(value) {
// rejection handler
console.log(value); // 43
});

여기서, 거절 핸들러는 p2가 거절된 결과 호출됩니다. p2에서 거절된 값 43은 해당 거절 핸들러로 전달됩니다.

Promise Executor가 실행될 때 수행 또는 거절 핸들러로부터 Thenable을 리턴하는 것은 변하지 않습니다. 첫 번째 정의된 Promise는 먼저 Executor를 실행한 다음 두 번째 Promise의 Executor를 실행합니다. Thenable를 반환하면 간단하게 Promise 결과에 대한 추가 응답을 정의할 수 있습니다. 수행 핸들러 내에서 새로운 Promise를 작성하여 수행 핸들러의 실행을 연기할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
console.log(value); // 42
// create a new promise
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
return p2
}).then(function(value) {
console.log(value); // 43
});

이 예제에서 p1의 수행 핸들러에 새 Promise가 생성됩니다. 즉, 두 번째 수행 핸들러는 p2가 수행될 때까지 실행되지 않습니다. 이 패턴은 이전 Promise가 확정될 때까지 Promise가 기다리길 원할때 유용합니다.

Multiple Promise에 대한 대응

지금까지 이 장의 각 예제에서는 한번에 하나의 Promise에 응답하는 방법을 다루었습니다. 그러나 때로는 다음 작업을 결정하기 위해 여러 Promise의 진행 상태를 모니터링 해야할 수도 있습니다. ECMAScript 6은 Promise.all()Promise.race()와 같이 여러 Promise를 모니터링하는 두가지 메서드를 제공합니다.

Promise.all() 메서드

Promise.all() 메서드는 모니터링 할 Promise들을 Iterable(Array와 같은)하게 만든 단일 파라미터를 받아들이고 Iterable의 모든 Promise가 해결되었을 때 해결된 Promise를 리턴합니다. 리턴된 Promise는 아래 예제처럼 Iterable의 모든 Promise가 충족될 때 수행됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.then(function(value) {
console.log(Array.isArray(value)); // true
console.log(value[0]); // 42
console.log(value[1]); // 43
console.log(value[2]); // 44
});

각각의 Promise는 숫자로 Resolve됩니다. Promise.all()에 대한 호출은 Promise p4를 생성합니다. 이것은 p1, p2p3 Promise가 충족될 때 수행됩니다. p4의 수행 핸들러에 전달된 결과는 각 Resolve된 값 42, 4344를 포함하는 Array입니다. 값은 PromisePromise.all에 전달된 순서대로 저장되므로 Promise 결과를 Resolve한 Promise와 일치시킬 수 있습니다.

Promise.all()에 전달된 Promise가 거절되면 다른 Promise가 완료될 때까지 기다리지 않고 즉시 반환된 Promise는 거절됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
reject(43);
});
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.catch(function(value) {
console.log(Array.isArray(value)) // false
console.log(value); // 43
});

이 예제에서 p2는 43의 값을 사용하여 거절됩니다. p4에 대한 거절 핸들러는 p1 또는 p3가 실행 완료될 때까지 기다리지 않고 즉시 호출됩니다(여전히 실행은 완료하지만, p4가 대기하지 않습니다).

거절 핸들러는 항상 Array가 아닌 단일값을 받으며 그값은 거절된 Promise의 거절값입니다. 이 경우, 거절 핸들러에는 p2로부터의 거절을 반영하기 위해 43이 입력됩니다.

Promise.race() 메서드

Promise.race() 메서드는 약간 다른 방법으로 여러 Promise를 모니터링하는 방법을 제공합니다. 이 메서드 또한 PromiseIterable을 파라미터로 받고 Promise를 리턴하지만, 리턴된 Promise는 입력된 Iterable Promise들 중 어떤 Promise가 처음으로 확정(settled) 되자마자 확정(settled) 됩니다. Promise.all() 메서드처럼 모든 Promise가 수행되기를 기다리는 대신 Promise.race() 메서드는 Iterable Promise가 수행되는 즉시 적절한 Promise를 리턴합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.race([p1, p2, p3]);
p4.then(function(value) {
console.log(value); // 42
});

이 코드에서 p1은 수행된 Promise로 만들어지며 다른것들은 스케쥴 작업으로 생성됩니다. p4에 대한 수행 핸들러는 42라는 값으로 호출되고 다른 Promise를 무시합니다. Promise.race()에 전달된 Promise는 어느 것이 먼저 확정(settled)되는지 보기위한 경쟁입니다. 첫 번째 Promise가 확정되면 리턴된 Promise가 수행됩니다. 첫 Promise의 확정이 거절되면 리턴된 Promise는 거절됩니다. 다음은 거절 사례입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = Promise.reject(43);
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.race([p1, p2, p3]);
p4.catch(function(value) {
console.log(value); // 43
});

Promise.race()가 호출될 때 이미 p2가 거절된 상태이므로 p4가 거절됩니다. p1p3가 수행 되더라도 p2가 거절된 후에 발생하므로 무시됩니다

Promise에서 상속 받기

다른 Built-in 타입과 마찬가지로 파생 클래스의 기반으로 Promise를 사용할 수 있습니다. 이를 통해 자신만의 객체를 정의하여 Built-in Promise를 확장할 수 있습니다. 예를 들어, 일반적인 then()catch() 메서드 외에도 success()failure()라는 메서드를 사용할 수 있는 Promise를 만들고 싶다고 가정 해보십시오. 다음과 같이 Promise 타입을 작성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyPromise extends Promise {
// 기본 생성자를 사용
success(resolve, reject) {
return this.then(resolve, reject);
}
failure(reject) {
return this.catch(reject);
}
}
let promise = new MyPromise(function(resolve, reject) {
resolve(42);
});
promise.success(function(value) {
console.log(value); // 42
}).failure(function(value) {
console.log(value);
});

이 예에서 MyPromisePromise에서 파생되었으며 두가지 추가 메서드가 있습니다. success() 메서드는 resolve()를 모방하고 failure()reject() 메서드를 모방합니다.

추가된 각 메서드는 이를 사용하여 모방하는 메서드를 호출합니다. 파생된 PromiseBuilt-in Promise와 동일하게 작동하지만 원하는 경우 success()failure()를 호출할 수 있습니다.

정적 메서드도 상속되므로 MyPromise.resolve(), MyPromise.reject(), MyPromise.race()MyPromise.all() 메서드는 파생된 Promise에도 있습니다. 마지막 두 메서드는 기본 제공 메서드와 동일하게 동작하지만 처음 두 메서드는 약간 다릅니다.

MyPromise.resolve()MyPromise.reject()는 전달된 값에 관계없이 MyPromise의 인스턴스를 리턴합니다. 리턴할 Promise의 타입을 결정하기 위해 Symbol.species 프로퍼티(9 장에서 다뤘습니다.)를 사용하기 때문에 전달된 값에 관계없이 MyPromise의 인스턴스를 리턴합니다. Built-in Promise가 두 메서드 중 하나에 전달되면 Promise가 수행되거나 거절되고 메서드는 새로운 MyPromise를 반환하여 수행 및 거절 핸들러를 할당할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = MyPromise.resolve(p1);
p2.success(function(value) {
console.log(value); // 42
});
console.log(p2 instanceof MyPromise); // true

여기에서 p1MyPromise.resolve() 메서드로 전달되는 Built-in Promise입니다. 결과 p2p1의 Resolve된 값이 수행 핸들러로 전달되는 MyPromise의 인스턴스입니다.

MyPromise 인스턴스가 MyPromise.resolve() 또는 MyPromise.reject() 메서드에 전달되면 Resolve되지 않고 바로 리턴됩니다. 하지만 이 두 메서드의 다른 모든 실행은 Promise.resolve()Promise.reject()와 동일하게 동작합니다.

비동기 작업 실행

8 장에서는 Generator를 소개하고 이를 비동기 작업 실행에 사용할 수있는 방법을 보여주었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
let fs = require("fs");
function run(taskDef) {
// iterator를 만들고 다른곳에서 사용할수 있게 만든다.
let task = taskDef();
// task 시작
let result = task.next();
// next() 호출을 유지하는 재귀 함수
function step() {
// 할일이 더 있다면
if (!result.done) {
if (typeof result.value === "function") {
result.value(function(err, data) {
if (err) {
result = task.throw(err);
return;
}
result = task.next(data);
step();
});
} else {
result = task.next(result.value);
step();
}
}
}
// process 시작
step();
}
// task runner와 함께 사용할 함수를 정의
function readFile(filename) {
return function(callback) {
fs.readFile(filename, callback);
};
}
// task 실행
run(function*() {
let contents = yield readFile("config.json");
doSomethingWith(contents);
console.log("Done");
});

이 구현에는 몇가지 문제점이 있습니다. 첫째, 함수를 리턴하는 함수에서 모든 함수를 래핑하는 것은 다소 혼란 스럽습니다 (이 문장조차 혼란 스럽습니다). 둘째, Task runner의 Callback으로 의도된 함수 리턴값과 Callback이 아닌 리턴값을 구별할 수있는 방법이 없습니다.

Promise를 통해 각 비동기 작업이 Promise를 반환하도록 함으로써 이 프로세스를 크게 단순화하고 일반화할 수 있습니다. 공통 인터페이스는 비동기 코드를 크게 단순화할 수 있음을 의미합니다. Task runner를 단순화 할 수있는 방법은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
let fs = require("fs");
function run(taskDef) {
// iterator 생성
let task = taskDef();
// task 시작
let result = task.next();
// 반복하는 재귀 함수
(function step() {
// 할일이 더 있다면
if (!result.done) {
// promise를 resolve 하면 쉽게 해결됩니다.
let promise = Promise.resolve(result.value);
promise.then(function(value) {
result = task.next(value);
step();
}).catch(function(error) {
result = task.throw(error);
step();
});
}
}());
}
// task runner에서 사용할 함수를 정의
function readFile(filename) {
return new Promise(function(resolve, reject) {
fs.readFile(filename, function(err, contents) {
if (err) {
reject(err);
} else {
resolve(contents);
}
});
});
}
// task 실행
run(function*() {
let contents = yield readFile("config.json");
doSomethingWith(contents);
console.log("Done");
});

이 버전의 코드에서 범용 run() 함수는 Generator를 실행하여 Iterator를 만듭니다. 작업을 시작하기 위해 task.next()를 호출하고 Iterator가 완료될 때까지 step()을 재귀적으로 호출합니다.

step() 함수안에서 할일이 더 있다면 result.donefalse입니다. 그 시점에서 result.value는 Promise이어야합니다. 그러나 문제의 함수가 Promise를 반환하지 않은 경우 Promise.resolve()가 호출됩니다.(Promise.resolve()는 전달된 Promise는 통과시키고 Non-promisePromise로 래핑한다는 것을 기억하세요). 그런 다음 Promise 값을 검색하고 이 값을 Iterator에 전달하는 수행 핸들러가 추가됩니다. 그 후, resultstep() 함수가 스스로를 호출하기 전에 다음 yield 결과에 할당됩니다.

거절 핸들러는 거절 결과를 오류 객체에 저장합니다. task.throw() 메서드는 에러 객체를 Iterator로 되돌려 보내고, 만약 태스크에 에러가 잡히면 result가 다음 yield 결과에 할당됩니다. 마지막으로 step()catch() 내부에서 호출하여 계속 진행합니다.

run() 함수는 개발자에게 Promise(또는 Callback)을 노출시키지 않고 비동기 코드를 얻기 위해 yield를 사용하는 모든 Generator를 실행할 수 있습니다. 사실 함수 호출의 리턴 값은 항상 Promise로 래핑되어 있기 때문에 함수는 Promise 이외의 것을 리턴할 수도 있습니다. 즉, yield를 사용하여 호출할 때 동기및 비동기 메서드가 모두 올바르게 작동한다는 것을 의미하므로 리턴값이 Promise임을 확인할 필요가 없습니다.

유일한 관심사는 readFile()과 같은 비동기 함수가 해당 State를 올바르게 식별하는 Promise를 리턴하는지 확인하는 것입니다. Node.js Built-in 메서드의 경우 Callback을 사용하지 않고 Promise를 리턴하도록 이러한 메서드를 변환해야합니다.

미래의 비동기 Task 실행

필자가 글을 쓰고있는 지금 JavaScript로 실행되는 비동기 태스크에 간단한 구문을 사용하는 작업이 진행중입니다. 이전 섹션의 Promise 기반 예제와 유사한 await구문으로 진행 중입니다. 기본적인 아이디어는 Generator 대신에 async로 표시된 함수를 사용하고 함수를 호출할 때 yield 대신에 await을 사용하는 것입니다.

1
2
3
4
5
(async function() {
let contents = await readFile("config.json");
doSomethingWith(contents);
console.log("Done");
});

async 키워드가 function보다 먼저 나오면 함수가 비동기 방식으로 실행된다는 의미입니다. await 키워드는 readFile("config.json") 함수 호출이 Promise를 리턴해야 한다는 것을 알려주고, 그렇지 않을 경우 응답을 Promise로 감싸야합니다. 앞절에서 run()을 구현한 것과 마찬가지로, awaitPromise가 거절되면 에러를 던지고 그렇지 않을 경우 값을 반환합니다. 최종 결과는 Iterator 기반 상태 시스템을 관리하는 오버 헤드없이 동기식인 것처럼 비동기 코드를 작성하는 것입니다.

await 구문은 ECMAScript 2017 (ECMAScript 8)에서 완성될 것으로 예상됩니다.

요약

Promise는 JavaScript에서 비동기 프로그래밍을 개선하기 위해 설계되었습니다. EventCallback보다 비동기 작업에 대한 제어력과 합성 가능성을 향상시켜줍니다. Promise 스케줄 작업은 JavaScript 엔진의 Job Queue에 추가되어 나중에 실행될 수 있습니다. 그리고 두 번째 Job Queue는 적절한 실행을 보장하기 위해 Promise 수행 및 거절 핸들러를 추적합니다.

Promise에는 Pending(보류), Fulfilled(수행), Rejected(거절)이라는 세가지 상태가 있습니다. Promise는 보류중 상태에서 시작하여 성공적인 실행에서 수행되거나 실패로 인해 거부됩니다. 두경우 모두 Promise가 확정(settled)되는 시점을 나타내는 핸들러를 추가할 수 있습니다. then() 메서드를 사용하면 수행 및 거절 핸들러를 할당할 수 있으며 catch() 메서드를 사용하면 거절 핸들러만 할당할 수 있습니다.

Promise를 여러 가지 방식으로 연결하여 정보를 전달할 수 있습니다. then()을 호출할 때마다 이전 Promise가 Resolve될 때 Resolve
되는 새로운 Promise가 만들어지고 리턴됩니다. 이러한 체인을 사용하여 일련의 비동기 이벤트에 대한 응답을 트리거할 수 있습니다. 또한 Promise.race()Promise.all()을 사용하여 여러 Promise의 진행 상황을 모니터링하고 그에 따라 응답할 수 있습니다.

Promise는 비동기 작업이 리턴할 수 있는 공통 인터페이스를 제공하므로 GeneratorPromise를 결합할 때 비동기 작업 실행이 더 쉽습니다. Generatoryield 연산자를 사용하여 비동기 응답을 기다리고 적절하게 응답할 수 있습니다.

대부분의 새로운 웹 API는 Promise를 바탕으로 구축되고 있으며 향후 더 많은 것을 기대할 수 있습니다.


이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://leanpub.com/understandinges6/read#leanpub-auto-promises-and-asynchronous-programming]

참고

공유하기