[JavaScript] Promise

정진우·2024년 1월 23일
0
post-thumbnail

프로미스(Promise)

  • Promise는 자바스크립트에서 비동기 작업을 처리하는 객체입니다.
  • 비동기 처리는 특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 방식입니다.
  • Promise는 어떤 작업이 완료되거나 실패했을 때 실행할 콜백 함수를 정의할 수 있는 객체입니다.
  • 간단히 설명하면, Promise는 "약속"입니다. 무언가를 약속하고 그 약속을 지키면 뭔가를 실행하거나, 지키지 못하면 다른 일을 할 수 있게 도와주는 것입니다.
  • 레스토랑에서 음식을 주문했다고 가정했을 때, 주문한 것이 완료(pending)되면, 요리사는 주문 성공(resolve) 또는 주문 실패(reject)라는 약속을 합니다.
  • 성공(resolve)하면, 주문한 음식을 받아서 먹을 수 있게 됩니다.
  • 실패(reject)하면, 요리사가 다른 음식을 만들어 알려주면서 어떤 문제가 있었는지 설명해줄 수 있습니다.

콜백 함수의 비동기 처리

function addNumbers(num1, num2, callback) {
	setTimeout(() => {
		const result = num1 + num2;		
		callback(result);
	}, 1000);
}

addNumbers(2, 3, (n1) => {
	addNumbers(n1, 5, (n2) => {
		addNumbers(n2, 5, (n3) => {
			addNumbers(n3, 5, (n4) => {
				addNumbers(n4, 5, (n5) => {
					console.log("The final number is: " + n5);
				});
			});
		});
	});
});

콜백 함수가 중첩되면서 코드의 깊이가 깊어지면 코드의 가독성을 떨어뜨리고 코드의 흐름을 파악하기 어렵습니다. 또한 콜백 함수마다 에러처리를 따로 해줘야하며 에러가 발생한 위치를 찾기 어렵습니다.

프로미스로 개선된 비동기 처리

function addNumbers(num1, num2) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			const result = num1 + num2;
			resolve(result);
		}, 1000);
	});
} 

addNumbers(2, 3)
	.then((n1) => addNumbers(5, n1))
	.then((n2) => addNumbers(5, n2))
	.then((n3) => addNumbers(5, n3))
	.then((n4) => addNumbers(5, n4))
	.then((n5) => console.log("The final number is: " + n5));

비동기적으로 처리해야 하는 일이 많아질수록, 코드의 깊이가 계속 깊어지는 현상이 있는데, Promise를 사용하면 코드의 깊이가 깊어지는 현상을 방지할 수 있습니다.

Promise 문법

const myPromise = new Promise((resolve, reject) => {
	// executor (비동기 작업 수행)
	const data = fetch('서버로부터 요청할 URL');
    
  if(data)
		resolve(data); // 만일 요청이 성공하여 데이터가 있다면
  else
    reject("Error"); // 만일 요청이 실패하여 데이터가 없다면
})

executor는 새로운 Promise 객체가 생성될 때 자동으로 실행되는 함수입니다. new 키워드와 Promise 생성자 함수를 사용하여 생성할 수 있습니다. executor 함수는 resolvereject라는 두 개의 매개변수를 가지며, 비동기 작업을 수행한 후 작업이 성공한 경우 resolve 함수를 호출하고, 작업이 실패한 경우 reject 함수를 호출합니다.

Promise 예제코드

function fetchData() {
  return new Promise((resolve, reject) => {
  // 비동기 작업 수행 
  // 데이터를 성공적으로 가져오면 resolve 호출 
  // 에러가 발생하면 reject 호출
    setTimeout(() => {
      const data = "This is the fetched data";
      if (data) {
        resolve(data); // 데이터를 성공적으로 가져옴
      } else {
        reject("Failed to fetch data"); // 데이터 가져오기 실패
      }
    }, 2000);
  });
}

