Promise 란?


Promise 란 비동기 처리 로직을 추상화한 객체와 그것을 조작하는 방식을 말합니다. E 언어에서 처음 고안됐으며 병렬 및 병행 프로그래밍을 위한 일종의 디자인입니다. Promise 는 전통적인 콜백 패턴이 가진 단점을 일부 보완 하고 비동기 처리 시점을 명확하게 표현합니다. 그리고 Promise 객체의 인터페이스를 이용해 다양한 비동기 처리를 패턴화할 수 있기 때문에 복잡하고 불편한 비동기 예외 처리를 손쉽게 다룰 수 있습니다.

promise

[출처: https://mdn.mozillademos.org]




Promise 상태


  • 초기 상태 : unresolved 또는 Pending, 성공도 실패도 아닌 초기 상태

  • 성공(또는 해결) 상태 : has-resolution 또는 Fulfilled, 성공(resolve) 했을 때의 상태

  • 실패(또는 거부) 상태 : has-rejection 또는 Rejected, 실패(reject) 했을 때의 상태

  • 불편 상태 : settled, 성공 또는 실패 했을 때의 상태 (pending 과 settled 는 서로 대응 하는 관계)

  • promise는 항상 아래 3가지 상태 중(상호배타적인) 1가지 상태입니다.

    • 대기(pending) : 아직 결과 처리가 안 됐다
    • 완료(Fulfilled) : 성공적으로 완료되었다.
    • 거절(rejected) : 처리되는 동안 실패가 발생하였다.

Promise 객체는 pending 상태로 시작해 연산이 끝난 뒤 완료(fulfilled) 혹은 거절(rejected) 상태가 되면 다시는 변화하지 않습니다. 그래서 Fulfilled 및 Rejected 상태를 Settled(불변) 상태라고 합니다.
즉, Event 리스너와는 다르게 then()으로 등록한 콜백 함수는 한 번만 호출됩니다.


Promise 사용하기


  1. new Promise(fn) 으로 promise 객체를 생성합니다.
  2. fn 에는 비동기 처리를 작성합니다.
    • 처리 결과가 정상이라면 resolve(결과 값)을 호출합니다.
    • 처리 결과가 비정상이라면 reject(error)을 호출합니다.

// 예제 1
const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('success'), 1000);
});

promise1
  .then(value => console.log(value))
  .catch(error => console.log(error))
  .finally(() => console.log('finally'));
  
// resolve() 가 호출됐을 때,객체 상태가 Fulfilled 됐음을 뜻하며 onFulfilled 가 호출됩니다.  
// finally() 메서드는 결과에 관계없이 promise가 처리되면 
// 무언가를 프로세싱 또는 정리를 수행하려는 경우에 유용합니다.
  
  
// 예제 2  
const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('failed')), 1000);
});

// promise에서 추상화하고 있는 로직은 기본적으로 try-catch 되는 것과 같으므로 
// 처리 중 throw가 발생해도 프로그램은 종료되지 않고 promise 객체의 상태가 Rejceted 로 됩니다.
// 따라서 throw new error를 작성해도 되지만 promise 객체의 상태를 Rejected 하고자 할 때는 
// reject() 를 사용하는 것이 일반적입니다.


promise2
  .then(value => console.log(value))
  .catch(error => console.log(error));
// reject() 가 호출됐을 때,객체 상태가 Rejected 됐음을 뜻하며 onRejected 가 호출됩니다. 

Promise 객체는 몇 가지 인스턴스 메서드를 가지고 있습니다. 이 메서드를 이용해 promise 객체 상태가 변화할 때 한 번만 호출될 콜백 함수를 등록할 수 있습니다. promise 가 비동기 함수(일회성 결과)에 유용한 점은, promise 상태가 한 번 확정되면 더이상 변하지 않게 된다는 점입니다.


게다가 promise 가 확정(settled)되기 전이나 후에 then()을 호출 했는지는 중요하지 않기 때문에 어떤 경쟁상태(race condition)도 존재하지 않습니다. 전자의 경우, promise 상태가 확정(settled)되는 대로 바로 적절한 반응(reaction)이 호출됩니다. 후자의 경우, promise 결과(fulfillment 또는 rejection 값)가 캐시 되어, 적절하게 원하는 타이밍에 즉시 then() 다룰 수 있게 해줍니다. (task 로 큐에 저장)


// 예제 1
const promise1 = new Promise(resolve => resolve('success'))
  .then(value => console.log(value));
   
