[JS] 프로미스 (Promise)
01
Promise
자바스크립트에서 비동기 작업이 끝날 때, 그 결과를 알려주는 "약속"과 같은 역할의 오브젝트입니다.
예를 들어, 어떤 일이 잘 처리되면 결과를 알려주고, 문제가 생기면 에러를 알려주는 방식이죠.
1. Promise의 흐름: 생성, 사용, 결과 처리
✅ Producing Code (프로듀서 코드)
: Promise를 생성하는 코드입니다.
즉, 우리가 약속을 만들고 그 약속이 끝날 때까지 기다리는 코드라고 할 수 있습니다.
이 코드는 어떤 일이 완료되기를 기다리고, 완료되면 결과를 알려줄 resolve 또는 에러를 알려줄 reject를 호출합니다.
✅ Consuming Code (컨슘 코드)
: Consuming Code는 Promise를 사용하는 코드입니다.
즉, Promise가 완료될 때까지 기다리고, 그 결과(성공 또는 실패)를 처리하는 코드입니다.
✅ Promise
: 위에서 말한 것처럼 "약속"과 같아서, 어떤 일이 끝날 때까지 기다리며 그 결과를 전달합니다.
Producing Code에서 Promise를 생성하고, Consuming Code에서 그 결과를 처리하는 방식입니다.
✅ Promise는 세 가지 상태를 가질 수 있습니다:
- Pending: 아직 완료되지 않은 상태 (약속을 했지만 결과를 아직 모름)
- Resolved (Fulfilled): 일이 성공적으로 완료된 상태
- Rejected: 일이 실패한 상태
2. Promise 문법
new Promise는 executor라는 함수를 인자로 받습니다.
이 함수는 작업이 성공적으로 끝나면 resolve(value)를 호출하고, 실패하면 reject(error)를 호출합니다.
let promise = new Promise(function(resolve, reject) {
// executor
});
한편, new Promise 생성자가 반환하는 promise 객체는 다음과 같은 내부 프로퍼티를 갖습니다.
- state: 'pending' → 'fulfilled' (성공 시) 또는 'rejected' (실패 시)
- result: 'undefined' → 'value' (성공 시) 또는 'error' (실패 시)
3. Promise 예시
✅ promise 생성자와 간단한 executor 함수로 만든 성공예시입니다.
let promise = new Promise(function(resolve, reject) {
// 프라미스가 만들어지면 executor 함수는 자동으로 실행됩니다.
// 1초 뒤에 일이 성공적으로 끝났다는 신호가 전달되면서 result는 '완료'가 됩니다.
setTimeout(() => resolve("완료"), 1000);
});
📂 설명:
- state: "pending" → "fulfilled"
- result: undefined → "완료"
✅ 이번에는 executor가 에러와 함께 약속한 작업을 거부하는 예시입니다.
let promise = new Promise(function(resolve, reject) {
// 1초 뒤에 에러와 함께 실행이 종료되었다는 신호를 보냅니다.
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});
📂 설명:
- 1초 후 reject(...)가 호출되면 promise의 상태가 "rejected"로 변합니다.
✅ 이전 포스팅해서 다뤘던 Callback 함수를 Promise로 바꿔보겠습니다
//Callback
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`${src}를 불러오는 도중에 에러가 발생함`));
document.head.append(script);
});
}
//promise
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => alert(`${script.src}을 불러왔습니다!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('또다른 핸들러...'));
02
Consumers: then, catch, finally
Promise의 소비자(Consumer) 함수들은. then,. catch,. finally 메서드를 사용해 구독될 수 있습니다.
1. then
then은 Promise에서 가장 중요한 메서드로,
Promise가 해결되었을 때 결과를 다루거나, 에러가 발생했을 때 처리할 수 있습니다.
promise.then(
function(result) { /* 결과(result)를 다룹니다 */ },
function(error) { /* 에러(error)를 다룹니다 */ }
);
2. catch
에러만 다루고 싶다면. catch()를 사용합니다.. catch(f)는. then(null, f)와 동일하게 작동합니다.
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});
promise.catch(alert); // 1초 뒤 "Error: 에러 발생!" 출력
3. finally
finally는 try... catch에서 사용되던 것처럼, 결과와 상관없이 항상 마지막에 실행되는 코드입니다.
주로 로딩 인디케이터와 같은 정리 작업을 수행할 때 사용됩니다.
new Promise((resolve, reject) => {
// 시간이 걸리는 작업을 수행한 뒤 resolve 또는 reject를 호출
})
.finally(() => {
// 로딩 인디케이터 중지
})
.then(result => {
// 성공 결과 처리
})
.catch(error => {
// 에러 처리
});
4. 섞어서 사용 예시
then, catch, finally 3가지를 모두 한 번에 쓴다면 아래처럼 될 것입니다.
promise
.then(result => console.log(result), // Promise가 해결되었을 때
error => console.log(error) // Promise가 거부되었을 때
)
.catch(error => {
console.log(error);
})
.finally(() => {
// 마지막에 항상 실행됨
// finally는 로딩 인디케이터와 같은 정리 작업을 수행할 때 좋음
console.log('finally');
});
처리된 프라미스의 핸들러는 즉각 실행됩니다.
프라미스가 대기 상태일 때,. then/catch/finally 핸들러는 프라미스가 처리되길 기다립니다.
반면, 프라미스가 이미 처리상태라면 핸들러가 즉각 실행됩니다.
// 아래 프라미스는 생성과 동시에 이행됩니다.
let promise = new Promise(resolve => resolve("완료!"));
promise.then(alert); // 완료! (바로 출력됨)
03
Promises chaining
Promise 체이닝은 여러 비동기 작업을 순차적으로 처리하기 위해 then() 메서드를 이어서 사용하는 방식입니다.
then()은 Promise가 해결된 후 실행되는 콜백 함수를 지정하는데, 여러 then()을 연결하여 여러 비동기 작업을 순차적으로 처리할 수 있습니다.
❌초보자는 프라미스 하나에. then을 여러 개 추가한 후, 이를 체이닝이라고 착각하는 경우가 있습니다. 하지만 이는 체이닝이 아닙니다.
then()은 항상 Promise를 반환하기 때문에, 각 작업을 순차적으로 처리하고, 후속 작업에 전달할 값을 반환할 수 있습니다.
✅ 이해를 돕기 위한 비유
Promise 체이닝을 설명할 때는 줄 서서 기다리는 것처럼 비유할 수 있습니다.
- 첫 번째 사람이 일을 끝내면, 두 번째 사람은 그 일을 이어서 하고,
- 두 번째 사람이 일을 끝내면, 세 번째 사람이 이어서 하는 방식입니다.
✅ 예시
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("첫 번째 작업 완료"), 1000);
});
promise
.then(result => {
console.log(result); // 첫 번째 작업이 끝난 후, "첫 번째 작업 완료" 출력
return "두 번째 작업 완료"; // 두 번째 작업의 결과를 반환
})
.then(result => {
console.log(result); // 두 번째 작업이 끝난 후, "두 번째 작업 완료" 출력
return "세 번째 작업 완료"; // 세 번째 작업의 결과를 반환
})
.then(result => {
console.log(result); // 세 번째 작업이 끝난 후, "세 번째 작업 완료" 출력
});
⭐ Promise 체이닝에서 중요한 점
- then()은 값을 반환하거나 다른 Promise를 반환할 수 있습니다. 이때 반환된 값은 다음 then()으로 전달됩니다.
- 만약 then() 안에서 새로운 Promise를 반환하면, 그 Promise가 해결될 때까지 기다립니다.
new Promise((resolve, reject) => {
setTimeout(() => resolve("첫 번째 작업 완료"), 1000);
})
.then(result => {
console.log(result); // 1초 뒤 출력: "첫 번째 작업 완료"
return new Promise(resolve => setTimeout(() => resolve("두 번째 작업 완료"), 1000)); // 두 번째 작업은 1초 대기 후 완료
})
.then(result => {
console.log(result); // 2초 뒤 출력: "두 번째 작업 완료"
return "세 번째 작업 완료";
})
.then(result => {
console.log(result); // 출력: "세 번째 작업 완료"
});
04
유용한 메서드
내용
1. Promise.all
Promise.all은 여러 개의 Promise를 병렬로 처리하고,
모든 Promise가 해결될 때까지 기다린 후 결과를 한 번에 처리할 수 있게 해주는 메서드입니다.
비유를 하자면 Promise.all은 여러 명이 줄을 서서 기다리는 것과 비슷합니다.
첫 번째 사람이 일을 끝내면 두 번째 사람이 일을 시작하고, 그다음 사람이 일을 시작하는 방식입니다.
모든 사람이 끝난 후에 결과를 함께 처리하는 방식입니다.
✅ Promise.all 사용법
Promise.all은 배열로 여러 Promise를 받아들이며, 모든 Promise가 성공적으로 완료될 때까지 기다립니다.
하나라도 실패하면 reject로 바로 종료됩니다.
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("첫 번째 작업 완료"), 1000);
});
let promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("두 번째 작업 완료"), 2000);
});
let promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("세 번째 작업 완료"), 3000);
});
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log(results); // ["첫 번째 작업 완료", "두 번째 작업 완료", "세 번째 작업 완료"]
})
.catch(error => {
console.error("Error:", error);
});
📂 Promise.all 요약:
- 배열로 전달된 모든 Promise가 해결될 때까지 기다립니다.
- 모든 Promise가 성공적으로 해결되면, .then() 안에 전달된 results 배열로 모든 결과를 한 번에 받습니다.
- 하나라도 실패하면 catch()로 처리됩니다. 실패한 Promise가 있으면 즉시 에러가 발생하고, 다른 Promise들은 계속해서 실행됩니다.
2. Promise.allSettled
Promise.allSettled는 모든 Promise가 끝날 때까지 기다리지만, 성공과 실패를 따로 다룹니다.
이 메서드는 모든 Promise의 결과를 배열로 반환하는데, 각 Promise의 상태(fulfilled 또는 rejected)를 명시적으로 확인할 수 있습니다.
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("첫 번째 작업 완료"), 1000);
});
let promise2 = new Promise((resolve, reject) => {
setTimeout(() => reject("두 번째 작업 실패"), 2000);
});
let promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("세 번째 작업 완료"), 3000);
});
Promise.allSettled([promise1, promise2, promise3])
.then(results => {
console.log(results);
/*
[
{ status: "fulfilled", value: "첫 번째 작업 완료" },
{ status: "rejected", reason: "두 번째 작업 실패" },
{ status: "fulfilled", value: "세 번째 작업 완료" }
]
*/
});
📂 Promise.allSettled 요약:
- 모든 Promise가 완료된 후(성공 또는 실패) 그 결과를 배열로 반환합니다.
- 각 결과 객체는 status와 value(성공 시) 또는 reason(실패 시) 속성을 가집니다.
- 성공한 Promise는 status: "fulfilled", 실패한 Promise는 status: "rejected"로 나타납니다.
3. Promise.race
Promise.race는 여러 개의 Promise 중 가장 먼저 처리된 Promise를 기다리고 그 결과를 반환합니다.
다른 Promise들은 처리되지 않더라도 기다리지 않고 바로 결과를 처리합니다.
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("첫 번째 작업 완료"), 1000);
});
let promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("두 번째 작업 완료"), 500);
});
let promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("세 번째 작업 완료"), 2000);
});
Promise.race([promise1, promise2, promise3])
.then(result => {
console.log(result); // "두 번째 작업 완료"
})
.catch(error => {
console.error("Error:", error);
});
📂 Promise.race 요약:
- 가장 먼저 완료된 Promise의 결과만을 반환합니다.
- 첫 번째로 해결된 Promise가 성공하면 그 값을 반환하고, 실패하면 catch로 오류를 처리합니다.
- 다른 Promise들은 무시되고, 첫 번째로 해결된 Promise의 결과만 처리합니다.
4. Promise.any
Promise.any는 다수의 Promise 중 첫 번째로 성공한 Promise의 결과를 반환합니다.
만약 모든 Promise가 실패하면, 마지막에 AggregateError를 반환합니다.
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => reject("첫 번째 작업 실패"), 1000);
});
let promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("두 번째 작업 완료"), 500);
});
let promise3 = new Promise((resolve, reject) => {
setTimeout(() => reject("세 번째 작업 실패"), 2000);
});
Promise.any([promise1, promise2, promise3])
.then(result => {
console.log(result); // "두 번째 작업 완료"
})
.catch(error => {
console.error("Error:", error);
});
📂 Promise.any 요약:
- 여러 Promise 중 하나라도 성공하면 그 값을 반환합니다.
- 모든 Promise가 실패하면 AggregateError가 발생하고, 이를 catch로 처리합니다.
05
마무리
Promise 체이닝을 통해 여러 비동기 작업을 순차적으로 처리할 수 있으며, then(), catch(), finally()를 이용해 결과를 처리하고, 에러를 관리할 수 있습니다. 이를 통해 코드가 더 깔끔하고 직관적으로 작성될 수 있습니다.
또한, Promise.all, Promise.allSettled, Promise.race, Promise.any와 같은 다양한 메서드를 활용하면 여러 비동기 작업을 효율적으로 처리할 수 있습니다. 각 메서드는 특정 상황에 따라 유용하게 사용될 수 있습니다.
- Promise.all: 모든 작업이 끝날 때까지 기다린 후, 모든 결과를 처리합니다.
- Promise.allSettled: 모든 작업이 완료된 후, 각각의 결과를 반환합니다.
- Promise.race: 가장 먼저 해결된 작업을 처리합니다.
- Promise.any: 가장 먼저 성공한 작업을 처리합니다.
이들 메서드를 적절히 활용하면, 비동기 작업을 병렬로 처리하거나, 가장 빠른 작업을 우선적으로 처리하는 등의 다양한 방식으로 유용하게 활용할 수 있습니다. 이처럼 Promise는 비동기 작업을 효율적이고 관리하기 쉬운 방식으로 처리할 수 있게 도와주는 중요한 도구입니다.