코어 자바스크립트 - 프라미스와 async, await
promise
문법let promise = new Promise(function(resolve, reject) {
// executor (제작 코드, '가수')
});
executor
(실행자, 실행 함수) : new Promise
에 전달되는 함수. 결과를 최종적으로 만들어내는 제작코드를 포함한다.
executor에서는 결과를 즉시 얻든, 늦게 얻든 처리 성공 여부에 따라 인수로 넘겨준 콜백 중 하나를 반드시 호출해야 한다.
resolve(value) : 성공적으로 끝난 경우 그 결과를 나타내는 value와 함께 호출
reject(error) : 에러 발생 시 에러 객체를 나타내는 error와 함께 호출
new Promise
생성자가 반환하는 promise
객체는 다음과 같은 내부 프로퍼티를 갖는다.
state
: 처음엔 'pending'
이었다가 resolve
가 호출되면 'fulfilled'
, reject
가 호출되면 'rejected'
로 변한다.result
: 처음엔 undefined
이었다, resolve(value)
가 호출되면 value
로, reject(error)
가 호출되면 error
로 변한다.then, catch, finally
프라미스 객체는 executor와 결과나 에러를 받을 소비함수를 이어주는 역할을 한다. 소비함수는 .then
, .catch
, .finally
메서드를 사용해 등록(구독)된다.
then
promise.then(
function(result) { /* 결과(result)를 다룹니다 */ },
function(error) { /* 에러(error)를 다룹니다 */ }
);
.then
의 첫 번째 인수는 프라미스가 이행되었을 때 실행되는 함수이고, 여기서 실행 결과를 받는다.
.then
의 두 번째 인수는 프라미스가 거부되었을 때 실행되는 함수이고, 여기서 에러를 받는다.
catch
에러가 발생한 경우만 다루고 싶다면 .then(null, errorHandlingFunction)
과 같이 null
을 첫번째 인수로 전달해도 되고, .catch(errorHandlingFunction)
을 써도 된다.
.catch(f)
===.then(null, f)
finally
프라미스가 처리되면 항상 어떤 함수를 실행하고 싶을때 finally
를 사용한다. 쓸모가 없어진 로딩 인디케이터를 먼추는 것 처럼 결과가 어떻든 마무리가 필요할때 유용하게 사용될 수 있다.
new Promise((resolve, reject) => {
/* 시간이 걸리는 어떤 일을 수행하고, 그 후 resolve·reject를 호출함 */
})
// 성공·실패 여부와 상관없이 프라미스가 처리되면 실행됨
.finally(() => 로딩 인디케이터 중지)
.then(result => result와 err 보여줌 => error 보여줌)
.finally(f)
!==.then(f, f)
둘의 차이점은 다음과 같다.
1.finally
핸들러에는 인수가 없다. 프라미스가 이행되었는지 거부되었는지 알 필요가 없기 때문이다.
2.finally
핸들러는 자동으로 다음 핸들러에 결과와 에러를 전달한다. 따라서finally
는 결과를 처리하기위해 만든 것이 아니다. 결과는finally
를 통과해서 전달된다.
3. 함수를 중복해서 쓸 필요가 없기 때문에.then(f, f)
보다 문법 측면에서 편리하다.
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;
});
프라미스 체이닝이 가능한 이유는 promise.then
을 호출하면 프라미스가 반환되기 때문이다. 프라미스가 반환되기 때문에 .then
을 호출할 수 있다.
핸들러가 값을 반환할 땐, 이 값이 프라미스의 result
가 되고 다음 then
은 이 값을 이용해 호출된다.
.then(handler)
에 사용된 핸들러가 프라미스를 생성하거나 반환하는 경우도 있다. 이 경우 이어지는 핸들러는 프라미스가 처리될 때까지 기다리다가 처리가 완료되면 그 결과를 받는다.
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
});
프론트 단에서 네트워크 요청시 자주 사용한 프라미스!
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}
function loadGithubUser(name) {
return fetch(`https://api.github.com/users/${name}`)
.then(response => response.json());
}
function showAvatar(githubUser) {
return new Promise(function(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);
});
}
// 함수를 이용하여 다시 동일 작업 수행
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
// ...
fetch('https://no-such-server.blabla') // 거부
.then(response => response.json())
.catch(err => alert(err)) // TypeError: failed to fetch (출력되는 내용은 다를 수 있음)
가장 쉬운 에러 처리 방법은 체인 끝에 .catch
를 붙이는 것이다.
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));
try...catch
프라미스 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: 에러 발생!
핸들러 안에서도 throw
를 사용해 에러를 던지면, 이 자체가 거부된 프라미스를 의미하게 된다. 따라서 제어 흐름이 가장 가까운 에러 핸들러로 넘어간다.
new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
throw new Error("에러 발생!"); // 프라미스가 거부됨
}).catch(alert); // Error: 에러 발생!
꼭 throw
문으로 만든 에러가 아니라도 모든 종류의 에러를 처리할 수 있다. 핸들러 위쪽에서 비정상적으로 발생한 에러 또한 잡는다.
.catch
안에서 throw
를 사용하면 제어 흐름이 가장 가까운 곳에 있는 에러 핸들러로 넘어간다. 여기서 에러가 성공적으로 처리되면 가장 가까운 곳에 있는 .then
핸들러로 제어 흐름이 넘어가 실행이 이어진다.
// 실행 순서: catch -> then
new Promise((resolve, reject) => {
throw new Error("에러 발생!");
}).catch(function(error) {
alert("에러가 잘 처리되었습니다. 정상적으로 실행이 이어집니다.");
}).then(() => alert("다음 핸들러가 실행됩니다."));
에러를 처리하지 못하는 상황, 즉 체인 끝에 .catch
를 추가하지 못하는 경우엔 어떻게 될까? 에러가 발생하면 프라미스는 거부상태가 된다.
new Promise(function() {
noSuchFunction(); // 에러 (존재하지 않는 함수)
})
.then(() => {
// 성공상태의 프라미스를 처리하는 핸들러. 한 개 혹은 여러 개가 있을 수 있음
}); // 끝에 .catch가 없음!
자바스크립트 엔진은 프라미스 거부를 추적하다가 위와 같은 상황에는 전역 에러를 생성한다. 브라우저 환경에서는 이런 에러를 unhandledrejection
이벤트로 잡을 수 있다.
window.addEventListener('unhandledrejection', function(event) {
// 이벤트엔 두 개의 특별 프로퍼티가 있습니다.
alert(event.promise); // [object Promise] - 에러를 생성하는 프라미스
alert(event.reason); // Error: 에러 발생! - 처리하지 못한 에러 객체
});
new Promise(function() {
throw new Error("에러 발생!");
}); // 에러 처리 핸들러, catch가 없음
브라우저 환경에서 에러가 발생했는데 .catch
가 없으면 unhandledrejection
핸들러가 트리거 된다. 이 핸들러 안에서 원하는 작업을 할 수 있다.
Promise.all(promise)
모든 프라미스가 이행될 때까지 기다렸다가 그 결과값을 담은 배열을 반환한다. 주어진 프라미스 중 하나라도 실패하면 Promise.all
은 거부되고 나머지 프라미스의 결과는 무시된다.
let promise = Promise.all([...promises...]);
Promise.all
은 요소 전체가 프라미스인 배열을 받고 새로운 프라미스를 반환한다. 배열 안 프라미스가 모두 처리되면 새로운 프라미스가 이행되는데, 배열 안 프라미스의 결과값을 담은 배열이 새로운 프라미스의 result
가 된다.
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(alert); // 프라미스 전체가 처리되면 1, 2, 3이 반환됩니다. 각 프라미스는 배열을 구성하는 요소가 됩니다.
위 코드에서 result
는 배열 [1, 2, 3]
이다. 위 코드에서 알 수 있듯이, 첫 번째 프라미스가 가장 늦게 이행되지만 배열의 첫번째 요소에 그 결과가 저장된다.
Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("에러 발생!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: 에러 발생!