안녕하세요. 이번 시간에는 Promise에 대해서 알아보겠습니다.
Callback Hell
그동안 자바스크립트나 Node.js에서 callback 패턴은 잘 쓰고 계셨나요? 해당 동작이 완료될 때까지 직접 기다릴 필요 없이, 그냥 하라고만 말해두고 나중에 완료되었을 때 보고받으면 되기 때문에 정말 편했죠. 그런데 혹시 문제점을 느끼지는 못하셨나요? 못했다면 다음 코드를 보시죠!
Users.findOne({}, (err, user) => {
if (err) {
return console.error(err);
}
user.name = 'zero';
user.save((err) => {
if (err) {
return console.error(err); // 점점 더 앞의 공백이 깊어짐
}
Users.findOne({ gender: 'm' }, (err, user) => {
...
});
});
});
네.. callback이 중첩될 때마다 점점 코드가 안으로 들어갑니다. 코드 마지막을 보면 });가 계속 반복되고 있죠. callback이 중첩될수록 점점 더 많이 반복될 거에요. 이렇게 자꾸 쓸데없는 들여쓰기가 일어나 가독성을 해치는 현상을 callback hell이라고 부릅니다. 콜백 지옥이죠.
개발자는 저기 닫는 태그가 어떤 여는 태그와 한 쌍인지 헷갈려서 지옥을 경험할 수 있습니다. 또한 콜백 하나마다 모든 에러를 처리해야해서 번거롭습니다. 나중 가면 err 변수가 어떤 동작의 err인지도 헷갈립니다. 이를 해결할 수 있는 패턴이 바로 Promise 패턴입니다.
Promise
사실 Promise는 ES2015 이전에도 쓸 수 있었습니다. bluebird나 jquery같은 라이브러리에서도 일부 구현하여 지원했었죠. 하지만 이제는 ES2015에서 공식 지원한다는 소리입니다!
Promise를 만들어봅시다. Promise는 객체입니다.
const promise = new Promise((resolve, reject) => {
try {
...비동기 작업
resolve(결과);
} catch (err) {
reject(err);
}
});
위와 같이 만들 수 있습니다. resolve는 성공했을 때 결과를 전달하는 거고, reject는 실패했을 때 에러를 전달하는 겁니다.
위와 같이 만든 Promise 객체는
promise.then((result) => {
// result 처리
}).catch((err) => {
console.error(err);
});
then과 catch로 결과를 받습니다. resolve(결과)의 결과가 then의 result로 가고, reject(err)의 err이 catch의 err로 갑니다. 어떻게 resolve와 reject의 값이 then과 catch로 넘어가냐고요? 그건 내부적으로 처리되기 때문에 궁금해하지 않으셔도 됩니다. 정 궁금하시면 링크.
Promise가 좋은 점은 코드를 분리할 수 있다는 겁니다. 아까 위에 const promise에 대입한 객체를 가만히 놔뒀다가 필요한 곳에 쓰면 됩니다.
const promise1 = new Promise((resolve, reject) => { ... });
const promise2 = new Promise((resolve, reject) => { ... });
if (조건문) {
promise1.then((result) => {...});
} else {
promise2.then((result) => {...});
}
위와 같이 조건문별로 상황에 따라 Promise를 따로 처리할 수도 있습니다. Promise를 변수에 대입할 수 있기 때문에 활용도도 높습니다.
let promise;
if (조건문) {
promise = new Promise((resolve, reject) => {});
} else {
promise = new Promise((resolve, reject) => {}); // 다른 작업 Promise
}
promise.then(() => {});
다른 좋은 점은 then을 여러 번 연속해서 쓸 수 있다는 겁니다. then과 catch는 Promise 객체의 메소드입니다. 만약 then이 또 다른 값을 반환한다면 then을 다음에 한 번 더 연결해서 처리할 수 있죠. 게다가 반환하는 값이 다른 Promise라면 그 Promise가 resolve된 후에 다음 then이 실행됩니다. 완전 최고입니다.
처음에 보여드렸던 callback hell에 빠진 몽구스 코드가 어떻게 바뀌는지 보여드리겠습니다. 다행히 Mongoose는 Promise를 지원합니다.
Users.findOne({}).then((user) => {
user.name = 'zero';
return user.save();
}).then((user) => {
return Users.findOne({ gender: 'm' });
}).then((user) => {
...
}).catch(err => {
console.error(err);
});
위의 코드를 보면 then을 반복적으로 사용하여 결과를 연속적으로 처리하고 있습니다. catch는 마지막에 한 번만 등장해 위의 에러들을 한 번에 처리하고요.
참고로 위의 코드는 Mongoose의 findOne()
과 save()
메소드가 Promise 객체를 반환하기에 가능한 겁니다. 맨 처음의 코드와 비교해서 훨씬 깔끔해지지 않았나요? 불필요한 들여쓰기도 없고요. 만약 이 코드조차도 지저분하다고 여기신다면 async/await 을 참조하세요.
위의 방식의 문제점은 마지막 catch에서 에러를 한 번에 처리하기 때문에 어떤 Promise에 에러가 났는지 확인하기 힘듭니다. 그렇다면 어떻게 해야 할까요? Promise.then().catch().then().catch()
이렇게 하면 되지 않을까요? 하지만 실제로 해보시면 그렇게 잘 작동하지 않습니다. 에러가 나면, 에러가 난 위치보다 뒤에 붙여놓은 모든 catch들에서도 에러가 발생합니다.
가장 좋은 해결 방법은 return하는 Promise 객체에 catch를 붙이는 겁니다.
Users.findOne({}).then((user) => {
user.name = 'zero';
return user.save().catch((err) => console.log('save에서 에러'));
}).then((user) => {
return Users.findOne({ gender: 'm' }).catch((err) => console.log('gender로 찾기에서 에러'));
}).then((user) => {
// ...
}).catch(err => {
console.error('최종 에러', err);
});
catch를 붙였다면 이제 Promise 체이닝을 끊을 수 있습니다. throw로 에러를 발생시키면 뒤의 then들이 호출되지 않고 바로 catch로 넘어갑니다.
new Promise((resolve, reject) => {
resolve(true);
}).then((result) => {
if (result) {
throw new Error('도중에 중단!');
}
}).then(() => {
console.log('실행되지 않아요');
}).catch((err) => {
console.error(err);
});
참고로 Promise.all 메소드로 여러 프로미스 객체들을 한번에 모아서 처리할 수 있습니다. 이 메소드는 모든 프로미스가 성공하면 then, 하나라도 실패하면 catch로 연결됩니다.
let p1 = Promise.resolve('zero'); // new Promise 없이 성공한 Promise 객체를 만드는 방법
let p2 = Promise.resolve('nero');
let p3 = Promise.reject('error'); // new Promise 없이 실패한 Promise 객체를 만드는 방법
Promise.all([p1, p2, p3]).then((result) => {
console.log(result); // 만약 p3가 resolve였다면 ['zero', 'nero', 'error']
}).catch((err) => {
console.error(err); // error (p3가 reject이기 때문)
});
Promise.race라는 친구도 있는데, 여러 프로미스 중 가장 빨리 성공하거나 실패한 애를 보여줍니다.
let p4 = new Promise((resolve, reject) => {
setTimeout(resolve, 500);
});
let p5 = new Promise((resolve, reject) => {
setTimeout(resolve, 500);
});
let p6 = new Promise((resolve, reject) => {
setTimeout(reject, 500);
});
Promise.race([p4, p5, p6]).then((result) => {
console.log(result); // p6보다 빠르고, p4나 p5 중 더 빠르게 성공한 것
}, (err) => {
console.error(err); // p6가 p4나 p5보다 더 빠르면 실패한 걸로
});
이제 여러분의 코드를 보시고, Callback hell에 빠져 있는 부분이 있다면 적극적으로 Promise로 교체해보세요! 다음 시간에는 반복기, 생성기에 대해서 알아보겠습니다!
setTimeout(resolve(5000), 200);
})
.then((result) => { ***
//console.log(result);
return new Promise(function (resolve, reject) {
setTimeout(() => {
resolve(result);
}, 10000);
});
})
.then((result) => console.log(result))
.toString();
제가 프로미스를 공부중인데요.
이 코드를 보았을때
*** 지점에 then메소드에서 return한 Promise객체가 10초뒤에 resolve메소드가 호출되고 프로미스는 fulfilled상태가 되어
다음 then메소드가 실행되는 형태인것같은데 이 then메소드 다음 메소드가 toString인데요.
최소 10초가 지나야 호출되어야 할 toString함수가 어떻게 호출이 됬는지
***에서 resolve가 호출되기도 전에 a를 접근해보면 "[object Promise]" 이런식으로 뜨게 됩니다.
then이 어떻게 동작(이전 then 메소드에서 반환하는 프로미스 객체가 fulfilled일때 콜백함수가 호출되는 이벤트를 등록하는 형태라고도 생각이듭니다.. )하는지 가늠이 안되서 질문드립니다 그리고 toString은 어떻게 이렇게 빨리 실행됬는지도 잘모르겠습니다
만약 then함수를 3번 실행하면 마이크로태스크 큐에 마이크로태스크가 3개 등록되는게 맞죠? 그리고 전역 실행 컨텍스트가 있을때의 promise객체의 executor는 동기적으로 실행되네용
제로초님 정말 감사합니다!
3개가 다 Task Queue에 들어가 있지 않으면 then() 안의 코드들은 어디에 저장되어 있는지 문의 드립니다.