JS Async Functionality 2 - Promise

이 시리즈는 자바스크립트에서 비동기를 다룰 때 마주치는 개념들을 다룬다. 이번 글에서는 Promise를 다룬다.

JS Async Functionality 1 - Intro


Promise:

{ pending, fulfilled, rejected }상태를 가지는 객체로, executor 함수를 인자로 받는다.

  • executor 함수( (resolve, reject ) => {} )의 역할:

    1. 비동기 함수를 호출하고
    2. 그 비동기 함수의 콜백에서 resolve를 호출한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const fetch = url => new Promise((resolve, reject) => {
    // 이 함수가 executor 함수이다. (주석 설명 참조)

    // 1. example Async API provided by Node.js
    http.get(options, result => {
    let data;
    result.on('data', chunk => data += chunk);

    // 2. Calls either [ resolve, reject ] from async callback.
    result.on('end', () => resolve(data));
    result.on('error', err => reject(err));
    });
    });

    executor 함수는 기존의 비동기 처리 방식을 그대로 옮겨온 것으로 이해하기 어렵지 않다.

  • 다만 Promise Chaining이라는 개념으로 Callback Hell을 1차원으로 들여쓰기 단계를 낮출 수 있다.

  • 이렇게 들여쓰기 단계를 줄이는 것은 중요한데 가독성에 의한 논리 오류가 빈번하게 발생했기 때문이다.


또한 Javascript 특성상 CPS 패턴으로 작성된 비동기 처리 함수에서, 이후에 호출되는 함수는 이전 함수의 클로저 참조도 할 수 있다.

  • 부주의하게 클로저 영역의 변수들을 사용하는 경우 메모리 사용량 면에서 좋을 게 없었다.
  • (ex) 전체 비동기 절차가 끝날 때 까지 호출 함수의 지역 변수들이 해제되지 못하는 등.

Promise Chaining:

Promise는 타입이자 객체이다. Promise(); 인스턴스는 자신의 실행 흐름에 관여하는 메소드를 세 개 갖는다: then, catch, finally (참고로 메소드는 총 5개이다. race, all이 그 나머지 둘이다.)

  • Prototype에 등록된 함수로, then은 Promise를 반환하고(그래서 Chaining이 가능하고) catch는 reject된 Promise에 한 해 수행되는 조건문으로 then(undefined, onRejected)와 동등하다. finally는 JS의 try-catch-finally의 finally와 동등하다.

then

then = (onFulfilled, onRejected) => Promise

즉 then의 두 번째 인자는 catch 절인 셈이다. 웬만하면 가독성을 위해 따로 catch 절을 사용한다.

  • onFulfilled = value => Promise (여기서 value는 Promise가 resolve한 값이다. 보통 비동기 함수의 결괏값.)

  • onRejected = value => {} (여기서 value는 Promise가 reject한 값이다. 보통 Error 객체.)

  • 1
    2
    3
    4
    5
    6
    7
    p.then(onFulfilled, onRejected);

    p.then(function(value) {
    // 비동기가 별 탈 없이 진행된 경우.
    }, function(reason) {
    // 비동기 함수 수행 중 오류가 난 경우.
    });

    then에서는 값을 그냥 반환하는 경우 Promise.resolve로 감싼 것과 같다. 즉 Promise가 반환되는 것인데, 그렇기 때문에 Promise Chaining이 가능한 것이다.


catch

catch = onRejected => Promise (!)

  • catch 메소드는 try-catchcatch와 같은 역할이다. 즉, catch가 성공했느냐, 실패했느냐에 따라 다시 then이 실행될 수도 있고 다른 catch가 실행될 수도 있고 앱이 멈출 수도 있다.

  • Case 1: catch 절에서 resolved Promise를 반환하는 경우: 이후의 then 수행

  • catch 절에서 따로 throw를 하거나, Promise.reject()를 호출하지 않는 경우 Promise는 resolved 상태로 변하여 then을 수행한 것과 동등하게 된다.

  • Case 2: catch 절에서 rejected Promise를 반환하는 경우: 이후의 catch 수행

  • JS의 try-catch에서 catch는 여러 개가 존재할 수 없는 것에 비해 Promise가 rejected 상태이면 catch절은 계속해서 호출된다. 보통 여러 개의 catch 절은 특정 오류만 잡고 싶을 때 사용한다.

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var p1 = new Promise(function(resolve, reject) {
    resolve('Success');
    });

    // 아래처럼 pin-point로 catch 절을 사용하는 것이 가능하다.
    // 기존 JS에서 try-catch를 여러 번 순차적으로 사용한 것과 동등하다.

    // 1.Promise를 생성
    p1.then(function(value) { // 1. throw를 호출해 catch 절로 이동
    throw new Error('oh, no!');
    }).catch(function(e) { // 2. reject 혹은 throw를 하지 않으므로 then 수행
    console.error(e.message);
    }).then(function(){ // 3. 이 then이 수행되게 됨.
    console.log('after a catch the chain is restored');
    }).catch(function () { // 4. 만약 [2], [3]에서 throw를 하는 경우 여기로 오게 됨.
    console.log('Not fired due to the catch');
    });

finally

(설명 생략)


알기 어려운 Promise의 특징

1. then, catch는 비동기로 실행된다.

  • 아무리 Promise.resolve(); 로 resolve가 동기로 수행되더라도 then, catch는 비동기로 queue 된다.

  • 1
    2
    3
    4
    5
    6
    7
    Promise.resolve(null).then(v => console.log('Async: ' + v));
    console.log('Sync!');
    // 결과:
    // Sync!
    // Async: null
    // WHY? then이 async로 microtask queue에 들어갔기 때문.
    // then이

2: then, catch는 비동기이지만 한꺼번에 수행된다.

  • 만약 then, catch가 setTimeout과 같은 일반적인 비동기였다면 Task Queue에서 처리된다. Task Queue는 한 작업만 처리하고 나머지 작업은 다음 순서로 넘긴다.

    • 1
      2
      3
      4
      5
      6
      function loop() {
      setTimeout(loop, 0);
      }
      loop();
      // 무한 루프에 걸리지 않는다.
      // Microtask가 아니므로 이벤트 루프에서 한 작업씩만(!!!) 처리한다.
  • 그러나 Promise, then, catch는 Microtask Queue에서 수행되는데, 이는 Event Loop 내의 Event Loop으로 생각하면 된다.

    • 굳이 이렇게 하는 이유는

      1. 다른 Javascript 수행이 되지 않음을 보장
      2. 화면이 변경되지 않음을 보장 하기 위해서이다.

  • Microtask가 호출한 microtask 역시 이어서 수행되며 microtask queue가 빌 때까지 이 단계는 끝나지 않는다.

  • Promise가 연속적으로 수행되어 문제가 발생하는 예제를 생각하려고 했으나 대부분의 비동기는 microtask를 사용하지 않기에 큰 문제는 없을 것 같다. 따라서 이 본문의 내용을 몰라도 거의 문제는 없을 것 같다.


출처

Promise then

Promise catch (재밌는 점은 catch 문서는 한국어 번역이 없다는 점이다.)

Using Promises


TODO

처음에 React를 통해 ES6를 배우면서 Promise를 접했을 때보다 문서 개수나 번역이 훨씬 좋아졌다는 걸 느꼈다. 앞으로의 JS 표준을 다루는 MDN Wiki 문서가 있으면 나도 기여해야겠다

JS Async Functionality 2 - Promise

https://jsqna.com/js-async-2/

Author

Seongbin Kim

Posted on

21-01-25

Updated on

21-01-26

Licensed under

댓글