fetchData() // fetchData 함수를 호출하고 프로미스를 처리합니다.
  .then((data) => { // 성공적으로 수행했을 때 실행될 코드
    console.log("Data:", data); // resolve(data)의 data값이 출력된다
  })
  .catch((error) => { // 실패했을 때 실행될 코드
    console.error("Error:", error); // reject("Failed to fetch data")가 출력된다
  });

위 코드는 Promise를 반환합니다. 비동기 작업이 완료된 이후 작업 결과에 따라 then()catch()를 사용하여 성공과 실패에 대한 후속 처리를 진행할 수 있습니다. 주어진 코드에서 data가 존재하는 경우, resolve(data)를 호출하고, 그 후 .then()을 연결하여 콜백 함수에서 성공에 대한 추가 처리를 수행합니다. resolve() 함수의 매개변수 값은 then 메서드의 콜백 함수 인자로 전달되어, 프로미스 객체 내부에서 다룬 값을 사용할 수 있게 됩니다. 이 결과로 "This is the fetched data" 가 출력됩니다. 만약 처리가 실패하여 reject()를 호출하게 되면, .catch()로 이어져 catch메서드의 콜백 함수에서 실패에 대한 추가 처리를 진행하며 "Failed to fetch data" 를 출력합니다.

Promise 객체의 함수 등록

프로미스 객체를 변수로 할당하는 방식은 사용할 수 있지만, 보통의 경우 함수로 감싸서 사용하는 것이 더 일반적입니다.

const Promise1 = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
      console.log("This is the myPromise1");
    }, 1000);
  });
};

const Promise2 = new Promise((resolve) => {
  setTimeout(() => {
    resolve();
    console.log("This is the myPromise2");
  }, 1000);
});

위 코드에서 Promise1은 프로미스 객체를 반환하도록 구현했으며, Promise2는 프로미스 객체를 할당했습니다. executor는 새로운 Promise 객체가 생성될 때 자동으로 실행되는 함수입니다. 이 말은 Promise2는 즉시 실행되지만, Promise1은 호출하기 전까지 실행되지 않는다는 것을 의미합니다. 따라서 함수로 만들고 그 함수를 호출하면 프로미스 객체를 반환하여 생성된 프로미스 객체를 함수의 반환값으로 사용할 수 있습니다. 원하는 시점에 호출하거나 재사용하고 싶은 경우, Promise1과 같이 프로미스 객체를 반환하는 함수를 만들어야 합니다.

Promise 객체를 함수로 만드는 이유

  • 재사용성 및 모듈화
    - 여러 곳에서 동일한 비동기 로직을 재사용하고 모듈화할 수 있습니다.
    - 함수를 호출하여 다양한 입력값에 대한 결과를 얻을 수 있습니다.
    - 주로 코드의 가독성과 재사용성을 높이기 위한 이점을 강조합니다.
  • 가독성 및 유지보수성
    - 함수 이름이 프로미스의 목적을 명시적으로 전달하므로 코드를 읽는 사람이 해당 비동기 작업의 의도를 더 쉽게 이해할 수 있습니다.
    - 함수를 통해 프로미스를 감싸면 해당 함수의 이름 자체가 비동기 작업에 대한 의도를 명확하게 나타냅니다.
    - 비동기 작업을 하나의 모듈로 추상화하면 해당 모듈을 다른곳에서 쉽게 재사용할 수 있습니다. 이는 유지보수성을 높이는데 도움이 됩니다.
  • 확장성
    - 프로미스 객체를 함수로 만들면 인자를 전달하여 동적으로 비동기 작업을 수행할 수 있습니다.
    - 여러 개의 프로미스 객체를 반환하는 함수들을 연결하여 복잡한 비동기 로직을 구현할 수 있습니다.
    - 주로 동적인 비동기 작업 수행과 복잡한 비동기 로직의 확장성을 강조합니다.

    "동적으로 비동기 작업을 수행한다" 이 말은 함수 fetchData에 동적으로 전달된 URL에 대한 비동기 데이터를 가져오는 작업을 수행한다는 내용입니다.

