ES6 Promises(3) - the API

자바스크립트 ES6 Promise API에 대한 마지막 이야기입니다.

작성자

박관웅

박관웅

pouu69

이 문서는 http://www.2ality.com/2014/10/es6-promises-api.html 를 번역한 내용입니다.

또한 ES6 Promises(2) - the API 다음 편 입니다.

12. Debugging promises

비동기 코드를 디버깅하는 가장 주요 문제점은 비동기 함수와 메서드 호출이 포함 되어있다는 것입니다. 비동기 호출은 하나의 task에서 시작되어, 새로운 task에서 수행됩니다. 만약 새로운 task가 잘못되면, stack trace는 이전 task 정보는 포함하지 않고 해당 task만 처리합니다. 따라서, 비동기 프로그래밍에서 훨씬 적은 디버깅 정보로 작업을 수행 해야합니다.

최근에 구글 크롬에서 비동기 코드를 디버깅 할 수 있게 되었습니다. 아직 완벽하게 promises를 서포트 해주지는 않습니다. 그러나 일반적인 비동기 호출을 얼마나 잘 처리하는지 대해선 인상적입니다.
예를 들어 다음 코드에서는, 먼저 비동기적으로 second를 호출하고, third를 호출합니다.

    function first() {
        setTimeout(function () { second('a') }, 0); // (A)
    }
    function second(x) {
        setTimeout(function () { third('b') }, 0); // (B)
    }
    function third(x) {
        debugger;
    }
    first();

아래 스크린샷을 보면, 디버거는 3개 함수를 포함하고 있는 stack trace를 보여 줍니다. 심지어 익명 함수(A),(B)까지 포함되어 있습니다.

스샷

13. promises 의 내부

이번 섹션에서는 promises를 다른 각도로 다가갈 것 입니다.: API을 어떻게 사용하는지 배우는 대신에, 간단한 구현 방법을 배워보겠습니다.
이 다른 각도를 통해 promises의 감각을 키우는데 크게 도움이 되었습니다.
DemoPromise 라고 부르는 promise를 구현하고, GitHub을 통해 사용할수 있습니다. 쉽게 이해하게 하려고, 완전하게 API랑 일치 시키지는 않았습니다. 하지만 실제 당신이 직면한 것에 인사이트를 줄만큼 충분히 가깝게 되어있습니다.

DemoPromise는 생성자와 3가지 prototype 메서드가 있습니다.: - DemoPromise.prototype.resolve(value) - DemoPromise.prototype.reject(reason) - DemoPromise.prototype.then(onFulfilled, onRejected)

resolvereject는 메서드 입니다.(생성자 매개변수로 전달되는 함수)

13.1 A stand-alone promise

우리의 첫번째 구현은 최소한의 기능만 갖춘 독립형 promise 입니다.

  • promise를 생성 할수 있습니다.
  • promiseresolve또는 reject 할 수 있으며, 오직 한번 만 할수 있습니다.
  • then()을 통해 반응(reactions)(콜백)을 등록 할 수 있습니다. 이 메서드는 아직 체이닝을 할 수 없습니다. - 그 어느것도 반환 하지 않습니다. 그것은 promise가 이미 처리 됬는지 안됬는지 상관없이 독립적으로 작동 해야 할 것입니다.

다음은 첫번째 구현방법을 적용한 코드입니다.

    var dp = new DemoPromise();
    dp.resolve('abc');
    dp.then(function (value) {
        console.log(value); // abc
    });

다음 다이어그램은 첫번째 DemoPromise 가 어떻게 작동하는지 알려줍니다.

다이어그램

