본문 바로가기

Frontend Dev/JavaScript

ES6, 비동기 처리를 위한 프로미스 (Promise)

반응형

 자바스크립트는 비동기 처리를 위한 하나의 패턴으로 콜백 함수를 사용한다. 하지만 전통적인 콜백 패턴은 콜백 헬로 인해 가독성이 나쁘고 비동기 처리 중 발생한 에러의 처리가 곤란하며 여러 개의 비동기 처리를 한번에 처리하는 데도 한계가 있다.

 ES6에서는 비동기 처리를 위한 또 다른 패턴으로 프로미스(Promise)를 도입했다. 프로미스는 전통적인 콜백 패턴이 가진 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.

 

👾 Promise

 Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체로, 기본적으로 Promise는 함수에 콜백을 전달하는 대신에, 콜백을 첨부하는 방식의 객체이다.

 프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있다. 다만 최종 결과를 반환하는 것이 아니고, 미래의 어떤 시점에 결과를 제공하겠다는 '약속'(프로미스)을 반환한다.

 

const promise = new Promise((resolve, reject) => {
  // executor 비동기 작업 수행

  resolve(value) // 비동기 작업 성공시
  reject(error)  // 비동기 작업 실패시
});

 • Promise는 class이기 때문에 new키워드를 통해 Promise객체를 생성한다.

 • Promise는 비동기 처리를 수행할 콜백 함수 executor를 인수로 전달받고, 이 콜백 함수는 resolve reject 함수를 인수로 전달받는다.

 • Promise 객체가 생성되면 executor는 자동으로 실행되고 작성했던 코드들이 작동된다.

 • 코드가 정상적으로 처리가 되었다면 resolve 함수를 호출하고 에러가 발생했을 경우에는 reject 함수를 호출한다.

 

 

📌 new Promise가 반환하는 Promise 객체는 state, result 내부 프로퍼티를 가진다. 하지만 직접 접근할 수는 없고, then catch finally의 메서드를 사용해야 접근이 가능하다.

 

✔️ state

 Promise는 비동기 처리가 성공(fulfilled)하였는지 또는 실패(rejected)하였는지 등의 상태(state) 정보를 갖는다.

상태 (state) 의미 구현
pending (대기) 비동기 처리가 아직 수행되지 않은 상태 resolve 또는 reject 함수가 아직 호출되지 않은 상태
fulfilled (이행) 비동기 처리가 수행된 상태 (성공) resolve 함수가 호출된 상태
rejected (거부) 비동기 처리가 수행된 상태 (실패) reject 함수가 호출된 상태
settled 비동기 처리가 수행된 상태 (성공 또는 실패) resolve 또는 reject 함수가 호출된 상태

 Promise 생성자 함수가 인자로 전달받은 콜백 함수는 내부에서 비동기 처리 작업을 수행한다.

 이때 비동기 처리가 성공하면 콜백 함수의 인자로 전달받은 resolve 함수를 호출하고, 프로미스는 ‘fulfilled’ 상태가 된다. 비동기 처리가 실패하면 reject 함수를 호출하고, 프로미스는 ‘rejected’ 상태가 된다.

 

✔️ result

 처음에는 undefined 상태로, 비동기 처리를 수행할 콜백 함수 executor가 성공적으로 작동하여 resolve(value)가 호출되면 value로, 에러가 발생하여 reject(error)가 호출되면 error로 변한다.

 

 

👾 Promise 인스턴스 메서드 (프로미스의 후속 처리)

 Promise로 구현된 비동기 함수는 Promise 객체를 반환한다. Promise로 구현된 비동기 함수를 호출하는 측(promise consumer)에서는 Promise 객체의 후속 처리 메소드(then, catch, finally)를 통해 비동기 처리 결과 또는 에러 메시지를 전달받아 처리한다.

 

const promise = new Promise((resolve, reject) => {
  // executor 비동기 작업 수행

  resolve(value) // 비동기 작업 성공시 -> then 실행
  reject(error)  // 비동기 작업 실패시 -> catch 실행
});
  
promise.then(() => {
  // resolve가 호출되면 실행
})
.then(() => {
  // 그다음에 실행할 것
})
.catch(() => {
  // reject가 호출되면 실행
})
.finally(() => {
  // 콜백 작업을 마치면 무조건 실행 (생략가능)
})

 

✔️ then

 then 메서드는 Promise를 리턴하고 두 개의 콜백 함수를 인자로 전달 받는다. 첫 번째 콜백 함수는 성공(fulfilled, resolve 함수가 호출된 상태) 시 호출되고, 두 번째 함수는 실패(rejected, reject 함수가 호출된 상태) 시 호출된다.

 

promise.then(
  function(result) { /* resolve 함수가 호출된 상태 */ },
  function(error) { /* reject 함수가 호출된 상태 */ }
);

 • then 메서드는 Promise를 반환한다. 

 • 작업이 성공적으로 처리된 경우만 다루고 싶다면 then에 인수를 하나만 전달하면 된다.

 • 에러만 발생한 경우만 다루고 싶다면 then(null, errorHandlingFunction) 같이 null을 첫 번째 인수로 전달하면 된다. → catch와 완전히 동일하게 동작

 

✔️ catch

executor에 작성했던 코드들이 에러가 발생했을 경우에는 reject 함수를 호출하고 catch 메서드로 접근할 수 있다.

 

let promise = new Promise(function(resolve, reject) {
  reject(new Error("에러"))
});

