Language/Javascript

[Javascript/ES6] 모던 자바스크립트 프로미스 - 정의, 메소드, 체이닝

재은초 2023. 8. 5. 21:57
반응형

프로미스(Promise)란?

  • 자바스크립트는 비동기 처리를 위한 하나의 패턴으로 콜백 함수를 사용한다. 하지만 전통적인 콜백 패턴은 콜백 헬로 인해 가독성이 나쁘고 비동기 처리 중 발생한 에러의 처리가 곤란하며 여러 개의 비동기 처리를 한번에 처리하는 데도 한계가 있다.
  • ES6에서는 비동기 처리를 위한 또 다른 패턴으로 프로미스(Promise)를 도입했다. 프로미스는 전통적인 콜백 패턴이 가진 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.

 

동기식과 비동기식 처리

  • 동기식 처리 모델(Synchronous processing model)은 직렬적으로 태스크(task)를 순서대로 실행하는데, 어떤 작업이 수행 중이면 다음 작업은 블로킹(blocking, 작업 중단) 상태로 대기하게 된다.
  • 비동기식 처리 모델(Asynchronous processing model)은 병렬적으로 태스크를 수행한다. 즉, 태스크가 종료되지 않은 상태라 하더라도 대기하지 않고 다음 태스크를 실행한다. 자바스크립트의 대부분의 DOM 이벤트 핸들러와 Timer 함수(setTimeout, setInterval), Ajax 요청은 비동기식 처리 모델로 동작한다.
  • 보통 '비동기 실행'이 '동기 실행'에 비해, 동일한 작업을 더 빠른 시간 내에 처리할 수 있다.

https://poiemaweb.com/js-async

console.log('Start!');

fetch('https://www.google.com')
  .then((response) => response.text())
  .then((result) => { console.log(result); });

console.log('End');

 

기존 콜백 패턴의 단점

  • 어떤 조건이 만족되었을 때 실행되는 함수를 콜백 함수라고 한다.

콜백 헬

  • 자바스크립트에서 빈번하게 사용되는 비동기식 처리 모델은 요청을 병렬로 처리하여 작업이 중단되지 않고 계속 진행되는 장점이 있다. 하지만 비동기 처리를 위해 콜백 패턴을 사용하면 처리 순서를 보장하기 위해 여러 개의 콜백 함수가 중첩되는데, 이 때 복잡도가 높아지는 콜백 헬(Callback Hell)이 발생하는 단점이 있다.
  • 비동기 함수의 처리 결과를 가지고 다른 비동기 함수를 호출해야 하는 경우, 함수의 호출이 중첩(nesting)이 되어 복잡도가 높아지는 현상이 발생하는데 이를 Callback Hell이라 한다.
  • Callback Hell은 코드의 가독성을 나쁘게 하고 복잡도를 증가시켜 실수를 유발하는 원인이 되며 에러 처리가 곤란하다.
step1(function(value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        step5(value4, function(value5) {
            // value5를 사용하는 처리
        });
      });
    });
  });
});

에러 처리의 한계

try {
  setTimeout(() => { throw new Error('Error!'); }, 1000);
} catch (e) {
  console.log('에러를 캐치하지 못한다..');
  console.log(e);
}
  • try 블록 내에서 setTimeout 함수가 실행되면 1초 후에 콜백 함수가 실행되고 이 콜백 함수는 예외를 발생시킨다. 하지만 이 예외는 catch 블록에서 캐치되지 않는다.
  • 프로미스 객체 이전에는 비동기 처리로 콜백 패턴을 주로 사용했었지만 콜백 지옥(Callback Hell)으로 인해 가독성도 나쁘고 비동기 처리 중에 발생한 에러의 처리가 까다로웠다. Promise는 이러한 단점을 보완하기 위해 나온 대안으로 비동기를 간편하게 처리할 수 있게 도와주는 객체다.

 

프로미스 객체 생성

  • 프로미스 객체는 생성자를 사용해서 만들 수 있으며, 생성자의 인자로 executor라는 함수를 이용한다.
  • Promise 생성자 함수는 비동기 작업을 수행할 콜백 함수를 인자로 전달받는데 이 콜백 함수는 resolve와 reject 함수를 인자로 전달받는다.
new Promise( /*executor*/ (resolve, reject) => {
  // pending
  // ...비동기 처리
  // resolve(); // fulfilled
  // reject();  // rejected
} );

프로미스 객체의 상태값

  • 생성자를 통해 프로미스 객체를 만드는 순간 pending 상태가 된다. 이때 비동기 처리가 성공하면 콜백 함수의 인자로 전달받은 resolve 함수를 호출하고 이 때 프로미스는 fulfilled 상태가 된다. 비동기 처리가 실패하면 reject 함수를 호출하고 이 때 프로미스는 rejected 상태가 된다.