먼저 2가지 경우를 처리하는 then()을 살펴보겠습니다.

  • 만약 promise가 아직도 pending 상태라면, promise가 확정(settled)될때 사용되는 onFulfilledonRejected 호출을 큐에 삽입합니다.
  • 만약 이미 fulfilled 또는 rejected상태 라면, onFulfilled 또는 onRejected는 곧바로 호출 할 수 있습니다.
    DemoPromise.prototype.then = function (onFulfilled, onRejected) {
        var self = this;
        var fulfilledTask = function () {
            onFulfilled(self.promiseResult);
        };
        var rejectedTask = function () {
            onRejected(self.promiseResult);
        };
        switch (this.promiseState) {
            case 'pending':
                this.fulfillReactions.push(fulfilledTask);
                this.rejectReactions.push(rejectedTask);
                break;
            case 'fulfilled':
                addToTaskQueue(fulfilledTask);
                break;
            case 'rejected':
                addToTaskQueue(rejectedTask);
                break;
        }
    };
    function addToTaskQueue(task) {
        setTimeout(task, 0);
    }

resolve()는 다음과 같이 작동합니다. :
만약 이미 promise가 확정(settled)되었다면, promise는 더이상 확정 될 수 없습니다. 그리고 promise 상태는 fulfilled로 변경되고, 결과는 this.promiseResult에 캐시됩니다. 지금 까지 큐에 추가 되었던 모든 fulfillment 반응(reactions)들은 바로 실행 될 것입니다.

    Promise.prototype.resolve = function (value) {
        if (this.promiseState !== 'pending') return;
        this.promiseState = 'fulfilled';
        this.promiseResult = value;
        this._clearAndEnqueueReactions(this.fulfillReactions);
        return this; // enable chaining
    };
    Promise.prototype._clearAndEnqueueReactions = function (reactions) {
        this.fulfillReactions = undefined;
        this.rejectReactions = undefined;
        reactions.map(addToTaskQueue);
    };

reject()resolve()랑 비슷합니다.

13.2 체이닝

다음으로 체이닝을 구현 해봅시다.:

  • then()onFulfilledonRejected 으로 확정된 promise를 반환합니다.
  • 만약 onFulfilled 또는 onRejected가 누락되어도, 어쨋든 수신한 데이터는 then()에 의해 반환된 promise로 전달됩니다.

체이닝

분명하게 then()만 변경됩니다.

    DemoPromise.prototype.then = function (onFulfilled, onRejected) {
        var returnValue = new DemoPromise(); // (A)
        var self = this;

        var fulfilledTask;
        if (typeof onFulfilled === 'function') {
            fulfilledTask = function () {
                var r = onFulfilled(self.promiseResult);
                returnValue.resolve(r); // (B)
            };
        } else {
            fulfilledTask = function () {
                returnValue.resolve(self.promiseResult); // (C)
            };
        }

        var rejectedTask;
        if (typeof onRejected === 'function') {
            rejectedTask = function () {
                var r = onRejected(self.promiseResult);
                returnValue.resolve(r); // (D)
            };
        } else {
            rejectedTask = function () {
                // Important: we must reject here!
                // Normally, result of `onRejected` is used to resolve
                returnValue.reject(self.promiseResult); // (E)
            };
        }
        ...
        return returnValue; // (F)
    };

then()new promise를 생성하여 반환합니다.((A),(F)라인) 게다가, fulflledTaskrejectedTask는 다르게 설정됩니다.

  • onFulfilled 결과는 returnValue를 해결 하는데 사용됩니다.((B)라인)
    • 만약 onFulfilled가 누락된 경우, returnValue를 해결하는데 fulfullment를 사용합니다.((C)라인)
  • onRejected의 결과는 returnValue를 해결하는데 사용됩니다.(거절이 아닌(reject))((D)라인)
    • 만약 onRejected가 누락된 경우, returnValue를 해결하는데 거절(rejection) 값을 사용합니다.

13.3 Flattening(편평한)

역자주 : Flattening 은 해석하기 애매한 단어 임으로, 영어로 표기

Flattening 은 체이닝을 좀더 편리하게 만듭니다. : 일반적으로, 반응(reaction)에서 값을 반환하면 다음 then()으로 전달 됩니다. 만약 우리가 promise를 반환하면, 그것이 우리가 풀지 않을 수 있다면, 그것은 아래 예제와 같이 좋을 것입니다.

    asyncFunc1()
    .then(function (value1) {
        return asyncFunc2(); // (A)
    })
    .then(function (value2) {
        // value2 is fulfillment value of asyncFunc2() promise
        console.log(value2);
    });

