코딩항해기

[JS] Promise 본문

HTML CSS JS

[JS] Promise

miniBcake 2024. 11. 7. 17:34

 

 

Promise

 

Promise - JavaScript | MDN

Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타냅니다.

developer.mozilla.org

 

자바스크립트의 비동기 처리를 위한 하나의 패턴으로 콜백 함수를 사용한다. 하지만 전통적인 콜백 패턴은 콜백 헬로 인해 가독성이 좋지않다. 또한 발생한 에러의 처리가 곤란하며, 여러 개의 비동기를 한 번에 처리하는데도 어려움이 있다. 

 

이러한 문제를 개선하기 위해 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