Javascript Promise Basic



Prmoise/A+ 사양에 기반을 둔 ECMAScript6
비동기 프로그래밍을 위한 또 다른 패턴 - Promise
전통적인 콜백 패턴이 가진 단점을 일부 보완하며 비동기 처리 시점을 명확하게 표현하도록 한다.

Promise는 비동기 처리 로직을 추상화한 객체와 그것을 조작하는 방식을 말한다.
E 언어에서 처음으로 Promise가 고안됐으며, 병렬 및 병행 프로그래밍을 위한 일종의 디자인이라 할 수 있다.

그리고 꼭 명심할 것은,
Promise는 단순하게 콜백-헬만을 해결하기 위한 도구는 아니라는 것이다.



Promise의 사용성

1. 만약 이미지 엘리먼트에 Promise를 반환하는 ready()메서드가 있었다면!?

아래는 보통 이미지엘리먼트의 load와 error 이벤트를 작성하는 코드이다.

var img = document.querySelector("#myImg");

img.addEventListener("load", function() {
  // 로딩 완료.
});

img.addEventlistener("error", function() {
  // 에러
});

그런데 만약 이벤트리스너에 콜백이 등록되기 전에 이미지의 로드가 끝나버린다면, 기껏 등록한 리스너를 수행되지 않는다. 그래서 "complete" 속성을 이용한 차선책이 필요해진다.

function loaded() {
  // 로딩 완료
}

if (img.complete) {
  loaded();
} else {
  img.addEventlistener("load", loaded);
}

img.addEventListener("error", function() {
  // 에러
});

그렇지만 여전히 문제가 있다.
에러 처리 리스너를 등록하기 전에 에러가 발생한다면 에러를 처리하기 꽤 까다로워 진다는 것이다.
만약 하나의 이미지가 아니라 여러 이미지들의 모든 로딩을 기다려야 한다면? 코드는 더욱 복잡하게 변할 것이다.

HTML 이미지 엘리먼트가 promise 객체를 반환하는 ready()메서드를 가지고 있다면 어떻게 될까?

img
  .ready()
  .then(function() {
    // 로딩 완료
  })
  .catch(function() {
    // 에러
  });

끝이다. 이미지들이 여러개 있다면?

Promise.all([img1.ready(), img2.ready(), img3.ready()])
  .then(function() {
    // 모든 이미지 로딩완료
  })
  .catch(function() {
    // 이미지 로딩이 1개라도 실패됨.
  });

모든 문제가 간단하고 효과적으로 해결된다.
아래의 특징들이 이러한 비동기 처리를 훨씬 간편하게 처리하도록 만든다.

  • Promise는 단 한번 성공하거나 실패한다.
  • Promise가 성공/실패한 이후 나중에 콜백을 추가하였더라도, 이벤트가 먼저 발생되었더라도, 올바른 콜백이 호출된다.

이는 비동기 처리에서 성공/실패의 어떤 정확한 시점보다 그 결과에 따른 처리에 더 집중되기 때문이다.

2. 콜백-헬 완화, 에러처리

아래는 보통 비동기 자바스크립트 로직을 처리할 때 사용하는 콜백 패턴이다.

asyncFn(
  "foo",
  function(result) {
    // success
  },
  function(error) {
    // error
  }
);

// 또는
asyncFn("foo", function(error, result) {
  if (error) {
    // error
  } else {
    // success
  }
});

그렇다면 result값을 가지고 처리하는 부분에 비동기 처리가 한번 더 들어가면 어떻게 될까?

asynFn(
  "foo",
  function(result) {
    // success asynFn
    anotherAsyncFn(
      result,
      function(nResult) {
        // success anotherAsyncFn
      },
      function(error) {
        // error anotherAsyncFn
      }
    );
  },
  function(error) {
    // error asynFn
  }
);

// 또는
asyncFn("foo", function(error, result) {
  if (error) {
    // error asynFn
  } else {
    // success asynFn
    anotherAsyncFn(result, function(nError, nResult) {
      if (nError) {
        // error anotherAsyncFn
      } else {
        // success anotherAsyncFn
      }
    });
  }
});