const fetchData = (url) => {
  return new Promise((resolve, reject) => {
  // 동적으로 전달된 url을 이용하여 비동기 작업을 수행
    setTimeout(() => {
      const data = "Data fetched successfully from " + url;
      if (data) {
        resolve(data);
      } else {
        reject(new Error("Failed to fetch data from " + url));
      }
    }, 1000);
  });
};

const processData = (data) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const processedData = "Processed data: " + data;
      if (processedData) {
        resolve(processedData);
      } else {
        reject(new Error("Failed to process data"));
      }
    }, 1000);
  });
};

fetchData(`https://jsonplaceholder.typicode.com/posts/1`)
  .then((data) => processData(data))
  .then((processedData) => {
    console.log("Final result:", processedData);
  })
  .catch((error) => {
    console.error("Error:", error);
  });

위 코드는 fetchDataprocessData라는 두 개의 함수로 구성된 프로미스입니다. fetchData 함수는 주어진 URL에서 데이터를 가져오는 프로미스를 반환하고, processData 함수는 데이터를 가공하는 프로미스를 반환합니다. 이렇게 프로미스를 사용하면 fetchDataprocessData 함수를 재사용할 수 있습니다. 예를 들어 다른 API 엔드포인트에서 데이터를 가져와 처리해야 하는 경우에도 같은 로직을 사용할 수 있습니다. 이런 재사용성은 코드의 유지 보수성을 높이고, 중복을 최소화하며 개발 생산성을 향상시킬 수 있습니다.

API 엔드포인트
API 엔드포인트는 웹 서비스 또는 웹 애플리케이션에서 외부에 제공되는 특정 URI 또는 URL을 의미합니다. 클라이언트가 서버와 통신할 때 해당 리소스에 접근하는 데 사용되는 URL 주소가 API 엔드포인트입니다.

"다른 API 엔드포인트에서 데이터를 가져와 처리"라는 문장은 다른 서버 또는 다른 리소스에서 데이터를 가져와서 그 데이터를 처리하는 것을 의미합니다. 위 코드에서는 fetchData 함수가 이러한 엔드포인트를 사용하여 데이터를 가져오고, 이후 processData 함수에서 해당 데이터를 처리합니다.

Promise의 3가지 상태

Promise는 처리과정을 의미하는 3가지 상태(state)를 가지고 있습니다. Promise를 생성하고 종료될 때까지의 3가지 상태를 갖습니다. new Promise생성자가 반환하는 Promise객체는 다음과 같은 내부 프로퍼티를 갖습니다.

  • Pending(대기) : 프로미스가 아직 처리되지 않은 상태입니다.
  • Fulfilled(이행) : 프로미스가 성공적으로 처리되어 결과 값을 반환한 상태입니다.
  • Rejected(거부) : 프로미스가 처리 중에 오류가 발생하여 처리를 실패한 상태입니다.

Pending(대기)

Pending 상태는 프로미스 객체가 생성되고, 비동기 작업이 아직 완료되지 않은 초기 상태를 나타냅니다. 프로미스 객체를 생성한 후 콘솔로 확인해보면 프로미스 객체의 상태가 Pending으로 출력됩니다.

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("처리 완료");
  }, 5000);
});

console.log(myPromise); // "pending"

Fulfilled(이행)

위에서 작성한 코드를 실행한 후 5초 뒤에 콘솔로 다시 확인하면 Fulfilled 상태로 변하게 됩니다. 5초가 지나면 resolve("처리 완료")가 실행되어 프로미스의 성공을 알리는 개체를 호출하고, 비동기 로직이 성공적으로 완료되었음을 나타내는 상태를 보여줍니다.

그리고 Fulfilled 상태로 변한 프로미스는 .then()을 연결하여 성공에 대한 추가 처리를 수행할 수 있습니다.

