이 블로그는 광고 클릭 수익으로 운영됩니다!
괜찮으시다면 광고 차단을 풀어주세요 ㅠㅠ

게시글

강좌10 - EcmaScript - 2년 전 등록 / 2달 전 수정

ES2015(ES6) Promise

조회수:
0
이 블로그는 광고 클릭 수익으로 운영됩니다!
괜찮으시다면 광고 차단을 풀어주세요 ㅠㅠ
이 블로그는 광고 클릭 수익으로 운영됩니다!
괜찮으시다면 광고 차단을 풀어주세요 ㅠㅠ

 안녕하세요. 이번 시간에는 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);
});

thencatch로 결과를 받습니다. 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 객체를 반환하기에 가능한 겁니다. 맨 처음의 코드와 비교해서 훨씬 깔끔해지지 않았나요? 불필요한 들여쓰기도 없고요. 만약 이 코드조차도 지저분하다고 여기신다면 asnyc/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로 교체해보세요! 다음 시간에는 반복기, 생성기에 대해서 알아보겠습니다!

투표로 게시글에 관해 피드백을 해주시면 많은 도움이 됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright © 2016- 무단 전재 및 재배포 금지

댓글

3개의 댓글이 있습니다.
2달 전
then() 전에 exec() 을 걸어 주는 이유가 뭔지 알수 있을까요...? mongoose docs 에는 fully-fledged promise 라는 표현이 있는데 관련 한글 자료를 못찾아서 질문 드립니다!
2달 전
옛 버전 몽구수에서는 걸어야했는데 이제는 아닙니다!
3달 전
만약 이 코드조차도 지저분하다고 여기신다면 asnyc/await을 참조하세요. <-- 이부분이요
클릭하면 callback hell 로 가는데 맞는건가요??
https://www.zerocho.com/category/EcmaScript/post/58d142d8e6cda10018195f5a
여기로 가야할거 같아서 글 남깁니다~
3달 전
오잉 감사합니다!
6달 전
감사합니다 ㅎㅎ 제가 몰라서 체인끊을줄 몰랐었던 부분 업데이트해주셨네요 ^^