콜백함수로 프로그램의 비동기 코드를 작성하면 순차성과 믿음성(언제 내 코드가 실행될건지)이 결여되는 중요한 결함이 있다.
콜백의 문제는 제어의 역전이다. 내 콜백 함수를 다른 곳에 전달하고 정상적으로 콜백이 실행되길 기도할 수 밖에 없다.
하지만 제어의 역전을 되역전시킬수 있다면? 다른 파트에 프로그램 진행을 넘겨주지 않고, 개발자가 언제 작업이 끝날지 알 수 있고, 무슨 일을 해야할 지 스스로 결정할 수 있다면?
그게 바로 프로미스다.
Promise - 비동기적으로 미래에 내가 원하는 값을 받거나 내가 원하는 에러 처리를 하는 방법
기존의 콜백만을 사용하여 비동기 프로그래밍을 하면 다음과 같은 문제가 있다.
ES6에 나온 이쁜이 프로미스는 이것들에 대한 해결책을 제시하고 있다.
let p = new Promise(function (resolve, reject) {
// 무언가 처리중...
...
resolve(value); // resolve() 함수를 통한 값 전달
if(!value) reject(new Error("큰일이 난 모양입니다."));
});
Promise 생성자는 항상 new 키워드와 함께 쓰며 콜백 함수를 인자로 받는다.
Promise 생성자의 인자로 받는 콜백 함수는 두 개의 인자를 가져야 한다.
resolve
와 reject
는 js쪽에서 제공하는 콜백 함수다.
resolve
- then()
핸들러로 값을 전달해 줄 수 있는 함수이다. Promise 안에서 resolve
호출을 통해 약속을 이룰 수 있다.
reject
- catch()
핸들러로 값을 전달해 줄 수 있는 함수이다. Promise 안에서 reject
호출을 통해 약속을 버릴 수 있다.
(then()
과 catch()
는 밑에 작성되어 있다.)
Promise는 상태를 가지고 있다. Promise의 상태를 통해 개발자는 해당 promise의 작업 현황을 알 수 있다.
Promise의 상태는 세 가지다. pending (미결)
, fulfilled (이행됨)
, rejected (거부됨)
생성된 Promise는 기본적으로 pending
상태다.
Promise의 내부 로직에 의해 resolve 함수가 실행되면 fulfilled
,
그러지 못하고 reject 함수가 실행되면 rejected
상태가 된다.
만들어진 프로미스에 .then()
, .catch()
, finally()
핸들러를 붙이면 된다.
let p = new Promise(function (resolve, reject) { ... });
// (1)-1
p
.then(v => {
console.log(v);
})
.catch(err => {
console.log(err);
});
(1)-1: 프로미스가 정상적으로 fulfilled 되었다면 then 핸들러로 넘어갈 것이다. then 핸들러는 2개의 콜백 함수 인자를 가질 수 있다.
(1)-1의 경우 1개의 콜백함수만 들어갔다. 에디터의 자동완성에 보면 onFulfilled 라고 적혀있는 걸 볼 수 있다.
onFulfilled 친구는 resolve되었다면 받아볼 수 있는 함수다.
(v) ⇒ {}
의 형태로 작성하면 된다.
// (1)-2
p
.then(v => {
return v + 1;
})
.then(v => {
console.log(v);
})
.catch(err => {
console.log(err);
});
then 핸들러에서 리턴한다면 이 또한, then 핸들러를 추가로 이어줄 수 있다.
이런 형태로 then과 catch를 덕지덕지 이어놓은 형태를 프로미스 체이닝(Promise chaining) 이라고 부른다.
// (2)
p
.then(
v => {
console.log(v);
},
err => {
console.log(err);
},
);
아까 (1)-1의 설명에서 then 핸들러는 두 개의 콜백함수 인자를 가진다고 했다.
에디터의 자동완성에 의하면 첫번째 콜백함수는 onFulfilled
, 두번째 콜백함수는 onRejected
라는 이름을 가진다.
두번째 콜백함수 onRejected는 프로미스가 거부되었을 때 실행되는 녀석이다.
onFulfilled 에서 에러가 난다면 onRejected에서 처리를 하지 못하고 가까운 에러 핸들러를 찾는다.
아래의 3번예시를 보자.
// (3)
p
.then(v => {
console.log(v)
}).catch(err => {
console.log(err);
}).finally(() => {
console.log("finally!");
})
.then()과 .catch(), .finally()를 섞어서 사용한 국밥 프로미스다.
.then()에서 에러가 나더라도 .catch()에서 잡아줄 수 있다.
또한 .catch()는 .then(null, onRejected)와 같다.
new Promise((resolve, reject) => {
throw new Error("에러 발생!");
}).catch(function(error) { // (*)
if (error instanceof URIError) {
// 에러 처리
} else {
alert("처리할 수 없는 에러");
throw error; // 에러 다시 던지기
}
}).then(function() {
/* 여기는 실행되지 않습니다. */
}).catch(error => { // (**)
alert(`알 수 없는 에러가 발생함: ${error}`);
// 반환값이 없음 => 실행이 계속됨
});
.catch()
같은, 거부상태가 된 promise를 해결하는 코드가 없다면 전역 에러를 생성하게 된다. 브라우저 환경에서 unhandledrejection
이벤트로 확인 가능하다.
Promise.all([...promises])
여러 개의 프로미스를 iterable object(보통은 배열)로 집어넣고, 이 프로미스들이 전부 완료된 다음 특정 코드를 실행시키고 싶을 때 사용한다.
Promise.allSettled([...promises]
인자로 넣은 프로미스들중 하나라도 거절되면 전체를 거절한다.
Promise.race([...promises])
인자로 넣은 프로미스들중 가장 먼저 처리된 프로미스의 결과(혹은 에러)를 반환한다.
Promise.any([...promises])
race와 유사한 동작을 보인다. 차이점은 인자로 넣은 프로미스들중 가장 먼저 ‘fulfilled’ 된 프로미스를 반환한다는 것이다.