myPromise.then((data) => {
  console.log("프로미스 처리 완료!!!");
});

Rejected(거부)

비동기 작업이 실패하거나 에러가 발생한 상태입니다. reject("처리 실패")를 호출하면 프로미스 객체가 Rejected상태가 됩니다. 5초뒤 콘솔로 확인해보면 Rejected가 출력되는걸 확인할 수 있습니다.

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("처리 실패");
  }, 5000);
});

console.log(myPromise);

Rejected 상태로 변한 프로미스는 .catch()을 연결하여 실패에 대한 추가 처리를 진행할 수 있습니다.

myPromise.catch((error) => {
  console.log(error);
  console.log("프로미스 처리 실패!!!");
});

Promise Chaining

프로미스의 체이닝은 여러 개의 Promise를 연결하여 비동기 작업을 순차적으로 처리하는 방식입니다. 아래 코드는 순차적으로 다섯 번의 비동기적인 숫자 덧셈 작업을 수행합니다. 각 작업은 이전 작업의 결과값을 기반으로 차례로 실행되며, 마지막에 최종 결과값이 콘솔에 출력됩니다.

function addNumbers(num1, num2) {
	return new Promise((resolve) => {
		setTimeout(() => {
			const result = num1 + num2;
			resolve(result);
		}, 1000);
	});
} 

addNumbers(2, 3)
	.then((n1) => addNumbers(5, n1))
	.then((n2) => addNumbers(5, n2))
	.then((n3) => addNumbers(5, n3))
	.then((n4) => addNumbers(5, n4))
	.then((n5) => console.log("The final number is: " + n5));

Promise 정적 메소드

프로미스에는 여러 정적(static)메소드가 있습니다. 이러한 정적 메소드들은 주로 프로미스의 생성과 관련된 작업을 수행하거나, 여러 프로미스를 조작하고 처리하는 데 사용됩니다.

Promise.all()

Promise.all()은 여러 개의 프로미스를 동시에 실행하고, 모든 프로미스가 완료될 때까지 기다렸다가 그 결과를 배열로 반환하는 메소드입니다.

const promise1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve('Promise 1 resolved');
  }, 2000);
});

const promise2 = new Promise((resolve) => {
  setTimeout(() => {
    resolve('Promise 2 resolved');
  }, 3000);
});

const promise3 = new Promise((resolve) => {
  setTimeout(() => {
    resolve('Promise 3 resolved');
  }, 1500);
});

Promise.all([promise1, promise2, promise3])
  .then((results) => {
    console.log(results);
  })
  .catch((error) => {
    console.error(error);
  });

위 코드에서 Promise.all()[promise1, promise2, promise3] 배열의 프로미스들을 동시에 실행합니다. 모든 프로미스가 완료되면, .then() 메소드가 실행되고, 각 프로미스의 결과가 results 배열로 전달됩니다. 만약 하나라도 프로미스가 거부된다면 .catch() 블록이 실행됩니다.

Promise.race()

Promise.race()메소드는 입력된 프로미스들 중에서 가장먼저 fulfilled(이행)되거나 rejected(거부)된 프로미스를 기반으로 새로운 프로미스를 반환합니다.

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 1');
  }, 2000);
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 2');
  }, 1000);
});

const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('Promise 3');
  }, 3000);
});

const promises = [promise1, promise2, promise3];

Promise.race(promises)
  .then(result => {
    console.log(result); // 'Promise 2'
  })
  .catch(error => {
    console.error(error);
  });

Promise.race()는 fulfilled(이행) 또는 rejected(실패) 여부와 관계없이 가장 먼저 처리가 끝난 Promise2를 콘솔에 출력합니다.

Promise.any()

Promise.all()은 주어진 모든 프로미스가 모두 완료되어야만 결과를 도출하는 반면, Promise.any()는 주어진 모든 프로미스 중 하나라도 완료(fulfilled)되면 즉시 반환하는 정적 메서드입니다.

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('Promise 1');
  }, 2000);
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('Promise 2');
  }, 1000);
});