promise.catch(error => {
  console.log(error); // Error: 에러
})

 • catch 메서드는 Promise를 반환한다.

 • catch 메서드를 호출하면 내부적으로 then(undefined, onRejected)을 호출한다.

 • catch 메서드를 모든 then 메서드를 호출한 이후에 호출하면 비동기 처리에서 발생한 에러(reject 함수가 호출된 상태)뿐만 아니라 then 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있다.

 또한 then 메서드에 두 번째 콜백 함수를 전달하는 것보다 catch 메서드를 사용하는 것이 가독성이 좋고 명확하다.

 

✔️ finally

 finally 메서드는 Promise가 처리되면 충족되거나 거부되는지 여부에 관계없이 지정된 콜백 함수가 무조건 한 번은 실행된다. 결과에 관계없이 promise가 처리되면 무언가를 프로세싱 또는 정리를 수행하려는 경우에 유용하다.

let promise = new Promise(function(resolve, reject) {
  resolve("성공");
});

promise
.then(value => {
  console.log(value); // (resolve) "성공" 출력
})
.catch(error => {
  console.log(error); // (reject)
})
.finally(() => {
  console.log("무조건 작동"); // "무조건 작동"
})

 • finally 메서드는 Promise를 반환한다.

 

 

👾 Promise 정적 메서드, Promise.all

 Promise.all()은 여러 개의 비동기 작업을 동시에 처리하고 싶을 때 사용한다.

  해당 배열에 있는 모든 Promise에서 executor 내 작성했던 코드들이 정상적으로 처리가 되었다면, 결과를 배열에 저장해 새로운 Promise를 반환한다. 배열 요소의 순서는 매개변수에 지정한 프로미스의 순서를 유지한다.

 만약 반환하는 프로미스가 거부된다면, 매개변수의 프로미스 중 거부된 첫 프로미스의 사유를 그대로 사용한다.

 

 • Promise.all() 메서드는 전달 받은 이터러블의 요소가 프로미스가 아닌 경우, Promise.resolve 메서드를 통해 프로미스로 래핑된다.

Promise.all([
  1, // => Promise.resolve(1)
  2, // => Promise.resolve(2)
  3  // => Promise.resolve(3)
])
.then(console.log) // [1, 2, 3]
.catch(console.log);

 

👩🏻‍💻 example. Promise.all 동작 순서

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
])
.then(console.log) // [ 1, 2, 3 ]
.catch(console.log);
Promise.all([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 1!')), 3000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 2!')), 2000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 3!')), 1000))
])
.then(console.log)
.catch(console.log); // Error: Error 3!

 

  1. 첫번째 프로미스는 3초 후에 1을 resolve하여 처리 결과를 반환
  2. 두번째 프로미스는 2초 후에 2을 resolve하여 처리 결과를 반환
  3. 세번째 프로미스는 1초 후에 3을 resolve하여 처리 결과를 반환
  4. Promise.all 메서드는 전달받은 모든 프로미스를 병렬로 처리한다.
    → 이때 모든 프로미스의 처리가 종료될 때까지 기다린 후 아래와 모든 처리 결과를 resolve 또는 reject
  5. 모든 프로미스의 처리가 성공하면 각각의 프로미스가 resolve한 처리 결과를 배열에 담아 resolve하는 새로운 프로미스를 반환한다. → 이때 첫번째 프로미스가 가장 나중에 처리되어도 Promise.all 메서드가 반환하는 프로미스는 첫번째 프로미스가 resolve한 처리 결과부터 차례대로 배열에 담아 그 배열을 resolve하는 새로운 프로미스를 반환한다. 즉, 처리 순서가 보장된다.
  6. 만약 프로미스의 처리가 하나라도 실패하면 가장 먼저 실패한 프로미스가 reject한 에러를 reject하는 새로운 프로미스를 즉시 반환한다.

 

👾 Promise chaining

 비동기 함수의 처리 결과를 가지고 다른 비동기 함수를 호출해야 하는 경우, 함수의 호출이 중첩(nesting)이 되어 복잡도가 높아지는 콜백 헬이 발생한다. 프로미스는 후속 처리 메소드를 체이닝(chainning)하여 여러 개의 프로미스를 연결하여 사용할 수 있다. 이로써 콜백 헬을 해결한다.

 

  Promise chaining가 필요한 경우는 비동기 작업을 순차적으로 진행해야 하는 경우이다. then catch finally 의 메서드들은 Promise를 리턴하기 때문에 Promise chaining이 가능하다.

 

👩🏻‍💻 example

let promise = new Promise(function (resolve, reject) {
  resolve('성공');
  // reject("실패");
});

promise
.then((value) => {
  console.log(value);
  return '성공1';
})
.then((value) => {
  console.log(value);
  return '성공2';
})
.catch((error) => {
  console.log(error);
  return '실패';
})
.finally(() => {
  console.log('성공이든 실패든 작동!');
});

 

 

👾 Promise hell

 Promise를 통해 비동기 코드의 순서를 제어할 수 있지만 Callback 함수와 같이 코드가 길어질수록 복잡해지고 가독성이 낮아지는 Promise Hell이 발생하는 단점이 있다.

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(string);
    }, Math.floor(Math.random() * 100) + 1);
  });
};
  
const printAll = () => {
  printString('A').then((value) => {
    console.log(value);

    printString('B').then((value) => {
        console.log(value);

        printString('C').then((value) => {
            console.log(value);

            printString('D').then((value) => {
                console.log(value);
            });
        });
    });
  });
};
  
printAll();

 

 ES8에서 async/await 키워드가 제공됨으로써 복잡한 Promise 코드를 간결하게 작성할 수 있게 되었다.

 


참고자료: poiemaweb, MDN Promise

✏️ 공부하며 정리한 내용입니다. 잘못된 정보나 더 공유할 내용이 있으면 댓글로 알려주세요!

읽어주셔서 감사합니다 😊

반응형