단 하나의 비동기 처리 로직이 추가되었을 뿐인데 눈에 보이는 복잡도는 훨씬 더 커졌다.
설마 이런 로직이 있을까하는 의심이 들겠지만, 실제로 있다.

아래는 현재 WebRTC를 구현하기 위한 일부 코드이다.

(현재까지 나온 W3C 스펙에서는 WebRTC API가 Promise 기반으로 정의되어 있지만, 크롬 브라우저에서는 아직 Promise 기반의 API를 지원하지 않아 콜백 패턴을 사용한다.)

connection.setRemoteDescription(
  new RTCSessionDescription(offer),
  function() {
    connection.createAnswer(
      function(answer) {
        connection.setLocalDescription(
          answer,
          function() {
            socket.emit("singnaling", connection.localDescription);
          },
          function(error) {
            console.log(error);
          }
        );
      },
      function(error) {
        console.log(error);
      }
    );
  },
  function(error) {
    console.log(error);
  }
);

Promise기반의 API를 사용하는 코드는 아래와 같다.

connection
  .setRemoteDescription(new RTCSessionDescription(offer))
  .then(function() {
    return connection.createAnswer();
  })
  .then(function(answer) {
    return connection.setLocalDescription(answer);
  })
  .then(function() {
    socket.emit("signaling", connection.localDescription);
  })
  .catch(function(error) {
    console.log(error);
  });

여기에서 확인할 수 있는것은 콜백-헬을 완화한 것과,
에러처리 메커니즘이 하나로 줄었다는 것이다.

꼭 명심해야 할 것은, Promise는 콜백-헬을 완화시켰지, 해결하진 않는다는 것이다.
위의 코드는 단순하게 중첩되지 않는 형태로 변환했을 뿐, 콜백-헬 문제를 해결했다고 보기는 어렵다.



Promise state

아래는 promise 객체의 [[PromiseState]] 프로퍼티가 갖는 값들이다.
(참고로 자바스크립트에서 [[PromiseState]]에 접근할 수 있는 API는 없다.)

  • "pending": 성공도 실패도 아닌 상태
  • "fulfilled": 성공(resolve)했을 때의 상태
  • "rejected": 실패(reject)했을 때의 상태

참고로 몇몇 글에서 "rejected"와 "fulfilled"의 상태를 합쳐서 또 다른 "settled"라는 상태와 그 값이 있는 것처럼 설명하지만, 실제 promise 객체의 [[PromiseState]]에 "settled"라는 상태와 값은 존재하지 않는다. 단순히 한번 정해지면 앞으로 불변하는 상태라는 것을 의미적으로 표현하기 위해 "settled"라는 단어로 "fulfilled"와 "rejected"를 표현했을 뿐이다.



Promise API

Constructor

new 연산자를 사용하여 promise 객체를 생성한다.

var promise = new Promise(function(resolve, reject) {
  // 비동기 처리 로직
  // 완료되면 resolve 호출
  // 실패하면 reject 호출
});

Instance Method

promise 객체의 then 메서드를 통해,
성공(resolve) 또는 실패/거절(reject)하였을때 호출할 콜백 함수들을 등록할 수 있다.

promise.then(successCallback, errorCallback);

successCallback이나, errorCallback은 option인자이므로 생략할 수 있다.
만약 에러 처리만 하고 싶다면, 아래 코드처럼 사용하면 된다.

promise.then(undefined, errorCallback); // 또는
promise.catch(errorCallback);

Static Method

정적 메서드들은 Promise를 다루는데 필요한 보조 메서드들이다.

  • Promise.all(iterableObject) - 모든 Promise가 해결되었을 때 처리.
  • Promise.race(iterableObject) - 가장 먼저 해결되는 Promise에 관한 처리.
  • Promise.reject(reason) - Rejected promise 객체 반환
  • Promise.resolve - Fulfilled promise 객체 반환

    • Promise.resolve(value)
    • Promise.resolve(promise)
    • Promise.resolve(thenable)



Promise.prototype.then

promise의 상태 변화시 수행할 콜백들,
taskA, taskB, onError, finalTask가 있다고 생각해보자.

