프로그래밍/NODEJS 강좌 BY GEMINI

프로미스(Promises)로 우아하게 비동기 처리하기 ✨

lazy_web_devloper 2025. 6. 9. 12:41
728x90
반응형

프로미스(Promises)로 우아하게 비동기 처리하기 ✨

프로미스(Promise)는 ES6(ECMAScript 2015)에 도입된 객체로, 비동기 작업의 최종 완료(또는 실패)와 그 결과 값을 나타냅니다. 이름 그대로, 지금 당장은 결과를 알 수 없지만 "나중에 결과를 알려주겠다"는 '약속'을 하는 객체라고 생각할 수 있습니다.

프로미스를 사용하면 콜백의 중첩 구조를 벗어나, 보다 순차적이고 가독성 높은 코드를 작성할 수 있습니다.

1. 프로미스의 세 가지 상태 (States)

모든 프로미스 객체는 다음 세 가지 상태 중 하나를 가집니다.

  • pending (대기): 비동기 작업이 아직 완료되지 않은 초기 상태. 프로미스가 생성되고 아직 성공도, 실패도 하지 않은 상태입니다.
  • fulfilled (이행/성공): 비동기 작업이 성공적으로 완료된 상태. 이때 프로미스는 결과 값을 가집니다. (이 상태를 resolved 라고 부르기도 합니다.)
  • rejected (거부/실패): 비동기 작업이 실패한 상태. 이때 프로미스는 실패 이유(에러 객체)를 가집니다.

프로미스는 한 번 fulfilled 또는 rejected 상태가 되면, 그 상태와 값은 절대 변하지 않습니다 (불변성). 즉, '약속'은 한 번만 지켜집니다.

2. 프로미스 생성하기

프로미스는 new Promise() 생성자를 통해 만들 수 있습니다. 이 생성자는 executor 라는 함수를 인자로 받으며, executor 함수는 다시 resolve와 reject라는 두 개의 함수를 인자로 받습니다.

JavaScript
 
const myPromise = new Promise((resolve, reject) => {
  // 비동기 작업을 수행하는 로직 (예: setTimeout, fs.readFile, DB 조회 등)
  console.log('프로미스 내부에서 비동기 작업 시작...');

  setTimeout(() => {
    const success = true; // 작업 성공 여부를 가정

    if (success) {
      // 작업 성공 시, resolve 함수를 호출하여 결과를 전달합니다.
      // 그러면 프로미스는 'fulfilled' 상태가 됩니다.
      resolve('작업 성공! 결과 데이터입니다.');
    } else {
      // 작업 실패 시, reject 함수를 호출하여 에러를 전달합니다.
      // 그러면 프로미스는 'rejected' 상태가 됩니다.
      reject(new Error('작업 실패! 문제가 발생했습니다.'));
    }
  }, 2000); // 2초 후 작업이 완료된다고 가정
});

3. 프로미스 사용하기 (.then(), .catch(), .finally())

생성된 프로미스의 최종 결과를 처리하기 위해 다음과 같은 메소드를 사용합니다.

  • .then(onFulfilled, onRejected): 프로미스가 fulfilled 상태가 되면 onFulfilled 함수가 호출되고, rejected 상태가 되면 onRejected 함수(선택 사항)가 호출됩니다.
    • onFulfilled: 프로미스가 성공했을 때 실행될 함수. resolve가 전달한 결과 값을 인자로 받습니다.
    • onRejected: 프로미스가 실패했을 때 실행될 함수. reject가 전달한 에러 객체를 인자로 받습니다.
  • .catch(onRejected): 프로미스가 rejected 상태가 되었을 때 실행될 함수를 등록합니다. .then(null, onRejected)와 동일한 역할을 하는 문법적 설탕(syntactic sugar)입니다. 에러 처리를 더 명확하게 표현할 수 있어 널리 사용됩니다.
  • .finally(onFinally): 프로미스가 성공하든 실패하든 상관없이, 작업이 완료되면 항상 실행될 함수를 등록합니다. 로딩 스피너 숨기기, 데이터베이스 연결 종료 등 마무리 작업에 유용합니다.

사용 예시:

JavaScript
 
console.log('프로미스 사용 시작');