우리는 라인(A)에서 promise를 반환했습니다. 그리고 현재 메서드에서 중첩된 then()을 호출 하지 않았습니다. 우리는 메서드 결과에서 then()을 호출합니다. 따라서, then()은 더이상 중첩되지 않고, 모든것이 편평(flat)하게 유지됩니다.
우리는 resolve()flattening을 수행 할 수 있도록 구현하겠습니다.

  • promise Qpromise P를 해결(Resolving)하는 것은 Q's의 확정(settlement)이 P's의 반응(reactions)으로 전달되는 것을 의미합니다.
  • PQ에 갇힙니다. : 더이상 해결 할수 없습니다.(rejected포함). 그리고 그 상태와 결과는 Q's와 항상 같습니다.

우리가 Qthenable이 되도록 허락 한다면, 좀더 제네릭하게 만들 수 있습니다.(promise를 대신하여)

플래튼

위에 말한 것처럼 갇히게 하는것을(locking-in) 구현하기 위해 사용하는 새로운 boolean flagthis.alreadyResolved를 소개합니다. 일단 flag가 true가 되면, this는 갇히게 되고, 더이상 해결(resolve) 할 수 없습니다. this는 여전히 pending 상태인것을 기억하십시오. 왜냐하면 그 상태는 지금 promise에 잠겨있는 것과 같기 때문입니다.

    DemoPromise.prototype.resolve = function (value) {
        if (this.alreadyResolved) return;
        this.alreadyResolved = true;
        this._doResolve(value);
        return this; // enable chaining
    };

이제 실제 해결은 private 메서드인 _doResolve()에서 발생합니다.

    DemoPromise.prototype._doResolve = function (value) {
        var self = this;
        // Is `value` a thenable?
        if (value !== null && typeof value === 'object'
            && 'then' in value) {
            addToTaskQueue(function () { // (A)
                value.then(
                    function onFulfilled(value) {
                        self._doResolve(value);
                    },
                    function onRejected(reason) {
                        self._doReject(reason);
                    });
            });
        } else {
            this.promiseState = 'fulfilled';
            this.promiseResult = value;
            this._clearAndEnqueueReactions(this.fulfillReactions);
        }
    };

flattening은 (A)라인에서 수행됩니다. : 만약 valuefulfilled면, selffulfilled되길 원합니다. 그리고 만약 valuerejected라면, selfrejected 되길 원합니다.
private메서드인 _doResovle()_doReject() 통해 발생하고, alreadyResolved를 통해 보호받습니다.

13.4 상세한 Promise 상태

체이닝 통해서 promises 상태는 점점 더 복잡해집니다.

머지?

만약 당신이 promises만 사용하면, 일반적으로 단순한 세계관을 채택하고 가두는 것을(locking-in)무시할 수 있습니다. 가장 중요한 상태-관련 개념은 확정(settledness)을 유지하는 것입니다. promise는 완료(fulfilled)되거나, 거절(rejected)되면 확정(settled) 됩니다. promise가 확정(settled)되면 더이상 변하지 않습니다. (상태 그리고 완료(fulfillment) 또는 거절(rejection) 값)

만약 당신이 promise를 수행하길 원한다면, 문제를 해결하기도 어렵고, 지금 그것을 이해하기는 어렵습니다.

  • 직관적으로 해결됨(resolved)은 더이상 (직접적으로)해결 할수 없다는 의미입니다. promise는 확정(settled)되었거나, 갇혀(locked in)있으면, 해결(resolved)된것입니다.
    스펙 인용 : "promise가 해결되지 않았으면, 항상 보류(pending) 상태 입니다. 해결된 promise는 보류(pending), 이행(fulfilled), 거절(rejected) 상태일 수 있습니다."

  • 해결된 promise가 꼭 확정된(settling) 상태는 아닐수 있습니다. : 당신은 보류(pending)된 상태로 promise를 해결(resolve) 할 수 있습니다.

  • 해결은 거절(rejection)을 포함합니다. : 거절(reject)된 promise로 해결(resolving) 하면 promise를 거절(reject) 할 수 있습니다.

