본문으로 바로가기

프로미스

category Node JS/ES2015+ 2022. 4. 24. 23:01

자바스크립트와 노드에서는 주로 비동기를 접하는데 특히 이벤트 리스너를 사용할때 콜백 함수를 자주 사용한다. ES2015부터는 자바 스크립트와 노드의 API들이 콜백 대신 프로미스 기반으로 재구성되며 악명 높은 콜백 지옥 현상을 극복했다는 평가를 받고 있다.

 

추가로 프로미스는 주로 서버에서 받아온 데이터를 화면에 표시할 때 사용합니다.

 

일반적으로 웹 애플리케이션을 구현할 때 서버에서 데이터를 요청하고 받아오기 위해 꼭 알아야하는 개념이다.

프로미스는 다음과 같은 규칙이 있는데 먼저 객체를 생성해야한다.

const condition = true; // true면 resolve, false면 reject
const promise = new Promise((resolve, reject) => {
  if (condition) {
    resolve('성공');
  } else {
    reject('실패');
  }
});
// 다른 코드가 들어갈 수 있음
promise
  .then((message) => {
    console.log(message); // 성공(resolve)한 경우 실행
  })
  .catch((error) => {
    console.error(error); // 실패(reject)한 경우 실행
  })
  .finally(() => { // 끝나고 무조건 실행
    console.log('무조건');
  });

코드를 설명하자면 new Promise로 프로미스를 생성할수 있으며 그내부에 resolve와 reject를 매개변수로 갖는 콜백 함수를 넣는다.

 

이렇게 만든 promise 변수에 then과 catch 메서드를 붙일수있다.

 

프로미스 내부에서 resolve가 호출 되면 then이 실행되고, reject가 호출되면 catch가 실행된다. finally 부분은 성공/실패 여부와 상관없이 실행된다.

 

resolve와 reject에 넣어준 인수는 각각 then과 catch의 매개변수에서 받을 수 있습니다.

즉 resolve(’성공’)이 호출 되면 then의 message가 ‘성공’이 되고, 만약 reject(’실패’)가 호출되면 catch의 error가 ‘실패’가 되는거다.

 

위함수에서 condition 변수를 false로 바꾸면 catch에서 에러가 로깅된다.

 

 

 

프로미스를 쉽게 설명하면, 실행은 바로 하되 결괏값은 나중에 받는 객체이다. 결괏값은 실행이 완료된후 then이나 catch 매서드를 통해 받는다.

 

위 예제에서는 new Promise와 promise. then 사이에 다른 코드가 들어갈수도 있다 new Promise는 바로 실행되지만, 결괏값은 then을 붙였을때 받게 된다.

 

 

 


 

then이나 catch에서 다른 then이나 catch를 붙일수있다.

 

이전 then의 return 값을 다음 then의 매개변수로 넘깁니다. 프로미스를 return한 경우에는 프로미스가 수행된후 다음 then이나 catch가 호출된다.

promise
  .then((message) => {
    return new Promise((resolve, reject) => {
      resolve(message);
    });
  })
  .then((message2) => {
    console.log(message2);
    return new Promise((resolve, reject) => {
      resolve(message2);
    });
  })
  .then((message3) => {
    console.log(message3);
  })

  .catch((error) => {
    console.error(error);
  });

처음 then에서 message를 resolve하면 다음 then 에서 message2로 받을수 있다.

여기서 다시 message2를 resolve 한 것을 다음 then에서 message3로 받았다. 단, then에서 new Promise를 return해야 다음 then에서 받을수 있다.

 


 

이것을 활용해서 콜백을 프로미스로 바꿀수있다 다음은 콜백을 쓰는 패턴중 하나이다.

function findAndSaveUser(Users) {
  Users.findOne({}, (err, user) => { // 첫 번째 콜백
    if (err) {
      return console.error(err);  //console.error는 에러를 표시할때 log대신 쓰기도함
    }
    user.name = 'zero';
    user.save((err) => { // 두 번째 콜백
      if (err) {
        return console.error(err); 
      }
      Users.findOne({ gender: 'm' }, (err, user) => { // 세 번째 콜백
        // 생략
      });
    });
  });
}

콜백마다 함수가 세번 중첩되어있다. 콜백 함수가 나올때 마다 코드의 깊이가 깇어진다.

각 콜백 함수마다 에러도 따로 처리해줘야한다.

 

이코드를 다음과 같이 바꾼다.