// 위의 코드는 아래와 같이 단축해서 표기 할 수 있습니다.
Promise.resolve('success').then(value => console.log(value));
  
  
// 예제 2  
const promise2 = new Promise((_, reject) => reject(new Error('failed')))
  .catch(value => console.log(value));

// 위의 코드는 아래와 같이 단축해서 표기 할 수 있습니다.  
Promise.reject(new Error('failed')).catch(value => console.log(value));  

앞의 코드는 객체 초기화 시 resolve('success') 하기 때문에 ‘success’ 라는 값과 함께 then() 을 이용해 등록한 콜백 함수가 바로 호출됩니다. Promise.resolve() 역시 Fulfilled 상태인 promise 객체를 반환하므로 다음과 같이 작성할 수 있습니다. 때에 따라 promise.resolve()new promise()는 같은 의미를 가지며 promise 객체를 초기화할 때나 테스트 코드를 작성할 때 활용할 수 있습니다.

  • then() 을 가진 유사 promise 객체를 thenable 한 객체라고 합니다.
    • 가장 대표적인 tenable 객체는 jQuery.ajax()가 반환하는 객체 입니다.
    • jQuery.ajax() 는 jqXHR 을 반환하는데 이 객체는 then() 을 갖고 있습니다.
  • Promise.resolve() 는 thenable 객체를 promise 객체로 변환 할 수 있습니다.



Promise 특징


항상 비동기로 처리되는 Promise


const promise = new Promise(resolve => {
  console.log('inner promise');
  resolve('success');
});

promise.then(value => console.log(value));

console.log('outer promise'); 

실행 결과 :
'inner promise'
'outer promise'
'success'

Promise.resolve() 나 resolve() 를 사용하면 promise 객체는 바로 Fulfilled 상태가 되므로 then() 으로 등록한 콜백 함수도 동기적으로 호출될 것이라 예상할 수도 있습니다. 하지만 then()으로 등록한 콜백 함수는 비동기적으로 호출된다.

Promise 는 항상 비동기로 처리됩니다. 그 이유는 아래와 같습니다.

동기와 비동기 혼재의 위험성


const onReady = (fn) => {
  const readyState = document.readyState;

  if (readyState == 'interactive' || readyState === 'complete') {
    fn();
  } else {
    window.addEventListener('DOMContentLoaded', fn);
  }
}

onReady(() => {
  console.log('DOM fully loaded and parsed');
});

console.log('Starting...');

실행 결과 :
'DOM fully loaded and parsed'
'Starting...'

[비동기 콜백을 절대 동기적으로 호출하지 마라] - ‘Effective javascript item 67’

  1. 데이터를 즉시 사용할 수 있더라도, 절대로 비동기 콜백을 동기적으로 호출하지 마라.
  2. 비동기 콜백을 동기적으로 호출하면 기대한 연산의 순서를 방해하고, 예상치 않은 코드의 간섭을 초래할수 있다.
  3. 비동기 콜백을 동기적으로 호출하면 스택 오버플로우나 처리되지 않는 예외를 초래할 수 있다.
  4. 비동기 콜백을 다른 턴에 실행되도록 스케줄링 하기 위해 setTimeout 같은 비동기 API 를 사용하라.


해결 방안 1


const onReady = (fn) => {
  const readyState = document.readyState;

  if (readyState == 'interactive' || readyState === 'complete') {
    setTimeout(fn, 0);
  } else {
    window.addEventListener('DOMContentLoaded', fn);
  }
}

onReady(() => {
  console.log('DOM fully loaded and parsed');
});

console.log('Starting...');

실행 결과 :
'Starting...'
'DOM fully loaded and parsed'


해결 방안 2


const onReadyPromise = () => new Promise(resolve => {
  const readyState = document.readyState;

  if (readyState == 'interactive' || readyState === 'complete') {
    resolve();
  } else {
    window.addEventListener('DOMContentLoaded', resolve);
  }
})


onReadyPromise().then(() => {
  console.log('DOM fully loaded and parsed');
});

console.log('Starting...');

실행 결과 :
'Starting...'
'DOM fully loaded and parsed'


이처럼 동기와 비동기를 혼재했을 때 발생하는 문제를 예방하기 위해 항상 비동기로 처리하도록 ES6 Promise 사양이 정해진 것입니다. Promise 를 사용하면 항상 비동기로 처리되기 때문에 명시적으로 비동기 처리를 위한 코드를 추가로 작성할 필요가 없습니다.


Reference