var promise = Promise.resolve();
promise
  .then(taskA)
  .then(taskB)
  .catch(onError)
  .then(finalTask);

flow

빨간선은 중간 테스크에서 에러가 발생할때의 흐름을 나타낸다.
taskA에서 에러가 발생하게 된다면 taskB는 수행되지 않고, onError가 수행된다.
onError의 수행이 올바르게 완료된다면 finalTask가 수행된다.

주의!
finalTask 이후 catch가 없으므로, onError수행 도중 다시 또 발생하게 되는 예외나 finalTask에서 발생하는 예외는 확인할 수 없다.

이제 간단한 수식을 계산하는 Promise를 만들어보자.

_((1 _ 2) + 1) / 2*

function doubleUp(value) {
  return value * 2;
}

function increment(value) {
  return value + 1;
}

function goHalves(value) {
  return value / 2;
}

function printOut(value) {
  console.log(value);
}

var promise = Promise.resolve(1);
promise
  .then(doubleUp)
  .then(increment)
  .then(goHalves)
  .then(printOut);

각 테스크들(double, increment, goHalves)의 반환값은 다음 테스크에 전달된다.
resolve(1) -> doubleUp(1) -> increment(2) -> goHavles(3) -> printOut(1.5)
반환값으로 숫자, 문자열, 객체뿐만 아니라 promise 객체도 가능한데, 사실은 return Promise.resolve(returnValue)로 처리되기 때문에 무엇을 반환하더라도 최종적으로는 새로운 promise 객체가 반환되는 것이다.



Promise.prototype.catch

Promise.prototype.then 에서 이미 catch()를 사용해보았다.
catch()promise.then(undefined, onError)의 랩핑 함수다. 즉, prmoise가 "rejected"상태가 됐을 때 호출될 콜백 함수를 등록하기 위한 메서드다.

var promise = Promise.reject(new Error("error message"));

promise.catch(function(error) {
  console.log(error.message);
});

IE8이하 브라우저에서 Promise 폴리필을 사용하게 된다면, 예약어 관련 문제로 syntax error가 발생한다. 아래 방식으로 우회하도록 하자.

var promise = Promise.reject(new Error("error message"));

promise["catch"](function(error) {
  console.log(error.message);
});

// 또는

promise.then(undefined, function(error) {
  console.log(error.message);
});

서비스에서 IE8 이하 브라우저를 지원해야 한다면 catch를 조심하도록 하자.



Promise.all

Promise.all은 인자로 배열을 받는다. 배열의 원소는 promise.then에서 리턴할 수 있는 값/객체들이 될 수 있다.
배열 내의 모든 원소들이 해결된다면, 각 원소들의 결과가 순서대로 들어간 배열로 다음 동작에 전달한다.

var promise1 = Promise.resolve("abcd");
Promise.all([promise1, 1, 2, 3]).then(function(results) {
  // 모든 작업이 완료됨
  console.log(results); //['abcd', 1, 2, 3]
});



Promise.race

사용방법은 Promise.all과 같지만, 결과가 다르다. Promise.all은 모든 promise가 "fulfilled"상태가 될때까지 기다리지만, Promise.race는 전달된 값들 중 하나만 완료되어도 다음 동작으로 넘어간다. 또한 다른 원소들의 동작을 취소시키지도 않는다.

function timer(time) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      console.log("timer", time);
      resolve(time);
    }, time);
  });
}

var promise1 = timer(10);
var promise2 = timer(100);
var promise3 = timer(1000);

Promise.race([promise1, promise2, promise3]).then(function(result) {
  console.log("race winner", result);
});

//  timer 10
//  race winner 10
//  timer 100
//  timer 1000



마치며

기억해야할 Promise의 특징

  1. Promise는 항상 비동기로 처리된다.
  2. Promise.prototype.then매번 새로운 promise객체를 반환한다.
  3. 예외처리는 마지막에
    Promise.resolve(1).then(throwError, onRejected): onRejected가 호출되지 않는다. Promise.resolve(1).then(throwError).catch(onRejected): onRejected가 호출된다.

함께보면 좋은 것

References


이민규, FE Development Lab2015.09.04Back to list