function findAndSaveUser(Users) {
  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에서 한번에 처리할수있다.

 

하지만 모든 콜백함수를 위와같이 바꿀수있는것은 아니다 메서드가 프로미스 방식을 지원 해야한다.

 

예제의 코드는 findOne과 save 메서드가 내부적으로 프로미스 객체를 가지고 있다고 가정했기에 가능하다 (new Promise 가 함수 내부에 구현되어있어야한다).

 

지원 하지 않는 경우 콜백 함수를 프로미스로 바꿀수있는 방법은 나중에 알아보겠다.

 


 

프로미스 여러개를 한번에 실행할수 있는 방법이있는데 기존의 콜백 피턴이었다면 콜백을 여러번 중첩해서 사용해야 했을거다 하지만 Promise.all을 활용하면 간단히 할수있다.

const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('성공2');
Promise.all([promise1, promise2])
  .then((result) => {
    console.log(result); // ['성공1', '성공2'];
  })
  .catch((error) => {
    console.error(error);
  });

Promise.resolve는 즉시 resolve하는 프로미스를 만드는 방법이다.

 

비슷한것으로 즉시 reject하는 Promise.reject도 있다 프로미스가 여러 개 있을때 Promise.all에 넣으면 모두 resolve될때까지 기다렸다가 then으로 넘어간다. result 매개 변수에 각각의 프로미스 결괏값이 배열로 들어 있따.

Promise 중 하나라도 reject가 되면 catch로 넘어간다.

 

 

 


 

 

콜백 프로미스 이해를 돕기 위한 자료

 

Promise를 사용하는 방법

먼저 콜백 지옥을 벗어나는 방법을 알아보기 전에 Promise의 사용 방식을 알아보려고 합니다. Promise의 사용 방식은 생각보다 중요한데요. 그 이유는 바로 Promise는 비동기에 Callback을 전달하지 않고 첨부하기 때문입니다. 말로는 이해하기가 어려울 수 있으니 코드를 통해 Promise의 사용 방식을 설명해보도록 하겠습니다.

function successCallback(result) {
  console.log("구독 성공: " + result);
}

function failureCallback(error) {
  console.log("구독 실패: " + error);
}

subscriptionYoutubeChannelAsync(subscription, successCallback, failureCallback);

일반적으로 사용하는 비동기 처리 방식의 예제 코드입니다. 코드의 내용은 유튜브의 구독 기능을 예시로 성공, 실패 처리를 정의하고 해당 식을 실행되는 함수에 인자로 전달해 실행 시점을 실행되는 함수에게 위임했습니다. 즉 콜백 함수(Callback function)를 정의하고, 비동기로 실행되는 함수가 콜백 함수를 실행하는 형태가 되었습니다. 이 코드를 Promise를 통해 정의한다면 아래와 같습니다.

subscriptionYoutubeChannelAsync(subscription).then(successCallback, failureCallback);

Promise를 사용하게 되면서 중간에 then()를 추가하게 되었습니다. 사실 그 부분 말고는 차이를 알 수 없지만 앞서 이야기한 것처럼 실행과 동시에 함수를 전달하지 않고 then()을 이용해서 콜백 함수(Callback function)를 첨부하는 방식으로 바뀌었다는 것을 알 수 있습니다. 사소한 차이라고 생각할 수 있지만 첨부하는 형태로 비동기를 처리할 수 있다는 점은 코드를 간결하고 읽기 쉽게 만들어 줄 뿐만 아니라 Chaning 형태로 비동기를 처리할 수 있도록 해줍니다. 콜백 지옥(Callback hell)을 벗어날 수 있는 것도 바로 이러한 방식을 채용했기 때문입니다.

 

콜백 지옥(Callback hell)을 벗어 날 수 있는 Promise

 

콜백 지옥은 어떤 기능의 실행 결과를 받아 또 다른 어떤 기능을 실행해야 하는 상황에 만들어지는 코드입니다. 

[웹 개념/javascript] - 콜백함수

 

주로 비동기적으로 처리해야하는 일이 2개 이상인 경우에 만들어집니다.

doSomething1(function(result1) {
  doSomething2(result1, function(result2) {
    doSomething3(result2, function(finalResult) {
      console.log('최종 실행 결과: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

예제 코드에서는 3가지의 일들을 이전의 결과를 받아 순서대로 처리해야 하는 상황을 표현했습니다. 지금은 복잡하지 않다고 생각할 수 있지만 4개 아니면 그 이상의 일들을 순차적으로 처리해야 하는 상황이 온다면 보기도 어렵고, 이해하기 힘든 코드가 만들어집니다. 이럴 때 바로Promise가 가진 매력을 엿볼 수 있습니다.

doSomething1().then(function(reulst1) {
  return doSomething2(result1);
})
.then(function(result2) {
  return doSomething3(result2);
})
.then(function(finalResult) {
  console.log('최종 실행 결과: ' + finalResult);
})
.catch(failureCallback);

Promise 객체를 사용한다면 then()을 이용해 기존의 중첩되는 구조를 피할 수 있습니다. 몇 가지의 일들이 추가된다고 해도 Promise 객체를 사용한다면 코드 줄 수가 추가될 뿐 중첩되지 않기 때문에 보다 쉽게 이해할 수 있는 코드를 만들 수 있습니다.

 

예외를 처리하는 방법에 있어서도 Promise는 좋은 코드를 만들 수 있게 해 줍니다.

일반적인 비동기 처리 방식에서는 예외 상황을 위해 failureCallback를 3번 사용하고 있지만 Promise를 이용한 처리 방식은 failureCallback를 단 한 번만 사용하고 있습니다. 

 

예외를 처리하는 방법은 실행하는 기능의 개수만큼 이 필요할 수 도 있다는 관점에서라면 안 좋아 보일 수 있지만 그 부분은 아래와 같이 catch()를 중간에 적절하게 섞어서 사용하면 문제가 없습니다.

new Promise((resolve, reject) => {
    console.log('시작');
    resolve();
})
.then(() => {
    throw new Error('문제 발생');
    console.log('실행1');
})
.catch(() => {
    console.log('문제 해결');
})
.then(() => {
    console.log('실행2');
});

 

 

끝맺음

모던한 자바스크립트 코드를 작성할 때 비동기를 잘 처리할 수 있는 방법은 Promise입니다.

그렇기 때문에 Promise는 비동기 처리가 필수적인 Fetch API나 Axios에서도 활용되는 개념입니다. 처음 Promise를 접하게 되면 어려울 수 있지만, 이게 왜 필요한가 싶다가도 콜백 지옥을 만나거나 비동기적인 일들을 순차적으로 처리해야 하는 상황을 부딪히게 되면 Promise만큼 멋진 방법이 있다는 게 참 다행이라는 생각이 듭니다.

 

 

출처:

https://7942yongdae.tistory.com/138

 

'Node JS > ES2015+' 카테고리의 다른 글

async/await  (0) 2022.04.24
클래스  (0) 2022.04.24
ES2015+ 란??  (0) 2022.04.24
구조분해 할당  (0) 2021.11.27
화살표 함수  (0) 2021.11.27