13.5 예외

마지막으로 우리는, 거절(rejections)을 사용자 코드단에서 예외로 처리하고 싶습니다. 현재 사용자 코드(user code)then()의 2개 콜백 파라미터를 의미합니다.

예외

다음 발췌한 부분은 onFulfilled 내부 예외를 (A)라인에서 호출할 때 try-catch로 감싸서 거절(reject)하는 방법을 보여줍니다.

    var fulfilledTask;
    if (typeof onFulfilled === 'function') {
        fulfilledTask = function () {
            try {
                var r = onFulfilled(self.promiseResult); // (A)
                returnValue.resolve(r);
            } catch (e) {
                returnValue.reject(e);
            }
        };
    } else {
        fulfilledTask = function () {
            returnValue.resolve(self.promiseResult);
        };
    }

13.6 공개 생성자패턴

만약 DemoPromise를 실제 promise 구현체로 변경하고 싶다면, 공개 생성자 패턴을 구현 해야 합니다.
ES6 Promises는 메서드를 통해 해결(resolved)되거나 거절(rejected)되어지지 않습니다. 그러나 생성자 콜백 파라미터 실행자에 전달되어지는 함수를 통해서..(but via functions that are handed to the executor, the callback parameter of the constructor)

공개생성자패턴

만약 실행자(executor)가 then으로 예외를 던진다면, promise는 거절(rejected)되어져야 합니다.

14. 추가적인 2가지 유용한 promise 메서드's

이번 섹션에서는 ES6 promises에 쉽게 추가 할 수 있는 2가지 유용한 메서드를 설명 합니다. promise는 많은 라이브러리를 가지고 있습니다.

14.1 done()

몇몇 promise를 여러개 체이닝 호출을 하면 에러가 쥐도새도 모르게 삭제될 위험이 있을 수 있습니다.
예를들어:

    function doSomething() {
        asyncFunc()
        .then(f1)
        .catch(r1)
        .then(f2); // (A)
    }

만약 (A)라인 then()이 거절(rejection)당하면, 아무데서도 처리 되지 않습니다. promise 라이브러리 Q는 메서드 체인 마지막 요소에 done()을 사용합니다.
마지막 then() 을 대신 합니다.(1~2개의 인자가 있슴)

    function doSomething() {
        asyncFunc()
        .then(f1)
        .catch(r1)
        .done(f2);
    }

또는 마지막 then() 이후에 삽입됩니다. (0개 인수를 가짐)

    function doSomething() {
        asyncFunc()
        .then(f1)
        .catch(r1)
        .then(f2)
        .done();
    }

Q 문서에 인용문:
- done의 황금룰 vs then 의 사용법 : 당신의 promise를 누군가에게 반환 하거나, 체이닝이 끝이라면 done을 호출 하여 제거하세요. 왜냐하면, catch 핸들러 자체적으로 에러를 발생 시킬 수 있기 때문에 catch로 종료하는 것만으론 충분하지 못합니다.

ECMAScript6done()을 구현하는 방법

    Promise.prototype.done = function (onFulfilled, onRejected) {
        this.then(onFulfilled, onRejected)
        .catch(function (reason) {
            setTimeout(() => { throw reason }, 0);
        });
    };

done()의 기능은 유용하지만, ECMAScript6에는 추가 되지 않습니다. 왜냐하면, 이런 일련의 과정은 미래에 디버거가 자동적으로 수행될 것 입니다.

14.2 finally()

때론 오류가 발생하던지 말던지, 독립적인 액션을 수행하길 원할 때가 있습니다. 예를들어, 작업을 마친후 리소스는 정리 해야합니다. 그것을 위한게 바로 finally() 메서드 입니다. 예외처리에서 finally 구문과 비슷하게 작동합니다.
finally() 는 인자가 없는 콜백을 받지만, 해결(resolution)인지 거절(rejection)인지 통보 받습니다.

    createResource(...)
    .then(function (value1) {
        // Use resource
    })
    .then(function (value2) {
        // Use resource
    })
    .finally(function () {
        // Clean up
    });