https://poiemaweb.com/es6-promise

 

프로미스 후속 처리 메소드

  • Promise로 구현된 비동기 함수는 Promise 객체를 반환하여야 한다. Promise로 구현된 비동기 함수를 호출하는 측에서는 Promise 객체의 후속 처리 메소드(then, catch)를 통해 비동기 처리 결과 또는 에러 메시지를 전달받아 처리한다. Promise 객체는 상태를 갖는다고 하였다. 이 상태에 따라 후속 처리 메소드를 체이닝 방식으로 호출한다.

then

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

catch

  • 예외(비동기 처리에서 발생한 에러와 then 메소드에서 발생한 에러)가 발생하면 호출된다. catch 메소드는 Promise를 반환한다.
fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => { console.log(result); })
  .catch((error) => { console.log(error); })    // catch 에러날 때만 실행
  .finally(() => { console.log('exit'); })      // finally 성공이나 실패 상관없이 항상 실행

 

Promise Chaining

  • 여러개의 프로미스 객체를 연결하는 것을 프로미스 체이닝(Promise Chaining)이라고 한다.
  • 비동기 함수의 처리 결과를 가지고 다른 비동기 함수를 호출해야 하는 경우, 함수의 호출이 중첩(nesting)이 되어 복잡도가 높아지는 콜백 헬이 발생한다. 프로미스는 후속 처리 메소드를 체이닝(chainning)하여 여러 개의 프로미스를 연결하여 사용할 수 있다. 이로써 콜백 헬을 해결한다.
  • Promise 객체를 반환한 비동기 함수는 프로미스 후속 처리 메소드인 then이나 catch 메소드를 사용할 수 있다. 따라서 then 메소드가 Promise 객체를 반환하도록 하면(then 메소드는 기본적으로 Promise를 반환한다.) 여러 개의 프로미스를 연결하여 사용할 수 있다.
  • 프로미스 체인(Promise Chaining)은 비동기 작업을 순차적으로 수행해야할 때 전체 코드를 깔끔하게 나타내야할 때 사용한다.
console.log('Start');

// Promise 객체의 then 메소드로 여러 개의 프로미스 연결
fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.text())
  .then((result) => {
    const users = JSON.parse(result);
    return users[0];
  })
  .then((user) => {
    console.log(user);
    const { address } = user;
    return address;
  })
  .then((address) => {
    console.log(address);
    const { geo } = address;
    return geo;
  })
  .then((geo) => {
    console.log(geo);
    const { lat } = geo;
    return lat;
  })
  .then((lat) => {
    console.log(lat);
  })

 

프로미스 정적 메소드

  • Promise는 주로 생성자 함수로 사용되지만 함수도 객체이므로 메소드를 갖을 수 있다.

Promise.resolve

  • Promise.resolve 메소드는 인자로 전달된 값을 resolve하는 Promise를 생성한다.
const resolvedPromise = Promise.resolve([1, 2, 3]);
resolvedPromise.then(console.log); // [ 1, 2, 3 ]

Promise.reject

  • Promise.reject 메소드는 인자로 전달된 값을 reject하는 프로미스를 생성한다.
const rejectedPromise = Promise.reject(new Error('Error!'));
rejectedPromise.catch(console.log); // Error: Error!

Promise.all

  • Promise.all 메소드는 프로미스가 담겨 있는 배열 등의 이터러블을 인자로 전달 받는다. 그리고 전달받은 모든 프로미스를 병렬로 처리하고 그 처리 결과를 resolve하는 새로운 프로미스를 반환한다.
Promise.all([
  // 첫번째 프로미스는 3초 후에 1을 resolve하여 처리 결과를 반환한다
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  
  // 두번째 프로미스는 2초 후에 2을 resolve하여 처리 결과를 반환한다
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  
  // 세번째 프로미스는 1초 후에 3을 resolve하여 처리 결과를 반환한다
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(console.log) // [ 1, 2, 3 ]
  .catch(console.log);

Promise.race

  • Promise.race 메소드는 Promise.all 메소드와 동일하게 프로미스가 담겨 있는 배열 등의 이터러블을 인자로 전달 받는다. 그리고 Promise.race 메소드는 Promise.all 메소드처럼 모든 프로미스를 병렬 처리하는 것이 아니라 가장 먼저 처리된 프로미스가 resolve한 처리 결과를 resolve하는 새로운 프로미스를 반환한다.
  • 에러가 발생한 경우는 Promise.all 메소드와 동일하게 처리된다. 즉, Promise.race 메소드에 전달된 프로미스 처리가 하나라도 실패하면 가장 먼저 실패한 프로미스가 reject한 에러를 reject하는 새로운 프로미스를 즉시 반환한다.
Promise.race([
  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) // 3
  .catch(console.log);

 

Reference

반응형