본문 바로가기

오픈소스/노드

[Node] 05. Promise 그리고 Async/Await

 본 문서는 Node.js 디자인 패턴 바이블 을 읽고 리뷰를 남기고 있습니다. 문고들은 이 책의 일부분을 인용한 것임을 밝힙니다.

 

 Node.js 에서 콜백은 비동기 프로그래밍의 기본적인 방식이지만 개발자 친화적인 것과는 거리가 멀어 보입니다. 실제로 이전 장에서 콜백을 사용하여 다양한 제어 프름을 구현하기 위한 기술을 배웠습니다. 그리고 그것들은 우리가 해결하려는 문제에 비해서 매우 복잡하고 장황하다고 볼 수 있었습니다. 특히, 우리가 작성하는 대부분의 제어 흐름 구조는 순차적인 함수의 실행인데 이에 익숙하지 않은 개발자들에게 콜백 지옥이라는 문제를 일으키게 만듭니다. 게다가, 제대로 구현된 경우라 해도 콜백을 통한 순차적인 실행 흐름은 불필요하게 복잡하고 오류가 발생하기 쉽습니다. 또한 콜백을 사용한 오류의 관리가 얼마나 취약한 지 이미 알고 있을 것입니다. 오류를 다음 실행으로 전달하는 것을 잊으면 해당 오류에 대한 컨트롤을 잃게 되며, 동기적 코드에서 발생한 에러 코드를 탐지하지 못하면 프로그램이 망가집니다. 또한 Zalgo를 조심하지 않으면 언제나 우리를 괴롭히며 따라 다닐 것입니다. 

 

 Node.js와 JavaScript는 수 년간 일반적이고 흔히 발생하는 문제에 대한 네이티브 솔루션의 부재로 인해 지탄받아왔습니다. 운 좋게도 여러 해 동안 커뮤니티에서 그 문제에 대한 새로운 솔루션을 찾기 위해 노력해 왔습니다. 마침내 수 많은 반복과 논쟁을 거치며 몇년을 기다린 끝에 콜백 이슈에 대한 적절한 솔루션을 갖게 되었습니다. 

 

 

# Contents


  • 프라미스
  • Async/await
  • 무한 재귀 프라미스 해결

 

 

# 프라미스


 프라미스는 표준 ECMA 2015(또는 ES6)의 일부이며 Node 버전 4부터는 Node.js 에서 기본적으로 사용할 수 있습니다. 프라미스는 비동기 결과를 전파하기 위해서 사용한 연속 전달 방식(CPS)의 콜백을 대신할 강력한 대안으로 큰 발걸음을 내딛게 됩니다. 앞으로 보게 되겠지만, 프라미스는 모든 주요 비동기 흐름을 제어하는 대부분의 코드를 콜백 기반의 대안들에 비해 더 읽기 쉽고 덜 장황하며 더 강력하게 만들 것입니다.

 

1. Promise 란 무엇인가

 프라미스는 비동기 작업의 최종적인 결과(또는 에러)를 담고 있는 객체입니다. 프라미스의 용어로, 비동기 작업이 아직 완료되지 않았을 때 대기중(pending)이라 하며, 작업이 성공적으로 끝났을 때를 이행됨(fullfilled)라고 하고, 작업이 에러와 함께 종료됐을 때 거부됨(rejected)이라고 합니다. 프라미스가 이행되거나 거부되면 결정된(settled) 것으로 간주됩니다.

 

 이행 값이나 거부와 관련된 에러를 받기 위해 프라미스 인스턴스의 then() 함수를 사용할 수 있습니다. 다음은 그 형식입니다.

 

promise.then(onFulfilled, onRejected)

 

 위의 형식에서 onFulfilled는 최종적으로 프라미스의 이행값을 받는 콜백이며, onRejected는 거부 이유를 받는 콜백입니다. 두 콜백 모두 선택 사항입니다. 프라미스가 어떻게 우리의 코드를 변화할 수 있는지 보기 위해 다음과 같은 콜백 기반 코드를 생각해보겠습니다.

 

asyncOperation(arg, (err, res) => {
	if (err) {
    	// 에러처리
    }
    // 결과처리
})

 

 다음과 같이 프라미스는 전형적인 연속 전달 방식(CPS)의 코드를 보다 체계적이고 우아한 코드로 바꿀 수 있게 됩니다.

 

asnycOperationPromise(arg)
	.then(result => {
    	// 결과처리
    })
    .catch(err => {
		// 에러처리
    })

 

 위의 코드에서 asyncOperationPromise()는 프라미스를 반환합니다. 우리의 함수의 최종적인 결과인 이행값이나 거부 사유를 받기 위해 반환된 프라미스를 사용할 수 있습니다. 지금까지는 중대한 일이 없는 것처럼 보이지만 매우 중요한 then() 함수의 특성은 또 다른 프라미스를 동기적으로 반환한다는 것입니다.

 

 또한 onFulfilled() 또는 onRejected() 핸들러에서 예외를 발생시키면(throw 구문을 사용하여), then() 메소드에서 반환되는 프라미스는 발생된 예외를 거부 사유로 자동 거부됩니다. 이것은 CPS에 비해 매우 큰 이점이 있는데, 프라미스와 함꼐 예외가 체인 전체에 자동으로 전파되고, 최종적으로 throw 문을 사용할 수 있기 때문입니다.

 

