솔직히, 나는 여지껏 Promise를 잘못 사용하고 있었다. 이를테면 다음과 같이 사용했다.
1 | User |
맙소사. 보면 알겠지만 전에도 언급했던 Callback Hell에 비해서 전혀 구조적으로 나은 부분이 없다. 난 왜 진작 이것에 대해 의문을 품지 않았을까? 대략 저 글이 2015년 8월에 작성한 글이니 약 반 년간 Promise를 전혀 이해하지 못한 것이다.
Promise ?
Promise는 이런 Callback Hell을 해결할 수 있는 기법이다. Promise 명세를 따라 정의한 Promise 객체는 모두 then()
메소드를 가지고 있어서 Callback을 사용하지 않고도 절차적으로 동작하는 코드를 짤 수 있다.
여기서 Promise의 명세를 파악할 수 있다.
이 스펙은 언어자체의 스펙이 아니라서(ES2015부터는 언어에 포함) 별도의 라이브러리를 따로 포함해야만 Promise라는 개념을 이용할 수 있다.
Promise를 구현한 라이브러리는 대표적으로 Q.js와 bluebird 등이 꼽히는데, 이 글에서는 Q.js만 사용할 것이다. 왜냐하면 AngularJS에도 Q.js로부터 영향을 받은 $q라는 구현체가 존재하기 때문이다.
Promise 객체 생성
다음과 같이 1초 후에 랜덤으로 0에서 1사이의 숫자를 콜백으로 넘기는 함수가 있다고 하자.
1 | /* 여기부터 사용된 모든 코드는 ES2015의 스펙을 일부 사용합니다. |
이걸 실제로 사용하려면 이렇게 해야한다.
1 | getRandomNumber(function (result) { |
고작 깊이가 2임에도 불구하고 코드가 이렇게 더러워진다. 게다가 에러 블럭은 두 개가 생기고 똑같은 코드가 중복되는 것을 확인할 수 있다. 누가봐도 절대 좋은 코드라고 말할 수가 없는 상황..
이 문제를 해결하기 위해서 getRandomNumber()
함수를 Promise를 반환하도록 다음과 같이 다시 짤 수 있다.
1 | const Q = require('q'); |
Q.defer()
는 “deferred” 객체를 생성하는 함수이다. “deferred” 객체는 다음과 같은 프로퍼티를 포함한다.
deferred.promise
deferred.resolve()
deferred.reject()
그 외에도 deferred.notify()
, deferred.makeNodeResolver()
같은 녀석들을 포함하고 있지만 기본적인 Promise 함수 구현에는 위의 세 가지만 알고 있으면 충분하다.
deferred.promise
는 명세에 맞게 구현된 Promise 객체이다. Promise를 제대로 사용하려면 마지막에 이 녀석을 리턴해서 함수 외부에서 Promise 객체를 사용할 수 있도록 해야한다. 위의 코드에서도 함수의 마지막에 리턴했다.
deferred.resolve()
는 함수의 동작이 정상적으로 완료되면 호출하는 메소드이다. 이 메소드를 호출하면 promise 객체의 상태가 “fulfilled”로 바뀌고 promise.then()
안의 함수 블록을 실행시킨다. 결과 값을 파라미터로 넘겨줄 수도 있다. 위의 코드에서는 결과 값인 랜덤 값을 넘겨줬다.
주의: 넘겨주는 값이 “rejected” 상태의 promise 객체인 경우 이 deferred 객체의 promise 객체 역시 “rejected” 상태로 변하기 때문에 deferred.resolve()
를 호출해도 promise.catch()
블록을 실행하게 된다.
deferred.reject()
는 함수가 정상적으로 동작하지 않으면 호출하는 메소드이다. 일반적으로 에러가 났을 때 호출하게 되는 메소드이다. 역시 파라미터로 데이터를 넘길 수 있는데, 보통은 에러 메시지를 넘기게 된다. 위에서도 에러 메시지를 객체로 넘겼다.
Promise 객체 사용
이런 식으로 Promise를 이용해서 함수를 구현하면 더러웠던 코드를 다음과 같이 개선할 수 있다.
1 | getRandomNumber() |
코드가 훨씬 깔끔해지고 인덴트도 덜 들어가는 모습을 볼 수 있다. 특히 promise.catch()
를 통해 에러 핸들링을 더욱 깔끔하게 할 수 있게 되었다.
그리고 내가 도입부에 쓴 코드에서 가장 잘못된 부분인데, Promise는 기본적으로 chain이 가능하다. promise.then()
안의 함수 블록에서 다시 promise 객체만 리턴하면 된다. 이걸 몰라서 여태껏 Promise를 Callback처럼 사용하고 있었다.. 그게 이 글을 쓰게 된 계기이기도 하지만.
만약 에러 여부에 관계 없이 마지막에 항상 실행시키고 싶은 부분이 있다면 모든 Promise 체인이 끝난 뒤에 실행되는 promise.finally()
를 사용할 수 있다.
마지막으로..
최신 기술은 Promise를 넘어 async/await 패턴을 지원하는 것들도 많다.(그리고 사실은 어떤 때에는 이게 더 편하다.) 대표적으로 ES2016이 그러하고 IcedCoffeeScript같은 것도 있다. 하지만 두 패턴 모두 장단이 있으므로 적당히 사용해보고 취사 선택하면 좋은 코드를 짤 수 있을 것이다.