[JS] 프로미스 객체와 메서드
[콜백함수↗️] 에 이어 작성하는 글입니다.
1. 동기와 비동기
- 동기
- 작업이 순차적으로 실행된다. 한 작업이 끝나야 다음 작업이 시작된다.
- 즉, 동기적 처리는 기다리는 동안 아무 것도 할 수 없다. (멈춰있는 화면)
- 비동기
- 작업이 동시에 실행될 수 있다. 한 작업이 끝나기를 기다리지 않고 다음 작업을 진행할 수 있다.
- 아래 그림처럼 응답이 오지 않았음에도 요청을 동시 다발적으로 할 수 있다.
비동기 함수의 동기적 표현이 왜 필요할까?
-
외부 서버로부터 날씨 데이터를 가져와 화면에 출력하는 경우
- 상황: 외부 서버에서 데이터를 가져오는 작업은 시간이 걸릴 수 있다.
- 필요성: 데이터를 가져오는 작업은 비동기적으로 처리하지만, 데이터를 가져온 후 화면에 출력하는 작업은 순차적으로(동기적으로) 처리되어야 한다.
- 정리: 날씨 데이터를 가져오는 비동기 작업이 완료된 후 화면에 데이터를 출력하는 작업이 순차적으로 이루어져야 하므로, 비동기 작업을 동기적으로 표현할 필요가 있다.
-
신용카드 이용 내역에 대한 메일 알림 서비스 신청
- 상황: 알림 서비스 신청 후 실제 메일 발송까지 시간이 걸린다(예: 5분 이상).
- 필요성: 사용자가 신청을 완료했다는 즉각적인 피드백이 필요하지만, 메일 발송은 시간이 걸리므로 비동기적으로 처리된다.
- 정리: 사용자가 신청 후 빠른 응답(“신청이 완료됐습니다.”)을 받을 수 있도록 비동기 작업의 결과를 기다리지 않고 즉시 응답을 제공해야 한다.
-
여러 외부 정보 사이트에서 데이터를 읽어와 내부 데이터베이스에 저장하는 경우
- 상황: 여러 사이트에서 데이터를 읽어오는 작업은 동시에 비동기적으로 수행된다.
- 필요성: 각 비동기 작업이 완료된 후에 데이터를 내부 데이터베이스에 저장하는 작업은 순차적으로(동기적으로) 이루어져야 한다.
- 정리: 모든 외부 데이터가 준비된 후 동기적으로 데이터베이스에 저장하여 데이터를 일괄적으로 처리하기 위해 비동기 작업을 동기적 표현으로 처리한다.
2. 프로미스 개요
2.1 프로미스 개념
Promise는 비동기 작업에 대한 “약속”이다.
- 🤙비동기 처리가 끝나면 알려줘! 그러면(then) 내가 성공(resolve) 또는 실패(reject) 객체를 반환(return) 해줄게!
new Promise
로 생성된 프로미스는 인자로 전달된 콜백 함수를 즉시 실행한다.- 프로미스의 내부에서 resolve 또는 reject가 호출하는 구문이 있을 경우, 이 호출이 이루어진 후에 프로미스는 다음 단계(.then() 또는 .catch())로 넘어간다.
.then()
으로 성공했을 때,.catch()
로 실패했을 때 콜백을 등록한다.
- 비동기 처리 결과(resolve(성공), reject(실패))와, 진행 상태(pending(대기), filfilled(이행), rejected(실패)) 상태를 가진다.
➡️ 즉, 프로미스를 사용하여 비동기 작업의 동기적 표현을 구현할 수 있는 것이다.
2.2 프로미스 state
Pending하고 나면 Fulfilled 또는 Rejected가 발생한다.
상태 (State) | 설명 |
---|---|
대기 (Pending) | 프로미스가 생성된 초기 상태, 비동기 처리 로직이 완료되지 않은 상태. |
이행 (Fulfilled) | 프로미스가 성공적으로 완료된 상태이며 성공 리턴 값(resolve)이 전달됨. |
거부 (Rejected) | 프로미스가 실패한 상태이며 실패 리턴 값(reject)이 전달됨. |
2.3 Promise 메서드
“then”, “catch”, “finally”를 사용하여 프로미스 객체의 성공, 실패, 완료 상태를 처리하며 데이터를 소비할 수 있다.
메소드 | 설명 |
---|---|
then | 프로미스가 성공(resolve) 상태일 때 실행할 콜백 함수 등록 |
catch | 프로미스 체인에서 발생한 에러를 처리하는 콜백 함수 등록 |
finally | 프로미스 처리 완료시 항상 실행되는 로직을 정의하는 블록 |
- Resolve(성공리턴값)호출 -> then으로 연결
- Reject(실패리턴값)호출 -> catch로 연결
- reject는 Error라는 object를 통해서 값을 전달한다. 어떤 에러가 발생했는지 이유를 잘 명시해서 작성해줘야한다.
- Finally는 성공, 실패하던 상관없이 무조건 마지막에 호출된다.
3. 프로미스 사용하기
3.1 프로미스 생성
- 프로미스를 만드는 사람(Producer)은 기존에 존재하지 않는 새로운 비동기 작업을 정의할 때
new Promise
를 사용한다. - 프로미스를 이용하는 사람(consumer)은 이미 존재하는 프로미스(자신이 만들었거나 다른 사람이 만든)를 사용할 때
async/await
을 사용한다.
프로미스는 클래스이기 때문에 new 라는 키워드를 사용해서 object를 생성할 수 있다.
new Promise
: 프로미스 객체를 생성할 때 사용되는 구문이다. 이때 인자로 콜백 함수를 받는다. 이 콜백 함수는 resolve와 reject라는 두 개의 인자를 가진다.
const promise = new Promise();
프로미스 생성자는 콜백 함수를 매개변수로 받는다. 이 콜백 함수는 두 개의 매개변수(resolve, reject)를 가진다
- 프로미스를 만드는 순간 우리가 전달한 executor라는 콜백 함수가 바로 실행된다.
- 이행단계일때 resolve를 통해 성공 리턴 값을 호출하며, 거부단계일 때 reject를 통해 실패 리턴 값을 호출한다.
const promise = new Promise((resolve, reject) => {
// 무거운 일들을 실행한다.
});
resolve
- 비동기 작업이 성공적으로 완료되었을 때 호출되는 함수 ->
.then()
로 연결
- 비동기 작업이 성공적으로 완료되었을 때 호출되는 함수 ->
reject
- 비동기 작업이 실패했을 때 호출되는 함수 ->
.catch
로 연결
- 비동기 작업이 실패했을 때 호출되는 함수 ->
3.2 프로미스 사용
settimeout()을 이용해 원하는 콜백 함수를 1초 뒤에 실행시켜보자
resolve라는 콜백 함수를 호출하여 기능이 잘 수행됐을 때 “성공”을 호출하게 하였다.
// Promise 생성
const myPromise = new Promise((resolve, reject) => {
// 비동기 작업을 수행
setTimeout(() => {
const success = true;
if (success) {
resolve("Promise was successful!");
} else {
reject("Promise failed.");
}
}, 1000);
});
// Promise 사용
myPromise
.then((message) => {
console.log(message); // 성공했을 때 호출: "Promise was successful!"
})
.catch((error) => {
console.error(error); // 실패했을 때 호출: "Promise failed."
})
.finally(() => {
console.log("Promise 작업이 완료되었습니다."); // 항상 실행: 성공, 실패와 무관하게 호출
});
delay 함수를 이용해 1초 후 실행시켜보자
프로미스는 지정된 시간(ms)이 지난 후에 완료된다.
function delay(ms) {
// 새로운 프로미스를 생성하여 반환
return new Promise((resolve) => setTimeout(resolve, ms));
}
delay(1000).then(() => console.log("1초 후 실행"));
3. Promise chaining
프로미스를 연결할 때 Promise chaining을 사용하며, 각 Promise의 결과를 다음 Promise로 전달하며 작업을 이어나갈 수 있다.
// 1초 후에 숫자를 반환하는 Promise 생성
function waitAndReturnNumber(number) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`Returning number: ${number}`);
resolve(number);
}, 1000);
});
}
// Promise chaining
waitAndReturnNumber(1)
.then((number) => {
// 첫 번째 Promise가 성공적으로 완료된 후 호출됨
return waitAndReturnNumber(number + 1); // 2를 반환하는 Promise
})
.then((number) => {
// 두 번째 Promise가 성공적으로 완료된 후 호출됨
return waitAndReturnNumber(number + 1); // 3을 반환하는 Promise
})
.then((number) => {
// 세 번째 Promise가 성공적으로 완료된 후 호출됨
return waitAndReturnNumber(number + 1); // 4를 반환하는 Promise
})
.then((number) => {
// 마지막 Promise가 성공적으로 완료된 후 최종 값 출력
console.log(`Final number: ${number}`); // 최종 결과: 4
})
.catch((error) => {
// Promise 중 하나라도 실패하면 호출됨
console.error(`Error occurred: ${error}`);
})
.finally(() => {
// 성공 여부와 상관없이 항상 실행
console.log("Promise chaining complete.");
});
➡️ Promise Chaining은 비동기 작업을 순차적으로 처리할 때 유용하지만, 작업이 많아지면 코드가 복잡해지고 가독성이 떨어질 수 있다. [async와 await↗️]를 사용하면 이런 문제를 해결할 수 있다.
4. Promise와 병렬처리
4.1 Promise.all
Promise.all은 Promise를 병렬로 처리할 수 있게 해준다.
아래코드는 이 코드는 각 fetch 호출이 완료될 때까지 차례대로 기다린다. 따라서 첫 번째 요청이 완료되기 전까지는 두 번째 요청이 시작되지 않으며, 이로 인해 전체 작업 완료 시간이 길어지게 된다.
async function fetchPostsSequentially() {
try {
const response1 = await fetch(
"https://jsonplaceholder.typicode.com/posts/1"
);
const post1 = await response1.json();
const response2 = await fetch(
"https://jsonplaceholder.typicode.com/posts/2"
);
const post2 = await response2.json();
const response3 = await fetch(
"https://jsonplaceholder.typicode.com/posts/3"
);
const post3 = await response3.json();
console.log([post1, post2, post3]); // 3개의 게시물 데이터를 순차적으로 처리 후 출력
} catch (error) {
console.error("Error:", error);
}
}
fetchPostsSequentially();
Promise.all을 사용해서 병렬 처리를 해보자!
병렬 처리는 각 요청이 독립적으로 이루어지며, 모든 요청이 동시에 시작되어 각각 완료되는 즉시 다음 단계로 진행할 수 있다.
Promise.all([
fetch("https://jsonplaceholder.typicode.com/posts/1").then((response) =>
response.json()
),
fetch("https://jsonplaceholder.typicode.com/posts/2").then((response) =>
response.json()
),
fetch("https://jsonplaceholder.typicode.com/posts/3").then((response) =>
response.json()
),
])
// 모든 Promise가 성공적으로 완료되었을 때 실행됨
.then((posts) => {
console.log(posts); // 3개의 게시물 데이터가 배열로 반환
})
.catch((error) => {
// 하나라도 실패하면 여기서 에러를 처리
console.error("Error:", error);
});
Promise.all은 모든 Promise가 성공적으로 완료되어야만 결과를 반환하지만, 한 개의 Promise가 실패하면 전체 작업이 실패로 간주된다. 따라서 여러 서버 중 하나가 다운되면 Promise.all에 전달된 모든 Promise가 실패하게 되어, 전체 작업이 실패하게 된다.
4.2 Promise.allSettle
Promise.allSettled는 모든 Promise의 결과와 오류 정보를 담은 객체들을 배열로 반환한다.
즉, Promise.all과 다르게 배열을 순회하면서 각 Promise의 상태에 따라 처리할 수 있다.
이는 하나의 Promise가 실패하더라도 다른 Promise들에 영향을 미치지 않고 각 요청의 성공 또는 실패 여부를 확인하여 적절한 처리를 할 수 있다.
Promise.all([
fetch("https://jsonplaceholder.typicode.com/posts/1"),
fetch("https://this-url-does-not-exist.typicode.com/posts/999"), // 의도적으로 실패하도록 설정
fetch("https://jsonplaceholder.typicode.com/posts/3"),
])
.then((results) => {
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`요청 #${index + 1} 성공:`, result.value);
result.value.json().then((data) => console.log(data));
} else if (result.status === "rejected") {
console.error(`요청 #${index + 1} 실패:`, result.reason);
}
});
})
.catch((error) => {
console.error("실패했습니다", error);
})
.finally(() => {
console.log("------------------------");
});
Promise.allSettled([
fetch("https://jsonplaceholder.typicode.com/posts/1"),
fetch("https://this-url-does-not-exist.typicode.com/posts/999"), // 의도적으로 실패하도록 설정
fetch("https://jsonplaceholder.typicode.com/posts/3"),
]).then((results) => {
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`요청 #${index + 1} 성공:`, result.value);
result.value.json().then((data) => console.log(data));
} else if (result.status === "rejected") {
console.error(`요청 #${index + 1} 실패:`, result.reason);
}
});
});
정리
Promise.all | Promise.allSettled | |
---|---|---|
동작 방식 | 여러 프로미스를 병렬로 실행하고 모든 프로미스가 성공적으로 완료될 때까지 기다립니다. 모든 프로미스가 fulfilled 상태가 되면, 결과값들의 배열을 반환합니다. | 여러 프로미스를 병렬로 실행하고, 모든 프로미스가 완료될 때까지 기다립니다. 여기서 “완료”는 성공(fulfilled)이든 실패(rejected)이든 상관없습니다. 각 프로미스의 결과를 나타내는 객체 배열을 반환합니다. |
실패 처리 | 만약 주어진 프로미스 중 하나라도 rejected 상태가 되면, Promise.all 은 즉시 rejected 상태가 됩니다. 이때 첫 번째 발생한 에러가 전체 Promise.all 의 에러로 반환됩니다. |
반환되는 각 객체는 status 속성(값은 fulfilled 또는 rejected)과, 성공한 경우 value, 실패한 경우 reason 속성을 가집니다. |
반환 값 | 성공한 경우, 결과값들의 배열 반환 / 실패한 경우, 첫 번째 에러 반환 | 결과 및 오류 정보를 담은 객체들의 배열 반환 |
주 사용 사례 | 모든 작업이 성공적으로 완료되어야 하며, 어느 하나라도 실패하면 전체가 실패하는 것으로 간주될 때 사용합니다. | 여러 작업의 결과가 각각 독립적일 때 사용하며, 성공과 실패를 각각 처리해야 할 필요가 있을 때 유용합니다. |
Promise.all
: 모든 프로미스가 성공해야 하며, 하나라도 실패하면 전체가 실패한 것으로 간주한다.Promise.allSettled
: 모든 프로미스의 성공/실패 여부에 상관없이 모든 결과를 받아 각각을 개별적으로 처리한다.
5. 코드 리팩토링 연습
이전에 했던 예시인 콜백함수를 프로미스로 변경해보자!
콜백함수
setTimeout(
function (name) {
var coffeeList = name;
console.log(coffeeList);
setTimeout(
function (name) {
coffeeList += ", " + name;
console.log(coffeeList);
setTimeout(
function (name) {
coffeeList += ", " + name;
console.log(coffeeList);
setTimeout(
function (name) {
coffeeList += ", " + name;
console.log(coffeeList);
},
500,
"카페라떼"
);
},
500,
"카페모카"
);
},
500,
"아메리카노"
);
},
500,
"에스프레소"
);
프로미스로 변경해보자
new Promise((resolve) => {
setTimeout(() => {
const name = "에스프레소";
console.log(name);
resolve(name);
}, 500);
})
.then(
(prevName) =>
new Promise((resolve) => {
setTimeout(() => {
const name = `${prevName}, 아메리카노`;
console.log(name);
resolve(name);
}, 500);
})
)
.then(
(prevName) =>
new Promise((resolve) => {
setTimeout(() => {
const name = `${prevName}, 카페모카`;
console.log(name);
resolve(name);
}, 500);
})
)
.then(
(prevName) =>
new Promise((resolve) => {
setTimeout(() => {
const name = `${prevName}, 카페라떼`;
console.log(name);
resolve(name);
}, 500);
})
);
위 코드의 반복적인 로직을 함수화 시켜보자
const addCoffee = (name) => (prevName) =>
new Promise((resolve) => {
setTimeout(() => {
const nameName = prevName ? `${prevName}, ${name}` : name;
console.log(nameName);
resolve(nameName);
}, 500);
});
// 커피 이름을 순차적으로 추가
addCoffee("에스프레소")()
.then(addCoffee("아메리카노"))
.then(addCoffee("카페모카"))
.then(addCoffee("카페라떼"));
6. 참조
- https://www.youtube.com/watch?v=JB_yU6Oe2eE&t=508s
댓글남기기