프로미스(Promises)로 우아하게 비동기 처리하기 ✨
프로미스(Promise)는 ES6(ECMAScript 2015)에 도입된 객체로, 비동기 작업의 최종 완료(또는 실패)와 그 결과 값을 나타냅니다. 이름 그대로, 지금 당장은 결과를 알 수 없지만 "나중에 결과를 알려주겠다"는 '약속'을 하는 객체라고 생각할 수 있습니다.
프로미스를 사용하면 콜백의 중첩 구조를 벗어나, 보다 순차적이고 가독성 높은 코드를 작성할 수 있습니다.
1. 프로미스의 세 가지 상태 (States)
모든 프로미스 객체는 다음 세 가지 상태 중 하나를 가집니다.
- pending (대기): 비동기 작업이 아직 완료되지 않은 초기 상태. 프로미스가 생성되고 아직 성공도, 실패도 하지 않은 상태입니다.
- fulfilled (이행/성공): 비동기 작업이 성공적으로 완료된 상태. 이때 프로미스는 결과 값을 가집니다. (이 상태를 resolved 라고 부르기도 합니다.)
- rejected (거부/실패): 비동기 작업이 실패한 상태. 이때 프로미스는 실패 이유(에러 객체)를 가집니다.
프로미스는 한 번 fulfilled 또는 rejected 상태가 되면, 그 상태와 값은 절대 변하지 않습니다 (불변성). 즉, '약속'은 한 번만 지켜집니다.
2. 프로미스 생성하기
프로미스는 new Promise() 생성자를 통해 만들 수 있습니다. 이 생성자는 executor 라는 함수를 인자로 받으며, executor 함수는 다시 resolve와 reject라는 두 개의 함수를 인자로 받습니다.
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): 프로미스가 성공하든 실패하든 상관없이, 작업이 완료되면 항상 실행될 함수를 등록합니다. 로딩 스피너 숨기기, 데이터베이스 연결 종료 등 마무리 작업에 유용합니다.
사용 예시:
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()은 해당 프로미스가 완료될 때까지 기다렸다가 그 결과를 받아서 실행됩니다.
콜백 지옥 코드를 프로미스 체이닝으로 바꾸기:
// 콜백 지옥 예시 (다시 보기)
비동기작업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 상태가 되는 프로미스를 생성합니다.
프로미스는 비동기 코드를 다루는 방식을 근본적으로 개선했습니다. 콜백의 단점을 극복하고, 에러 처리를 간결하게 만들며, 코드의 가독성을 크게 높여줍니다.
'프로그래밍 > NODEJS 강좌 BY GEMINI' 카테고리의 다른 글
| 5️⃣ Express.js로 강력한 웹 애플리케이션 구축하기 🌐 (1) | 2025.06.10 |
|---|---|
| Async/Await: 동기 코드처럼 비동기 코드 작성하기 MAGIC ✨ (1) | 2025.06.10 |
| 콜백 함수 (Callbacks)와 콜백 지옥 (Callback Hell) 😫 (1) | 2025.06.05 |
| 4️⃣ 비동기 프로그래밍 마스터하기: Node.js의 심장 ⚡ (1) | 2025.06.05 |
| NPM (Node Package Manager) 심층 분석 - 전역 vs. 로컬 패키지 (0) | 2025.06.04 |