const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 3');
  }, 3000);
});

const promises = [promise1, promise2, promise3];

Promise.any(promises)
  .then(result => {
    console.log(result); // 'Promise 3'
  })
  .catch(errors => {
    console.error(errors); // ['Promise 1', 'Promise 2']
  });

예제 코드를 보면 Promise3이 출력되는 것을 확인할 수 있습니다. Promise.any()는 여러 프로미스 중에서 가장 먼저 이행(fulfilled)된 프로미스의 결과를 반환하기 때문에 거부(rejected) 상태인 Promise1Promise2는 무시됩니다.

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('Promise 1');
  }, 2000);
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('Promise 2');
  }, 1000);
});

const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 3');
  }, 3000);
});

const promises = [promise1, promise2, promise4];

Promise.any(promises)
  .then(result => {
    console.log(result);
  })
  .catch(errors => {
    console.error(errors); // AggregateError: All promises were rejected
    console.error(errors.errors); // ['Promise 1', 'Promise 2', 'promise4 failed']
  });

Promise.any()를 사용하여 여러 프로미스를 처리하고, 결과에서 거부(rejected)된 프로미스 에러들을 AggregateError로 묶어서 처리합니다.

AggregateError
AggregateError는 여러 에러를 하나의 에러로 묶어주는 객체로, ES2021(ECMAScript2021)에서 도입된 새로운 에러 타입 중 하나입니다. AggregateError는 여러 에러가 발생한 상황에서 모든 에러를 캡슐화 하여 하나의 AggregateError로 처리할 수 있으며, 여러 에러를 한 번에 사용자에게 전달할 때 유용합니다. 이 배열은 errors라는 속성을 통해 접근할 수 있습니다.

Promise.resolve()

Promise.resolve()는 주어진 값을 가지는 이행(resolve) 상태의 프로미스를 생성하는 메소드입니다. Promise.resolve()는 두 가지 경우를 다룰 수 있습니다.

  • 값이 프로미스가 아닌 경우 : 전달된 값이 프로미스가 아니라면, 해당 값으로 이행된(resolve) 상태의 새로운 프로미스가 생성됩니다.
const value = 42;
const promise = Promise.resolve(value);

promise.then((result) => {
  console.log(result); // 42
});
  • 값이 이미 프로미스인 경우 : 전달된 값이 이미 프로미스라면, 해당 프로미스가 그대로 반환됩니다.
const existingPromise = new Promise((resolve) => resolve('Hello, Promise!'));
const resolvedPromise = Promise.resolve(existingPromise);

resolvedPromise.then((result) => {
  console.log(result); // 'Hello, Promise!'
});

이러한 특징은 특히 함수에서 비동기 작업의 결과를 반환하거나 이미 완료된 작업을 프로미스로 감싸는 등의 상황에서 사용됩니다.

Promise.reject()

Promise.reject(reason)는 주어진 이유(reason)로 거부된(rejected) 프로미스를 반환합니다.

const errorReason = new Error('Something went wrong');

const rejectedPromise = Promise.reject(errorReason);

rejectedPromise.catch((error) => {
  console.error('Error:', error.message); // 'Something went wrong'
});

Promise.reject(reason)는 거부된 상태의 프로미스를 생성하며, catch 메소드를 사용하여 거부된 이유를 처리합니다. 여기서 reason은 거부된 프로미스의 이유를 설명하는 값입니다. 또한, reason은 선택적인 매개변수이며, 생략될 경우 기본값은 undefined입니다.

const rejectedPromise = Promise.reject();

rejectedPromise.catch((reason) => {
  console.error('Reason:', reason); // undefined
});


참고자료

profile
내가 바뀌지 않으면 아무것도 바뀌지 않는다 🔥🔥🔥

0개의 댓글