코딩항해기
[JS] Promise 본문
Promise
자바스크립트의 비동기 처리를 위한 하나의 패턴으로 콜백 함수를 사용한다. 하지만 전통적인 콜백 패턴은 콜백 헬로 인해 가독성이 좋지않다. 또한 발생한 에러의 처리가 곤란하며, 여러 개의 비동기를 한 번에 처리하는데도 어려움이 있다.
이러한 문제를 개선하기 위해 Promise가 도입됐으며, 단점을 보완하고 비동기처리 시점을 명확하게 표현한다.
Promise 생성
Promise는 생성자 함수를 통해 사용되며, 비동기 작업을 수행할 콜백 함수 resolve와 reject를 인자로 전달받느다.
(resolve 성공 시 수행, reject 실패 시 수행)
// Promise 객체의 생성
const promise = new Promise((resolve, reject) => {
// 비동기 작업을 수행한다.
if (/* 비동기 작업 수행 성공 */) {
resolve('result'); //성공 시 전달되는 값
}
else { /* 비동기 작업 수행 실패 */
reject('failure reason'); //실패 시 전달되는 값
}
});
Promise는 비동기 처리가 성공했는지 실패했는지 등에 대한 상태 정보를 갖는다.
상태 | 의미 | 구현 |
pending | 비동기 처리가 아직 수행되지 않은 상태 | resolve 또는 reject 함수가 아직 호출되지 않은 상태 |
fulfilled | 비동기 처리가 수행된 상태 (성공) | resolve 함수가 호출된 상태 |
rejected | 비동기 처리가 수행된 상태 (실패) | reject 함수가 호출된 상태 |
settled | 비동기 처리가 수행된 상태 (성공 또는 실패) | resolve 또는 reject 함수가 호출된 상태 |
Promise 생성자 함수가 인자로 전달받은 콜백 함수는 내부에서 비동기 처리 작업을 수행한다. 이때 비동기 처리가 성공하면 콜백함수의 인자로 전달받은 resolve 함수를 호출한다. 이 때 Promise는 fulfilled 상태가 된다. 비동기처리에 실패하면 reject 함수를 호출하고, Promise는 rejected 상태가 된다.
사용 예시
// 좋아요 수를 얻기 위한 비동기 처리 함수
function getLikeCnt(boardNum) {
return new Promise((resolve, reject) => { //Promise 확인
console.log("log: getLikeCnt start");
$.ajax({ //비동기처리
url: 'infoLikeCnt.do',
type: 'POST',
data: {boardNum: boardNum}, //인자로 받은 boardNum 데이터전달
success: function (likeCntData) {//Cnt 데이터 반환
console.log('likeCntData : ' + likeCntData);
if (likeCntData < 0) { // 0보다 작은 경우 fail 처리
reject('fail cnt data'); //실패
}
resolve(likeCntData); //성공 시 반환받은 데이터 그대로 전달
},
error: function (error) { //비동기 실패 시
console.error('Error:', error);
reject(error); //error 메세지 전달
}
});
});
}
resolve은 성공 시 reject는 오류 발생, 비정상적인 값일 때 사용한다. return처럼 처음 만난 resolve 또는 reject를 실행하고 그 다음로직은 무시된다.
후속처리 메서드 : then catch (finally)를 사용하는 경우
//사용하는 측의 예시자료
getLikeCnt(123) //boardNum 임의 값
.then(count => {
// 성공 시 실행될 코드
console.log('좋아요 수:', count);
updateLikeDisplay(count); // UI 업데이트
})
.catch(error => {
// 실패 시 실행될 코드 (음수이거나 네트워크 오류 등)
console.error('에러 발생:', error);
showErrorMessage(); // 에러 메시지 표시
});
then catch를 사용하지 않고 반환값을 이용해 사용할 수도 있다.
후속처리 : async/await를 사용하는 경우
async function handleLikeCount() {
try {
const count = await getLikeCnt(123); // boardNum 임의 값
// 성공 시 실행될 코드
console.log('좋아요 수:', count);
updateLikeDisplay(count); // UI 업데이트
} catch (error) {
// 실패 시 실행될 코드 (음수이거나 네트워크 오류 등)
console.error('에러 발생:', error);
showErrorMessage(); // 에러 메시지 표시
}
}
// 함수 실행
handleLikeCount();
대신 이 경우 resolve인 값만 반환되고 reject는 에러를 throw하기 때문에 try-catch를 필요로 한다.
차이
async/await를 선호하는 경우는 순차적인 비동기처리가 일어나고 try-catch로 에러처리가 깔끔할 때 선호하며, then, catch를 선호하는 경우는 병렬처리가 필요하고 데이터 변환 체인이 많을 때, Promise.all Promise.race 등이 사용될 때 선호된다.
여러 예시
const wrongUrl = 'https://jsonplaceholder.typicode.com/XXX/1';
1.
promiseAjax(wrongUrl)
.then(res => console.log(res), err => console.error(err)); //then에서 error로 함께 처리 가능
// Error: 404
2.
promiseAjax(wrongUrl)
.then(res => console.log(res))
.catch(err => console.error(err)); //catch를 통해 error 처리 가능
// Error: 404
3.
promiseAjax(wrongUrl)
.then(res => console.log(res))
.then(undefined, err => console.error(err)); //catch메서드를 호출하면 내부적으로 then(undefined, onRejected)을 호출한다.
// Error: 404
4.
promiseAjax('https://jsonplaceholder.typicode.com/todos/1')
.then(res => console.xxx(res), err => console.error(err));
// 두 번째 콜백 함수는 첫 번째 콜백 함수에서 발생한 에러를 캐치하지 못한다.
// Promise 자체의 reject만 잡기 때문이다.
5.
promiseAjax('https://jsonplaceholder.typicode.com/todos/1')
.then(res => console.xxx(res))
.catch(err => console.error(err)); //catch로 구분하는 것이 가독성이 좋다.
// TypeError: console.xxx is not a function
[출처] https://poiemaweb.com/es6-promise
프로미스 체이닝 Promise Chaining
비동기 함수의 처리 결과를 가지고 다른 비동기 함수를 호출해야하는 경우, 함수의 호출이 중첩되어 복잡도가 높아지는 콜백 헬이 발생한다. Promise는 후속처리 메서드를 체이닝해 여러 개의 Promise를 사용할 수 있다. 프로미스 체이닝을 통해 콜백 헬 단점을 해결했다.
Promise 객체를 반환한 비동기 함수는 후속처리 메서드인 then이나 catch메서드를 사용할 수 있다. 따라서 then 메서드가 Promise 객체를 반환하도록 하면 (기본 설정) 여러 개의 Promise를 연결해 사용할 수 있다.
프로미스 체이닝 예시
// DOM에서 결과를 표시할 요소 선택
const $result = document.querySelector('.result');
// JSON 데이터를 문자열로 변환하여 화면에 표시하는 함수
const render = content => {
$result.textContent = JSON.stringify(content, null, 2); // null, 2는 보기 좋게 포맷팅하기 위한 옵션
};
// Promise 기반의 AJAX 요청 함수
const promiseAjax = (method, url, payload) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(payload));
xhr.onreadystatechange = function () {
// 요청이 완료되지 않았으면 리턴
if (xhr.readyState !== XMLHttpRequest.DONE) return;
// HTTP 상태 코드가 200~399 사이면 성공으로 처리
if (xhr.status >= 200 && xhr.status < 400) {
resolve(xhr.response); // 성공시 응답 데이터로 resolve
} else {
reject(new Error(xhr.status)); // 실패시 에러로 reject
}
};
});
};
const url = 'http://jsonplaceholder.typicode.com/posts';
// Promise 체이닝 시작
promiseAjax('GET', `${url}/1`)
// 첫 번째 then:
// 1. ID가 1인 포스트를 가져옴
// 2. 응답을 파싱하여 해당 포스트의 userId를 추출
// 3. 추출한 userId로 새로운 AJAX 요청을 생성하여 반환
.then(res => promiseAjax('GET', `${url}?userId=${JSON.parse(res).userId}`))
// 두 번째 then:
// 이전 요청의 응답(문자열)을 JavaScript 객체로 파싱
.then(JSON.parse)
// 세 번째 then:
// 파싱된 데이터를 화면에 렌더링
.then(render)
// 체인 어디서든 발생한 에러를 처리
.catch(console.error);
/*
체이닝 상세 설명:
1. promiseAjax('GET', `${url}/1`)
- ID가 1인 포스트 데이터를 가져옴
- 결과: { id: 1, userId: X, title: "...", body: "..." }
2. .then(res => promiseAjax('GET', `${url}?userId=${JSON.parse(res).userId}`))
- 첫 번째 요청의 응답에서 userId를 추출
- 해당 userId가 작성한 모든 포스트를 요청
- 결과: 문자열 형태의 포스트 배열
3. .then(JSON.parse)
- 문자열을 JavaScript 객체로 변환
- 결과: 포스트 객체들의 배열
4. .then(render)
- 최종 데이터를 화면에 표시
5. .catch(console.error)
- 위 과정 중 발생하는 모든 에러를 처리
*/
[출처] https://poiemaweb.com/es6-promise
Promise 정적 메서드
메서드 | 설명 |
Promise.all([p1, p2, ...]) | 모든 Promise가 이행될 때까지 기다린다. 하나라도 거부되면 즉시 거부된다. |
Promise.race([p1, p2, ...]) | 가장 먼저 처리되는 Promise의 결과를 반환된다. (성공/실패 모두) |
Promise.resolve(value) | 주어진 값을 이행 상태의 Promise로 변환된다. |
Promise.reject(error) | 주어진 에러로 거부 상태의 Promise 생성된다. |
Promise.allSettled([p1, p2, ...]) | 모든 Promise가 처리될 때까지 기다린다. (성공/실패 상관없이) |
Promise.any([p1, p2, ...]) | 하나라도 성공하면 해당 결과 반환한다. 전부 실패하면 AggregateError가 난다. |
Promise.all
Promise.all([
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) // [ 1, 2, 3 ]
.catch(console.log);
Promise.all([
new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 1!')), 3000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 2!')), 2000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 3!')), 1000))
]).then(console.log)
.catch(console.log); // Error: Error 3!
Promise.all([
1, // => Promise.resolve(1)
2, // => Promise.resolve(2)
3 // => Promise.resolve(3)
]).then(console.log) // [1, 2, 3]
.catch(console.log);
Promise.race
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);
Promise.race([
new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 1!')), 3000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 2!')), 2000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 3!')), 1000))
]).then(console.log)
.catch(console.log); // Error: Error 3!
참고 블로그 : https://poiemaweb.com/es6-promise
'HTML CSS JS' 카테고리의 다른 글
[Bootstrap] 부트스트랩 프레임워크 (2) | 2024.11.09 |
---|---|
[JS] 콜백 지옥 Callback Hell (0) | 2024.11.08 |
[Web] HTTP 코드 정리 (0) | 2024.10.15 |
[JS] location.replace() 와 location.href 차이 (+ 페이지 함수) (0) | 2024.09.19 |
[JS] JQuery CDN 정리 (0) | 2024.09.17 |