상점에 가서 물건을 주문하는데 그 물건이 나올 때까지 시간이 걸린다고 가정해보자. 언제 완료되는지 알 수 없고, 물건을 만들다가 실패하는 경우도 있다. 이럴 때 소비자는 10초에 한 번씩 "다 됐나요?"라고 물어본다. 판매자는 "아니요" 또는 "다 됐습니다"라고 대답한다.
실패했다고 말하는 경우도 있다. 그러면 물건을 다시 주문해야한다. 이럴 때 상점에 주문을 하고 상품이 준비되었거나 실패되었으면 알려달라고 하면서 전화번호를 주고 나오는 게 낫다. 상품이 준비되는 동안 다른 작업도 할 수 있다. 상점은 이 번호를 기억했다가 작업이 완료되거나 실패하면 소비자에게 알려준다.
이와 같은 상황에서 사용할 수 있는 것이 Promise이다.
프로미스는 다음과 같이 사용한다.
const pr = new Promise((resolve, reject) => {
// code
});
인수 resolve는 성공한 경우, reject는 실패한 경우 실행되는 함수이다.
이와 같이 어떤 일이 완료된 이후 실행되는 함수를 콜백(callback) 함수라고 한다.
new Promise 생성자가 반환하는 프로미스 객체는 state와 result를 프로퍼티로 갖는다.
state는 초기에 pending이었다가
resolve가 호출되면, 즉 성공하면 fulfilled가 된다. 이 때 result는 resolve 함수로 전달된 값이다.
reject가 호출되면, 즉 실패하면 rejected가 된다. 이 때 result는 reject함수로 전달된 error이다.
const pr = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('OK')
}, 3000)
});
이 코드는 3초 뒤에 state가 fullfilled로 바뀌고 result는 'OK'가 된다.
const pr = new Promise((resolve, reject) => {
setTimeout(()=>{
reject(new Error('error..'))
}, 3000)
});
3초 뒤에 state가 rejected로 바뀌고 result는 error가 된다.
지금까지 판매자의 코드를 살펴보았다. 판매자는 주문을 받으면 3초동안 무언가를 하고, 성공인지 실패인지 알려준다.
이제 소비자의 코드를 살펴보자.
then을 이용해서 resolve와 reject를 처리할 수 있다.
const pr = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('OK')
}, 3000)
});
pr.then(
function(result){},
function(err){}
);
첫 번째 인수는 프로미스가 이행되었을 때 실행하는 함수이다. result에 'OK'가 들어온다.
두 번째 인수는 프로미스가 거부되었을 때 실행하는 함수이다. err에는 에러값이 들어온다.
then 대신 catch나 finally를 사용할 수도 있다.
catch는 reject된 경우에만 실행된다. 동일하게 동작하지만 catch로 구분해주면 가독성이 좋고 첫 번째 함수를 실행했다가 발생하는 에러도 잡아줄 수 있기 때문에 catch문을 사용하는 것이 좋다.
const pr = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('OK')
}, 3000)
});
pr.then(
function(result){}
).catch(
function(err){}
);
finally는 이행인지 거부인지와 상관없이 처리가 완료되면 항상 실행된다. 로딩 같은 것을 없앨 때 유용하다.
const pr = new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('OK')
}, 3000)
});
pr.then(
function(result){}
).catch(
function(err){}
).finally(
function(){
console.log('---주문 끝---')
}
);
3개의 상품을 주문하는 예시이다. 먼저 Promise를 사용하지 않은 코드이다.
const f1 = (callback) => {
setTimeout(function(){
console.log("1번 주문 완료");
callback();
}, 1000)
};
const f2 = (callback) => {
setTimeout(function(){
console.log("2번 주문 완료");
callback();
}, 3000)
};
const f3 = (callback) => {
setTimeout(function(){
console.log("3번 주문 완료");
callback();
}, 2000)
};
console.log('시작');
f1(function(){
f2(function(){
f3(function(){
console.log('끝');
});
});
});
이처럼 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드 들여쓰기가 감당하기 힘들 정도로 깊어지는 현상을 콜백 지옥(callback hell)이라고 한다.
Promise를 이용하여 바꾼 코드는 다음과 같다.
const f1 = () => {
return new Promise((res, rej) => {
setTimeout(() => {
res("1번 주문 완료");
}, 1000);
});
};
const f2 = (message) => {
console.log(message);
return new Promise((res, rej) => {
setTimeout(() => {
res("2번 주문 완료");
}, 3000);
});
};
const f3 = (message) => {
console.log(message);
return new Promise((res, rej) => {
setTimeout(() => {
res("3번 주문 완료");
}, 2000);
});
};
console.log("시작");
f1()
.then((res) => f2(res))
.then((res) => f3(res))
.then((res) => console.log(res))
.catch(console.log)
.finally(() => {
console.log("끝");
});
이렇게 프로미스가 연속 호출되는 것을 프로미스 체이닝(Promise Chaining)이라고 한다.
각 주문의 시간이 1초, 3초, 2초이기 때문에 순차적으로 주문하면 총 6초가 걸린다.
세 사람이 상점에서 동시에 주문한다면 3초 뒤에 모든 물건을 받을 수 있다. 이 때 쓸 수 있는 것이 Promise.all이다.
Promise.all([f1(), f2(), f3()]).then((res) => {
console.log(res);
});
Promise.race는 사용 방법이 Promise.all과 동일하다.
Promise.all([f1(), f2(), f3()]).then((res) => {
console.log(res);
});
차이점은 all은 모든 작업이 완료될 때까지 기다리지만 race는 하나라도 완료되면 끝낸다. 용량이 큰 이미지들을 로딩하는데 그 중에 하나라도 완료되면 그 이미지를 보여줄 때 이런 방식을 사용하면 된다.
'Language > Javascript' 카테고리의 다른 글
[Javascript] 호출 스택과 이벤트 루프 (0) | 2022.04.28 |
---|---|
[Javascript] async, await (0) | 2021.12.28 |
[Javascript] 클래스(Class) (0) | 2021.12.26 |
[Javascript] 상속, 프로토타입(Prototype) (0) | 2021.12.25 |
[Javascript] call, apply, bind (0) | 2021.12.25 |