myPromise
  .then((successMessage) => {
    // onFulfilled: 프로미스가 성공했을 때 실행됨
    console.log('성공:', successMessage);
  })
  .catch((errorMessage) => {
    // onRejected: 프로미스가 실패했을 때 실행됨
    console.error('실패:', errorMessage);
  })
  .finally(() => {
    // onFinally: 성공/실패 여부와 관계없이 항상 실행됨
    console.log('프로미스 작업 완료.');
  });

4. 프로미스 체이닝 (Promise Chaining)

프로미스의 가장 강력한 기능 중 하나는 체이닝(chaining) 입니다. .then() 메소드는 항상 새로운 프로미스를 반환하기 때문에, 여러 개의 비동기 작업을 순차적으로 연결하여 처리할 수 있습니다. 이를 통해 콜백 지옥을 해결할 수 있습니다.

  • .then()의 콜백 함수에서 일반 값을 반환하면, 그 값은 fulfilled 상태인 새로운 프로미스로 감싸져 반환됩니다.
  • .then()의 콜백 함수에서 또 다른 프로미스를 반환하면, 체인의 다음 .then()은 해당 프로미스가 완료될 때까지 기다렸다가 그 결과를 받아서 실행됩니다.

콜백 지옥 코드를 프로미스 체이닝으로 바꾸기:

JavaScript
 
// 콜백 지옥 예시 (다시 보기)
비동기작업1((err1, data1) => {
  if (err1) { /* 에러 처리 */ }
  비동기작업2(data1, (err2, data2) => {
    if (err2) { /* 에러 처리 */ }
    비동기작업3(data2, (err3, data3) => {
      // ...
    });
  });
});

// 프로미스 체이닝으로 개선한 예시
// (비동기작업1, 2, 3이 프로미스를 반환한다고 가정)
비동기작업1()
  .then(data1 => {
    console.log('작업1 결과:', data1);
    return 비동기작업2(data1); // 다음 작업을 위해 새로운 프로미스를 반환
  })
  .then(data2 => {
    console.log('작업2 결과:', data2);
    return 비동기작업3(data2);
  })
  .then(data3 => {
    console.log('작업3 결과:', data3);
    console.log('모든 작업 성공!');
  })
  .catch(err => {
    // 체인 중간 어디에서든 에러가 발생하면 여기서 잡힙니다.
    console.error('체인 중 에러 발생:', err);
  });

보시다시피, 코드가 중첩되지 않고 위에서 아래로 흐르는 선형적인 구조가 되어 훨씬 읽기 쉽고 관리하기 편해졌습니다. 또한, .catch()를 마지막에 한 번만 사용하여 체인 전체의 에러를 한 곳에서 처리할 수 있다는 큰 장점이 있습니다.

Java 개발자를 위한 비유:

Java 8 이상에서 도입된 CompletableFuture는 Node.js의 프로미스와 매우 유사한 개념입니다. 비동기 작업의 결과를 캡슐화하고, thenApply(), thenAccept(), thenCompose(), exceptionally() 등의 메소드를 사용하여 연쇄적인 처리를 정의할 수 있다는 점에서 공통점이 많습니다. CompletableFuture에 익숙하시다면 프로미스의 개념을 쉽게 이해하실 수 있을 겁니다.

Promise 유틸리티 메소드:

  • Promise.all(iterable): 여러 개의 프로미스를 배열(iterable)로 받아, 모든 프로미스가 fulfilled 될 때까지 기다립니다. 모든 프로미스가 성공하면 그 결과들을 배열에 담아 fulfilled 상태인 새로운 프로미스를 반환합니다. 하나라도 rejected 되면 즉시 그 에러와 함께 rejected 상태가 됩니다. (여러 개의 API를 동시에 호출하고 모든 응답을 기다릴 때 유용)
  • Promise.race(iterable): 여러 개의 프로미스를 배열로 받아, 그 중 가장 먼저 fulfilled 또는 rejected 되는 프로미스의 결과(또는 에러)를 그대로 따르는 새로운 프로미스를 반환합니다. (타임아웃 처리 등에 활용 가능)
  • Promise.resolve(value): 주어진 값으로 즉시 fulfilled 상태가 되는 프로미스를 생성합니다.
  • Promise.reject(reason): 주어진 이유(에러)로 즉시 rejected 상태가 되는 프로미스를 생성합니다.

프로미스는 비동기 코드를 다루는 방식을 근본적으로 개선했습니다. 콜백의 단점을 극복하고, 에러 처리를 간결하게 만들며, 코드의 가독성을 크게 높여줍니다.

728x90
반응형