Domenic Denicolafinally()를 구현한 목적입니다.

    Promise.prototype.finally = function (callback) {
        let p = this.constructor;
        // We don’t invoke the callback in here,
        // because we want then() to handle its exceptions
        return this.then(
            // Callback fulfills: pass on predecessor settlement
            // Callback rejects: pass on rejection (=omit 2nd arg.)
            value  => p.resolve(callback()).then(() => value),
            reason => p.resolve(callback()).then(() => { throw reason })
        );
    };

콜백은 수신자(this)의 확정(settlement) 방법을 결정합니다.

  • 콜백이 예외를 던지거나, promise then에 거절(rejected)을 반환하면, 거절 값(rejection value)이 됩니다.
  • 그렇지 않으면, 수신자(receiver)의 결정은 finally()에 반환된 promise 값으로 됩니다. 우리는 메서드 체인에서 finally()를 가져오게 됩니다.

  • 예제1 (by Jake Archibald): finally()로 스피너를 숨기는 방법. 간단버전:

    showSpinner();
    fetchGalleryData()
    .then(data => updateGallery(data))
    .catch(showNoDataError)
    .finally(hideSpinner);
  • 예제2 (by Kris Kowal ) 서버 다운 테스트
    var HTTP = require("q-io/http");
    var server = HTTP.Server(app);
    return server.listen(0)
    .then(function () {
        // run test
    })
    .finally(server.stop);

15 ES6 호환되는 promise 라이브러리

많은 promise 라이브러리가 밖에 있습니다. 다음은 ECMAScript 6 API에 따르며, 지금 바로 사용 할수 있으며, 쉽게 네이티브 ES6로 나중에 마이그레이션 할 수 있습니다.

  • RSVP.js : Stefan Penner에 의해 만들어진 RSVP.JS는 ES6 Promise API 상위 집합니다.
    • Jake Archibald의 ES6-Promise는 RSVP.js에서 ES6 API만 뽑아낸것입니다.
  • Kyle Simpson의 Native Promise Only는 엄격한 스펙 정의에 가능한한 근접한 네이티브 ES6 promises를 위한 polyfill 입니다.
  • Calvin Metcalf의 Lie는 Promises/A+ spec 을 구현한 작고 퍼포먼스있는 promise 라이브러리입니다.
  • Kris Kowal 의 Q.Promise는 ES6 API입니다.
  • 마지막으로 Paul Millr의 ES6 ShimPromise를 포함합니다.

16.레거시 비동기 코드와의 인터페이스

당신이 promise 라이브러리를 사용하면, 때론 promise기반이 아닌 비동기 코드가 필요할 때가 있습니다. 이번 섹션은 Node-js스타일 비동기함수와 jQuery deferreds가 어떻게 작동하는지 설명하고자 합니다.

16.1 Node.js 인터페이스

promise 라이브러리 Q는 Node.js 스타일 콜백을 사용하는 함수를 promise를 반환하는 함수로 변환하기 위한 몇몇 툴 함수를 가지고 있습니다.(반대 함수도 있다. promise를 반환하는 함수를 Node.js 스타일 콜백으로)
예를들어:

    var readFile = Q.denodeify(FS.readFile);

    readFile('foo.txt', 'utf-8')
    .then(function (text) {
        ...
    });

deonodify는 알림기능만 제공하고 ECMAScript6 promise API를 따르는 마이크로 라이브러리 입니다.

16.2 jQuery 인터페이스

jQuerypromise와 비슷한 deferreds를 가지고 있습니다. 그러나 호환성을 방해하는 몇몇 다른점이 있습니다. deferredsthen()은 거의 ES6 promises와 비슷합니다.(에러를 캐치 할 수 없는 주요한 다른점이 있습니다.) 따라서 Promise.resolve()jQuery deferredES6 promise로 변환 시킬수 있습니다.

    Promise.resolve(
        jQuery.ajax({
            url: 'somefile.html',
            type: 'GET'
        }))
    .then(function (data) {
        console.log(data);
    })
    .catch(function (reason) {
        console.error(reason);
    });

17. Further reading

Tags : javascript es6 promise 

comments powered by Disqus