2. Promise/A+와 thenable

 역사적으로 여러 가지 프라미스 구현이 존재했으며 대부분은 서로 호환되지 않았습니다. 즉, 서로 다른 라이브러리의 프라미스 객체 간에는 체인을 만들 수 없다는 것을 의미합니다.

 

 JavaScript 커뮤니티는 이러한 한계를 극복하고자 많은 노력을 했으며 이러한 노력으로 Promise/A+ 사양을 만들었습니다. 이 사양은 then() 함수의 동작을 자세히 설명하여 상호 운용 가능한 기반을 제공함으로써 서로 다른 라이브러리의 프라미스 객체를 바로 사용할 수 있게 하였습니다. 

 

 Promise/A+ 표준을 채택한 결과, 네이티브 JavaScript 프라미스의 API를 포함한 많은 프라미스 구현들은 then() 함수가 있는 모든 객체를 thenable 이라는 Promise와 유사한 객체로 간주합니다. 이 동작을 통해 서로 다른 프라미스 구현들이 서로 원활하게 연결될 수 있습니다.

 

3. 프라미스 API

 이제 네이티브 JavaScript 프라미스의 API를 빠르게 살펴보겠습니다. Promise 생성자는 새로운 Promise 인스턴스를 생성합니다. Promise 인스턴스는 인자로써 주어진 함수의 동작에 기반하여 이행과 거부를 합니다.

생성자에 주어지는 함수는 두개의 인자를 받습니다.

 

  • resolve(obj) : 이것은 호출될 때 제공된 이행값으로 프라미스를 이행하는 함수이며, obj가 값이면 obj 자체가 전달되고, obj가 프라미스나 thenable이면 obj의 이행값이 전달됩니다.
  • reject(err) : err 사유와 함께 프라미스를 거부합니다. err는 인스턴스를 나타내는 규약입니다.

 그리고 중요한 Promise 객체의 정적 메소드를 살펴보겠습니다.

 

  • Promise.resolve(obj) : 이 함수는 다른 프라미스, thenable 또는 값에서 새로운 프라미스를 생성합니다. 프라미스가 전달되면 해당 프라미스가 있는 그대로 반환됩니다. Thenable이 전달되면 해당 라이브러리의 프라미스로 변환됩니다. 값이 제공되면 그 값으로 프라미스가 이행됩니다.
  • Promise.reject(err) : 이 함수는 err를 이유로 거부하는 Promise를 생성합니다.
  • Promise.all(iterable) : 이 함수는 입력된 반복 가능한 객체 내의 모든 프라미스가 이행되면 이행된 결과값들의 배열을 이행값으로 하여 이행하는 새로운 프라미스를 생성합니다. 반복 가능한 객체 내의 하나라도 거부되면 Promise.all()로 반환된 프라미스는 첫 번째 거부 사유를 가지고 거부할 것입니다. 반복 가능한 객체의 각 항목은 프라미스, thenable, 또는 값일 수 있습니다.
  • Promise.allSettled(iterable) : 이 함수는 반복 가능한 객체 내의 모든 프라미스가 이행되거나 거부될 때까지 기다린 다음 입력된 각각의 Promise에 대한 이행값 또는 거부 사유를 담은 객체의 배열을 반환합니다. 각 객체는 이행됨 또는 거부됨같은 status 속성과 이행값을 담은 value 속성 그리고 거부 사유가 담긴 reason 속성이 있습니다. Promise.all() 과의 차이점은 Promise.allSettled()는 프라미스 중 하나가 거부될 때 즉시 거부되지 않고 모든 프라미스가 이행되거나 거부될 때 까지 기다린다는 것입니다.
  • Promise.race(iterable) : 이 함수는 반복 가능 객체에서 처음 결정된 프라미스를 반환합니다.

 마지막으로 Promise 인스턴스에서 사용 가능한 주요 함수들입니다.

 

  • Promise.then(onFulfilled, onRejected) : 이것은 프라미스의 필수 함수입니다. 이 동작은 앞서 언급한 promise/A+ 표준과 호환됩니다.
  • Promise.catch(onRejected) : 이 함수는 promise.then(undefined, onRejected)에 대한 편리한 버전입니다.
  • Promise.finally(onFinally) : 이 함수를 사용하면 프라미스가 결정될 때 호출되는 onFinally 콜백을 설정할 수 있습니다. onFulfilled 및 onRejected와 달리 onFinally 콜백은 입력으로 인자를 수신하지 않으며 여기에 반환된 값은 무시됩니다. Finally에서 반환된 프라미스는 현재 프라미스 인스턴스의 이행값 또는 거부 사유로 결정됩니다.

이제 우리가 생성자를 사용하여 어떻게 프라미스를 생성하는지 예제를 통해 살펴보겠습니다.

 

작성중...