프로미스는 아래와 같은 문법으로 만든다.
let promise = new Promise(function(resolve, reject) {
// executor (제작 코드, '가수')
});
new Promise에 전달되는 함수를 executer
라고 부른다.
executor
는 resolve
와 reject
인수를 가지고 있다.
executor
는 인수로 넘겨준 콜백 중 하나를 반드시 호출해야한다.
resolve(value)
— 일이 성공적으로 끝난 경우, 그 결과를 나타내는 value
와 함께 호출reject(error)
— 에러 발생 시 에러 객체를 나타내는 error
와 함께 호출new Promise
생성자가 반환하는 프로미스 객체는 state
, result
내부 프로퍼티를 갖는다. 내부 프로퍼티이기 때문에 직접 접근할 수 없다.
resolve
가 호출되면 "fulfilled", reject
가 호출되면 "rejected"로 변합니다.resolve(value)
가 호출되면 value로, reject(error)
가 호출되면 error로 변합니다.프로미스는 반드시 성공이나 실패를 해야한다.
then(1, 2)의 첫 번째 인수는 프로미스가 이행되었을 때 실행되고
두번째 인수는 프로미스가 거부되었을 때 실행된다.
에러가 발생한 경우만 다루고 싶다면 .catch(f)를 써도 된다. .catch()는 .then(null, f)와 완전히 같다.
프로미스가 실행되면 f가 반드시 실행되게 한다는 점에서 .finally(f) 호출은 .then(f, f)과 유사
차이점은 다음과 같다.
finally 핸들러에는 인수가 없고 프로미스가 이행되었는지 거부되었는지 알 수 없다.
finally 핸들러는 자동으로 다음 핸들러에 결과와 에러를 전달한다.
new Promise((resolve, reject) => {
setTimeout(() => resolve("결과"), 2000)
})
.finally(() => alert("프라미스가 준비되었습니다."))
.then(result => alert(result)); // <-- .then에서 result를 다룰 수 있음
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) { // (**)
alert(result); // 2
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) {
alert(result); // 4
});
promise.then()을 호출하면 프로미스가 반환되기 때문에 프로미스 체인을 사용할 수 있다.
핸들러가 값을 반환하면 다음 .then은 이 값을 이용해서 호출된다.
핸들러가 프로미스를 생성하거나 반환하면 이어지는 핸들러는 프로미스가 처리될 때까지 기다리다가 그 결과를 받는다.
프로미스가 거부되면 프로미스 체인 상에서 가장 가까운 rejection 핸들러로 넘어간다.
따라서 .catch는 바로 나올 필요가 없이 여러개의 .then 뒤에 올 수 있다.
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));
프로미스 executor와 프로미스 핸들러 코드 주위에는 보이지 않는 try...catch
가 있다. 예외가 발생하면 암시적 try...catch
에서 예외를 잡고 reject 처럼 다룬다.
new Promise((resolve, reject) => {
throw new Error("에러 발생!");
}).catch(alert); // Error: 에러 발생!
위의 코드가 아래 코드처럼 동작한다.
new Promise((resolve, reject) => {
reject(new Error("에러 발생!"));
}).catch(alert); // Error: 에러 발생!
에러를 던지면 catch에서 에러를 처리한다.
catch에서 에러를 처리하면 다음 .then이 실행된다. 만약 에러를 이번 catch에서 처리하지 못하면 다시 에러를 throw할 수 있다. 그러면 가장 가까운 catch로 가게된다.
에러를 처리하지 못하면 예외를 처리해줄 핸들러가 없어서 에러가 갇혀버린다.
new Promise(function() {
noSuchFunction(); // 에러 (존재하지 않는 함수)
})
.then((result) => {
// 성공상태의 프라미스를 처리하는 핸들러. 한 개 혹은 여러 개가 있을 수 있음
}); // 끝에 .catch가 없음!
reject 된 프라미스를 처리하지 못하는 경우에도 전역 에러를 생성한다.
브라우저 환경에서는 이런 에러를 unhandledrejection
이벤트로 잡을 수 있다.
window.addEventListener('unhandledrejection', function(event) {
// 이벤트엔 두 개의 특별 프로퍼티가 있습니다.
alert(event.promise); // [object Promise] - 에러를 생성하는 프라미스
alert(event.reason); // Error: 에러 발생! - 처리하지 못한 에러 객체
});
new Promise(function() {
throw new Error("에러 발생!");
}); // 에러 처리 핸들러, catch가 없음
여러개의 프로미스를 동시에 시키고 모든 프로미스가 준비될 때까지 기다리는 역할을 한다.
Promise.all은 요소 전체가 프로미스인 배열을 받고 새로운 프로미스를 반환한다.
배열 안의 프로미스가 모두 처리되면 새로운 프로미스가 이행된다. 배열 안의 프로미스들의 결과값을 담은 배열이 새로운 프로미스의 결과가 된다.
Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
]).then((result) => {}); // [1,2,3]
위 코드에서 alert는 3초 뒤에 실행된다.
만약 Promise.all에 전달되는 프로미스 중 하나라도 거부되면 Promise.all이 반환하는 프로미스는 에러와 함게 거부된다. 에러가 발생하는 즉시 다른 프로미스는 처리가 되지만 결과는 무시된다.
Promise.allSettled
는 각 프로미스가 다음과 같은 형태로 처리되어 배열로 나온다.
{status:"fulfilled", value:result}
{status:"rejected", reason:error}
Promise.all과는 다르게 요청 중 하나가 실패해도 다른 요청 결과는 여전히 존재한다.
[
{status: 'fulfilled', value: ...응답...},
{status: 'fulfilled', value: ...응답...},
{status: 'rejected', reason: ...에러 객체...}
]
Promise.race는 가장 먼저 처리되는 프로미스의 결과를 반환한다.
콜백을 받는 함수를 프로미스로 반환하는 함수로 바꾸는 것을 프로미스화라고 한다.
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`${src}를 불러오는 도중에 에러가 발생함`));
document.head.append(script);
}
// usage:
// loadScript('path/script.js', (err, script) => {...})
위 코드를 아래와 같이 프로미스화 할 수 있다.
let loadScriptPromise = function(src) {
return new Promise((resolve, reject) => {
loadScript(src, (err, script) => {
if (err) reject(err)
else resolve(script);
});
})
}
// 사용법:
// loadScriptPromise('path/script.js').then(...)
만약에 여러개의 함수를 프로미스화 해야한다면 프로미스화를 하는 헬퍼 함수를 만드는 것이 좋다. 함수 f를 받아서 프로미스화된 함수를 만드는 promise(f)를 만드는 방법은 다음과 같다.
function promisify(f) {
return function (...args) { // 래퍼 함수를 반환함
return new Promise((resolve, reject) => {
function callback(err, result) { // f에 사용할 커스텀 콜백
if (err) {
reject(err);
} else {
resolve(result);
}
}
args.push(callback); // 위에서 만든 커스텀 콜백을 함수 f의 인수 끝에 추가합니다.
f.call(this, ...args); // 기존 함수를 호출합니다. 1, 2,3 [1,2,3]
});
};
};
// 사용법:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
그런데 위 함수는 콜백함수가 모두 err, result 두 개의 인수를 받을 것이라고 가정하는 것. 만약 콜백의 인수가 더 많다면 다음과 같이 만들면 된다
function promisify(f, manyArgs = false) {
return function (...args) {
return new Promise((resolve, reject) => {
function callback(err, ...results) { // f에 사용할 커스텀 콜백
if (err) {
reject(err);
} else {
// manyArgs가 구체적으로 명시되었다면, 콜백의 성공 케이스와 함께 이행 상태가 됩니다.
resolve(manyArgs ? results : results[0]);
}
}
args.push(callback);
f.call(this, ...args);
});
};
};
// 사용법:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...)
프로미스 핸들러는 항상 비동기적으로 실행된다. 프로미스가 즉시 이행되더라도 아래에 있는 코드는 이 핸들러들이 실행되기 전에 실행된다.
let promise = Promise.resolve();
promise.then(() => alert("프라미스 성공!"));
alert("코드 종료"); // 이 얼럿 창이 가장 먼저 나타납니다.
비동기 작업을 처리하기 위해서 ECMA에서는 PromiseJobs라는 내부 큐를 명시한다. 이것을 V8엔진에서는 마이크로테스크 큐라고 부른다.
자바스크립트 엔진은 매크로테스크(이벤트큐 혹은 콜백 큐의 작업) 하나를 처리하고 난 직후 다른 매크로테스크나 렌더링 작업을 하기 전에 마이크로 테스크 큐에 있는 마이크로 테스크를 전부 처리한다.
이런 처리 순서는 마우스 좌표나 통신에 의한 데이터 변경 없이 마이크로 테스크를 동일한 환경에서 처리할 수 있다.
unhandledrejection
에러는 마이크로테스크 큐 끝에서 프로미스 에러가 처리되지 못할 때 발생한다. 엔진은 마이크로테스크 큐가 빈 이후에 프로미스를 검사하고 이들 중 하나라도 rejected
상태이면 unhandledrejection
핸들러를 트리거한다. catch가 되면 rejected
가 fullflled
로 바뀌는 듯하다.
let promise = Promise.reject(new Error("프라미스 실패!"));
setTimeout(() => promise.catch(err => alert('잡았다!')), 1000);
// Error: 프라미스 실패!
window.addEventListener('unhandledrejection', event => alert(event.reason));
위 코드가 실행이 되면? 프로미스 실패
가 먼저 나오고 이후에 잡았다
가 나중에 출력이 된다.
async와 await는 프로미스를 편하게 사용하기 위한 것이다.
async 함수를 이용하면 해당 함수는 항상 프로미스를 반환한다. 프로미스가 아닌 값을 반환하더라도 resolved 상태의 프로미스로 값을 감싸서 resolve 된 프로미스가 반환되도록 한다.
async function f() {
return 1;
}
function f() {
return new Promise(resolve => {
resolve(1);
});
}
f().then(alert); // 1
자바스크립트는 await
키워드를 만나면 프로미스가 처리될 때까지 기다린다.
프로미스가 처리되길 기다리는 동안에 엔진이 다른 스크립트를 실행하거나 이벤트 처리를 할 수 있기 때문에 cpu 리소스가 낭비되지 않는다.
일반 함수 안에서는 await
를 사용할 수 없다.
await
는 thenable
객체를 받는다. thenable
객체는 then 메서드가 있는 호출 가능한 객체를 말한다.
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// 1000밀리초 후에 이행됨(result는 this.num*2)
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
};
async function f() {
// 1초 후, 변수 result는 2가 됨
let result = await new Thenable(1);
alert(result);
}
f();
const a = await fetch();
await가 반드시 async 안에 있어야 하는 이유
프로미스는 resolve가 되거나 reject가 된 것을 caller에게 넘기는 역할을 한다. 그런데 await를 사용하면 이 중 resolve가 되는 것만 처리를 할 수가 있다. 따라서 await를 async로 감싸서 에러가 처리가 될 수 있도록 감싸는 것이다.
프로미스가 정상적으로 이행되면 await 프로미스는 객체의 result에 저장된 값을 반환한다. 반면 프로미스가 거부되면 마치 throw 문을 작성한 것처럼 에러가 던져진다.
async function f() {
try {
let response = await fetch('http://유효하지-않은-주소');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
위와 같이 try...catch를 이용해서 에러를 처리할 수 있다.
async 함수 안에서 return이 되지 않으면 undefined가 반환된다.
Promise.all 에도 await를 사용할 수 있다.