게시글

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

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

댓글

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