27. Promise
27. Promise

27. Promise

Authors
Date
Aug 23, 2025
Tags
Published
Published
Slug

1. Promise란?

Promise는 비동기 작업의 상태를 나타내는 객체다. Promise가 생성되고, 비동기 연산이 종료된 후에 결과값 또는 실패에 대한 처리를 연결할 수 있다. Promise는 3가지 상태를 가진다.
  1. pending: 이행하지도, 거부하지도 않은 초기 상태.
  1. fulfilled: 연산이 성공적으로 완료됨.
  1. rejected: 연산이 실패함.
세가지 상태는 각각 아래와 같이 구현할 수 있다.
const pendingPromise = new Promise(() => { console.log('pending...') }) const fulfilledPromise = new Promise((resolve) => { resolve('fulfilled') }) const rejectedPromise = new Promise((resolve, reject) => { reject('rejected') }) console.log('pendingPromise: ', pendingPromise) console.log('fulfilledPromise: ', fulfilledPromise) console.log('rejectedPromise: ', rejectedPromise)
여기서 fulfilled와 refected상태의 Promise객체에서 각각 thencatch매소드를 사용할 수 있다. (사실 then에서 rejected도 처리할수 있다.)
fulfiledPromise.then(value => { console.log('여기서 promise가 fulfilled 된 다음에 수행할 작업을 합니다.', value) }) rejectedPromise.catch(error => { console.log('여기서 promise가 rejected된 다음에 수행할 작업을 합니다.', error) })

1-1. Promise 한번 사용해보기

Promise를 사용하지 않고 콜백을 사용하는 방식과, Promise를 사용한 방식을 비교해볼 수 있다.
  • Promise 없이 사용
function successCallback(result) { console.log("Audio file ready at URL: " + result); } function failureCallback(error) { console.log("Error generating audio file: " + error); } createAudioFileAsync(audioSettings, successCallback, failureCallback);
  • Promise 사용
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
함수에 콜백을 전달하지 않고 then 매소드를 통해 콜백을 붙이는 것으로 간단하게 사용될 수 있다.

2. Chaining

chaining은 Promise의 핵심 장점 중 하나이다. 보통 두 개 이상의 비동기 작업을 순차적으로 실행해야하는 상황에서 사용된다.
아래는 chaining을 사용하지 않고 비동기 작업을 연속적으로 수행하는 예제코드이다.
doSomething(function (result) { doSomethingElse( result, function (newResult) { doThirdThing( newResult, function (finalResult) { console.log("Got the final result: " + finalResult); }, failureCallback, ); }, failureCallback, ); }, failureCallback);
이를 모던한 방식인 chaining을 사용하여 간단히 구성할 수 있다.
doSomething() .then((result) => doSomethingElse(result)) .then((newResult) => doThirdThing(newResult)) .then((finalResult) => { console.log(`Got the final result: ${finalResult}`); }) .catch(failureCallback);
catch 이후에도 작업을 추가로 수행할 수 있다.
new Promise((resolve, reject) => { console.log("Initial"); // 1 resolve(); }) .then(() => { throw new Error("Something failed"); console.log("Do this"); // 실행되지 않음 }) .catch(() => { console.log("Do that"); // 2 }) .then(() => { console.log("Do this, whatever happened before"); // 3 }); /* Initial Do that Do this, whatever happened before */
notion image

3. Error propagation

Promise는 에러 핸들링 처리에서도 유리하다. 앞선 [[Promise#2. Chaining]]에서의 콜백 지옥 예제에서는 실패 콜백인 failureCallback이 총 3번 발생을 했다. 하지만 chaining을 사용하면 이 또한 개선할 수 있다.
doSomething() .then((result) => doSomethingElse(result)) .then((newResult) => doThirdThing(newResult)) .then((finalResult) => console.log(`Got the final result: ${finalResult}`)) .catch(failureCallback);
  • 값을 반환하면 다음 then으로 resolved 값이 간다.
  • 에러를 throw하면 다음 catchrejected가 간다.
  • Promise를 반환하면 체인이 평탄화(flatten)되어 그 Promise의 상태/값을 따른다.
중요한점은, 중간의 체인 어디에서나 에러가 발생하면 catch로 이동한다는 점이다. 따라서 각 체인마다 catch를 작성할 필요없이 마지막에만 catch를 추가하여도 모든 에러를 잡을 수 있다. 이는 콜백 지옥의 근본적인 결함을 해결하며, 비동기 작업 구성에 필수적이다.

4. Promise rejection events

rejectionhandledunhandledrejection은 Promise와 관련된 이벤트이다. unhandledrejection은 Promise가 reject되었는데, catch메소드로 처리하지 않았을때 발생하는 이벤트이다.
const p = Promise.reject(new Error("문제 발생!")); window.addEventListener("unhandledrejection", (event) => { console.log("unhandledRejection:", event.reason); });
rejectionhandled는 처리되지 않은 Promise reject를 뒤늦게 처리한 경우 발생하는 이벤트이다.
const p = Promise.reject(new Error("문제 발생!")); setTimeout(() => { // reject된 뒤 나중에 catch를 붙임 p.catch((err) => { console.log("나중에 처리:", err.message); }); }, 2000); window.addEventListener("unhandledrejection", (event) => { console.log("unhandledrejection:", event.reason); }); window.addEventListener("rejectionhandled", (event) => { console.log("rejectionhandled:", event.reason); });

5. Timing

then에 전달된 함수는 동기적으로 호출되지 않는다.
Promise.resolve().then(() => console.log(2)); console.log(1); // 출력: // 1 // 2
전달된 함수는 마이크로 태스크 큐에 추가되기 때문에, 현재 콜스택이 비워진 다음에야 실행될 수 있다.

6. Nesting

Promise chaining은 가능한 평평하게 유지하는것이 좋다. 중첩된 체인은 대게 안티 패턴을 만들 가능성이 높다.
doSomethingCritical() .then((result) => doSomethingOptional(result) .then((optionalResult) => doSomethingExtraNice(optionalResult)) .catch((e) => {}), ) // Ignore if optional stuff fails; proceed. .then(() => moreCriticalStuff()) .catch((e) => console.log("Critical failure: " + e.message));
중첩된 체인 구조를 사용할 경우 catch는 내부 체인에 대한 에러만 처리가 가능하다. 따라서 위의 예제에서는 doSomethingCritical에서 오류가 발생할 시에 가장 하단의 catch에서만 오류를 잡을 수 있다.
유명한 안티패턴으로는 아래 예제가 있다.
function getStuffDone(param) { return new Promise(function(resolve, reject) { myPromiseFn(param+1) .then(function(val) { resolve(val); }).catch(function(err) { reject(err); }); }); }
위 예제에서는 myPromiseFn을 호출하면서 작업에 대한 fulfilled와 reject에 대한 처리를 새로운 Promise로 감싸서 처리하고있다. 하지만 위 getStuffDone함수는 아래와 같이 작성해도 차이가 없다.
function getStuffDone(param) { return myPromiseFn(param+1) }
이미 myPromiseFn에서 Promise를 반환하기 때문에 새로운 Promise로 감쌀 필요가 없는것이다.
 
 

Refs.