[ECMAScript 6] Promise - 비동기 코드 작성하기

모든 언어가 마찬가지겠지만, 기존의 JavaScript에서는 비동기적 코드를 작성하고 관리하는 것은 크게 어려운 일이었다. node.js에서는 콜백을 이용하는 방식을 사용했지만, 이는 콜백 헬이라는 새로운 문제를 만들어냈다. 이를 해결하기 위해 step이나 async 같은 다양한 라이브러리가 나왔지만 이런 라이브러리로도 콜백 방식이 가지는 복잡도는 해결하지 못했고, 여전히 비동기 코드를 작성하는 것은 어려운 문제였다. 그래서 ECMAScript 6에서는 비동기 코드를 쉽게 작성할 수 있도록 Promise를 표준 라이브러리에 도입하였다. Promise는 그 이름에서도 알 수 있듯이 비동기적인 코드를 작성할 수 있도록 도와주는 promise monad의 일종이다.

Promise는 기본적으로 생성자를 통해서 만들어진다.

이렇게 생성된 Promise는 pending state가 된다. pending state는 아무 값도 가지지 않은 상태다. pending인 Promise는 후에 resolved state(혹은 fulfill state)가 되거나 rejected state가 될 수 있지만, 이 상태로는 아무것도 할 수 없다.

Promise의 상태를 바꾸기 위해서는 콜백 함수를 이용해야 한다. Promise의 생성자는 한 개의 콜백 함수를 받는다. 이 콜백은 executor라고 불리는데, Promise 객체를 생성하는 중에 호출된다. executor가 호출될 때는 2개의 함수가 인자로 넘어간다. 첫 번째는 resolver라고 불리고, 두 번째는 rejecter라고 불린다. pending state인 Promiseresolver가 호출되면 이 Promise는 resolved state가 되고, resolver의 인자를 값으로 지닌다. 반대로 pending state인 Promiserejecter이 호출되었다면 이 Promise는 rejected state가 되고, rejecter의 인자를 Promise가 reject 된 이유로 가지게 된다. 중요한 점은 pending state인 Promise만 resolve 하거나 reject 할 수 있고, 이미 resolved 되었거나 rejected 된 Promise는 다시 resolve 하거나 reject할 수 없다.

혹은 함수를 실행시키지 않고, 이미 결정된 값을 가지고 콜백 없이 Promise를 만드는 방법도 있다. resolvereject 함수를 이용하는 것이다. resolve 함수를 이용하면 resolved 된 Promise가 만들어지고, reject 함수를 이용하면 reject 된 Promise가 만들어진다.

이렇게 생성된 Promise는 2개의 멤버 함수를 가진다. 우선 기본적인 멤버 함수는 then()이라는 함수다. then()은 promise monad의 map과 map_error operation을 합쳐놓은 함수이다. then()은 2개의 콜백을 받는다.

첫 번째 받은 콜백은 Promise가 resolved일 때 호출되는 함수이고, 두 번째 콜백은 Promise가 rejected일 때 호출되는 함수이다. 정확히는 상태가 변하면 바로 호출되는 것은 아니다. 만약 then() 함수가 호출될 때 Promise가 pending 되어 있었다면 상태가 변할 때 job queue에 추가되고, 상태가 resolved나 rejected였다면 그 즉시 job queue에 추가되었다가, job queue에서 다음 일을 꺼내질 때 실행된다. 이때 콜백 함수는 값을 하나 인자로 받는데, 첫 번째 콜백의 경우 Promise가 resolve 되었을 때의 값을, 인자로 받고, 두 번째 콜백은 Promise가 reject 된 이유를 받는다.

then method는 Promise를 리턴하기 때문에 체이닝 할 수도 있다. 이때 반환되는 Promise가 가지는 값은 실행되는 콜백의 결과에 따라 달라진다. 만약 콜백 함수가 정상적으로 실행되고 값을 반환했다면, 리턴되는 Promise는 함수의 결괏값을 resolve 한 값이 리턴되는 Promise의 값이 된다. 반대로 만약 콜백이 에러를 throw 했으면 throw 한 값을 reason으로 하는 rejected state인 Promise가 리턴된다.

하지만 then() 함수만 있으면 rejected 된 Promise에 대해서만 값을 진행시키고 싶을 때 약간 불편하다. 그래서 catch 함수가 존재한다. Promise.prototype.catch(onRejected) method는 Promise.prototype.then(undefined, onRejected)와 완전히 같은 동작을 한다. 따라서 아래의 코드의 promise1promise2는 같은 의미의 Promise가 된다.

ECMAScript 6의 Promise는 여러 개의 Promise를 join 할 수 있는 Promise.all이라는 함수도 제공한다. all() 함수는 인자로 iterable 한 ArraySet 같은 것을 받는다. 그리고 받은 iterable의 모든 값이 resolved 되면, 그 값들이 resolve 된 값의 배열을 리턴한다. 이를 통해서 여러 개의 독립적으로 돌아가는 비동기적인 코드의 실행 결과를 하나로 모을 수 있다.

혹은 Promise.race() 함수를 이용해서 여러 Promise 중에서 먼저 완료된 Promise를 얻어올 수도 있다.

사실 기존에도 JavaScript를 위해 Promise를 제공하는 라이브러리는 많이 있었다. node.js가 나오기 전에 유행했던 jQuerydeferred object도 사실상 Promise monad의 일종이었고, node.js나 브라우저 양쪽에서 돌아가도록 pure JavaScript로 작성된 Qbluebird 같은 라이브러리도 존재했다. 하지만 이는 다들 독립적인 동기로 작성된 것으로 서로 간에 API가 호환되지 않아 사용법이 전부 달랐다. 하지만 이제 표준 라이브러리에 Promise가 들어왔으므로 그냥 표준 라이브러리에서 제공하는 code class="language-javascript"Promise를 사용하면 된다.

댓글

댓글 쓰기

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

RAII는 무엇인가

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API