Promise 객체
Promise 객체는 비동기 작업의 최종 완료 또는 실패
를 나타내는 자바스크립트 내장 객체이다. 즉, 곧 완료될 수도 있는 작업에 대한 약속을 표현하는 방식이다.
자바스크립트는 싱글 스레드 언어
이기 때문에 시간이 오래 걸리는 작업을 그대로 실행하면 전체 프로그램이 멈출 수 있다. 이를 해결하기 위해 비동기 처리가 필요하고, 과거에는 주로 콜백 함수를 사용했다. 하지만 콜백 함수의 중첩이 깊어지면서 코드가 복잡하고 가독성이 떨어지는 콜백 지옥(callback hell) 문제가 발생하게 되었고, 이를 해결하기 위해 Promise 객체가 도입되었다.
Promise 객체
├── Promise 프로퍼티
├── Promise 객체 생성법
├── .then()
├── .catch()
├── 콜백 지옥 (Callback Hell)
└── 프로미스 체이닝 (Promise Chaining)
Promise 프로퍼티
Promise 객체
├── state: pending(대기)
├── result: undefined (초기 상태)
├── 성공: resolve(value)
│ ├── state: fulfilled(성공)
│ └── result: value
└── 실패: reject(error)
├── state: rejected(실패)
└── result: error
Promise 객체가 생성되면 즉시 executor
함수가 실행되고 state의 프로퍼티 값은 pending, result 프로퍼티 값은 undefined가 할당된다. 이후 비동기 작업이 성공하면 resolve()
함수가 호출된다면 state 프로퍼티의 값은 fulfilled, result는 전달된 value로 바뀐다. 반대로 실패하면 reject()
함수가 호출되어 state는 rejected, result에는 전달된 error가 할당된다. 프로미스는 상태가 정해지면 그 이후의 resolve와, reject는 무시된다.
Promise 객체는 내부적으로 [[PromiseState]]
와 [[PromiseResult]]
라는 비공개 내부 슬롯을 가지고 있다. resolve와 reject가 호출되면, 작업의 성공 또는 실패 여부에 따라 이 내부 슬롯인 state와 result의 값이 각각 "fulfilled" 또는 "rejected", 그리고 결과 값(value) 또는 에러 정보(error)로 갱신된다.
이 내부 슬롯은 직접 접근할 수 없으며, .then(), .catch() 등을 통해 간접적으로 결과를 처리한다. 다만 브라우저 개발자 도구에서는 디버깅 목적으로 pending, fulfilled, rejected 상태를 볼 수 있지만, 실제 코드에서는 promise.state처럼 직접 접근하는 방식은 동작하지 않는다.
Promise 객체 생성법
const executor = (resolve, reject) => {
// 비동기 작업 수행
};
const promise = new Promise(executor);
// new 키워드를 통해 Promise 객체를 생성하고 executor 함수를 인수로 전달
- promise 객체는
new
키워드와생성자 함수
를 사용해 생성할 수 있다. 객체 생성 시executor
함수를 인수로 전달하며, 이 함수는 Promise 가 생성되자마자 즉시 실행되는 함수이다. executor
함수는 두 개의 콜백 함수,resolve()
와reject()
를 매개변수로 받는다. resolve(value)는 작업이 성공했을 때 호출, reject(reason)는 작업이 실패했을 때 호출이다.- 따라서 executor 비동기 작업을 수행하고, 그 결과에 따라 resolve 또는 reject를 호출하여
Promise의 상태
를 결정하는 핵심 함수이다. - 비동기 작업은 동시에 실행되기 때문에, 작업의 성공 또는 실패에 따라 다른 처리가 필요하며, Promise는 이를 명확하게 구조화할 수 있게 도와준다.
.then
.then()
메서드는 Promise가 이행 상태(성공 상태)
가 되었을 때 실행되는 메서드이다.
.then(success)
promise.then((result) => {
console.log(result);
});
resolve()
가 호출되면.then()의 콜백이 실행
되며, reject()된 경우는 무시된다. 실패 처리는 .catch()를 사용한다.
// 3초 후에 성공을 출력하는 비동기 함수
const executor = (resolve, reject) => {
setTimeout(() => {
resolve("성공");
}, 3000);
};
const promise = new Promise(executor);
promise.then((result) => {
console.log(result); // 성공
});
executor
함수 내부에서 비동기 작업이 완료되면, resolve(value)를 호출하여 성공 결과를 Promise에 전달한다.- 이렇게 전달된 값은 .then()의
콜백 함수의 매개변수로 자동 전달
되며, 이를 통해 결과를 받아 원하는 처리를 할 수 있다. - .then()은
체이닝
이 가능하므로, 연속된 비동기 작업을 순차적으로 처리할 때도 유용하다. - 실패한 경우엔 .then()의 콜백은 실행되지 않으며, .catch()를 통해 에러를 처리할 수 있다.
.then(success, failure)
promise.then(
(result) => {
console.log(result);
},
(error) => {
console.log(error);
}
);
- 하나의 .then()에서 성공과 실패 모두 처리하는 방법이다.
- 첫 번째 콜백은 resolve() 시 실행, 두 번째 콜백은 reject() 시 실행한다.
- 실제 현업에서는 가독성과 일관된 에러 처리를 위해 .then().catch() 구조를 주로 사용한다.
// 3초 후에 실행되는 비동기 함수를 Promise 객체를 이용해 구현
const executor = (resolve, reject) => {
setTimeout(() => {
resolve("성공");
reject("실패");
}, 3000);
};
const promise = new Promise(executor);
promise.then(
(result) => {
console.log(result);
},
(error) => {
console.log(error);
}
);
// 성공
- .then()에 성공 콜백 + 실패 콜백을 함께 작성한 방식이다. 성공하면 첫 번째 콜백 실행, 실패하면 두 번째 콜백 실행한다.
- 예외(에러)도 여기서 처리할 수 있지만, 체이닝 구조에서는 잘 안쓰는 방식이다.
- 위 코드에서는 resolve("성공")와 reject("실패")가 연달아 호출되지만, Promise는
한 번 상태가 결정되면 바뀌지 않기
때문에 먼저 호출된 resolve("성공")만 반영되고, 이후 호출된 reject("실패")는 무시된다. 따라서 then()의 첫 번째 콜백만 실행되며, 콘솔에는 "성공"만 출력된다.
.catch()
.catch()
메서드는 Promise가 실패 상태(rejected)
가 되었을 때 실행되는 메서드이며, .then() 체이닝 중 발생한 예외도 처리할 수 있다.
.then().catch()
// 비동기 작업이 성공했을 때와 실패했을 때 처리하는 방법
const executor = (resolve, reject) => {
setTimeout(() => {
reject("실패");
}, 3000);
};
const promise = new Promise(executor);
promise
.then((result) => {
console.log(result);
})
.catch((result) => {
console.log(result); // 실패
});
executor
함수 내부에서reject(error)
가 호출되면, .then()은 실행되지 않고 건너뛰며, .catch()가 호출된다.- reject()에 전달한 값은 .catch()의
콜백 함수의 매개변수
로 전달되어, 이를 통해 실패 이유를 받아 처리할 수 있다.
// 3초 후에 실행되는 비동기 함수를 Promise 객체를 이용해 구현
const executor = (resolve, reject) => {
setTimeout(() => {
resolve("성공");
reject("실패");
}, 3000);
};
const promise = new Promise(executor);
promise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.log(error);
});
// 성공
- new Promise(executor)가 호출되면서 executor 함수가 자동 실행된다. setTimeout이 실행되고 3초 후, resolve("성공")이 먼저 호출되면서 Promise의 상태는 "fulfilled"로 확정된다. 이어서 reject("실패")가 호출되지만, 이미 상태가 정해졌기 때문에 무시된다.
- .then()의 첫 번째 콜백이 실행되어 "성공"이 콘솔에 출력된다. .catch()는 실행되지 않는다.
콜백 지옥 (Callback Hell)
const workA = (value, callback) => {
setTimeout(() => {
callback(value + 5);
}, 5000);
};
const workB = (value, callback) => {
setTimeout(() => {
callback(value - 3);
}, 3000);
};
const workC = (value, callback) => {
setTimeout(() => {
callback(value + 10);
}, 10000);
};
workA(10, (resultA) => {
console.log(`workA : ${resultA}`);
workB(resultA, (resultB) => {
console.log(`workB : ${resultB}`);
workC(resultB, (resultC) => {
console.log(`workC : ${resultC}`);
});
});
});
// workA : 15
// workB : 12
// workC : 22
- 비동기 함수의 결과를 다른 비동기 함수에서 사용하려면, 콜백 함수 안에 또 다른 콜백 함수를 중첩해서 작성해야 한다.
- workA, workB, workC는 각각 일정 시간이 지난 후 콜백을 호출하는 비동기 함수이며, setTimeout을 사용해 각각 5초, 3초, 10초 후에 결과를 반환한다.
- 이처럼 콜백 안에 콜백이 계속 중첩되는 구조를 콜백 지옥(callback hell)이라고 하며, 코드의 가독성과 유지보수성을 크게 떨어뜨리는 원인이 된다.
// executor 함수를 별도로 작성하지 않고 바로 콜백함수로 넣어줌
const workA = (value) => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(value + 5);
}, 5000);
});
return promise;
};
const workB = (value) => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(value - 3);
}, 3000);
});
return promise;
};
const workC = (value) => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(value + 10);
}, 10000);
});
return promise;
};
workA(10).then((resultA) => {
console.log(`1. ${resultA}`);
workB(resultA).then((resultB) => {
console.log(`2. ${resultB}`);
workC(resultB).then((resultC) => {
console.log(`3. ${resultC}`);
});
});
});
// 1. 15
// 2. 12
// 3. 22
- Promise를 반환하는 함수는 그 결과에 .then() 메서드를 연결하여 비동기 작업의 완료 후 로직을 정의할 수 있다.
- 위 코드는 Promise를 사용했지만, 각 .then() 내부에 또 다른 .then()이 중첩되어 있어 여전히 가독성이 떨어지고 유지보수가 어려운 구조를 갖고 있다. 이는 콜백 지옥(callback hell)과 유사한 형태로, 비동기 흐름이 많아질수록 코드의 복잡도가 증가한다.
- 이를 개선하려면 .then()을 체이닝 방식으로 연결하여 코드를 수평적인 흐름으로 재구성하는 것이 좋다.
프로미스 체이닝 (Promise Chaining)
프로미스 체이닝은 .then()에서 새로운 Promise를 반환함으로써, 비동기 작업을 순차적으로 연결하고 가독성 높은 코드 흐름을 구현하는 방식이다. 아래 코드는 위의 콜백 지옥 구조를 Promise로 리팩토링한 예시다. 중첩은 여전하지만, .then()을 체이닝하면 더 개선할 수 있다.
const workA = (value) => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(value + 5);
}, 5000);
});
return promise;
};
const workB = (value) => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(value - 3);
}, 3000);
});
return promise;
};
const workC = (value) => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(value + 10);
}, 10000);
});
return promise;
};
workA(10)
.then((resultA) => {
console.log(`1. ${resultA}`);
return workB(resultA);
})
.then((resultB) => {
console.log(`2. ${resultB}`);
return workC(resultB);
})
.then((resultC) => {
console.log(`3. ${resultC}`);
});
// 1. 15
// 2. 12
// 3. 22
- 프로미스 체이닝이란, Promise 객체를 반환하고
.then() 메서드를 연속적으로 연결
하여 비동기 작업을 순차적으로 처리하는 방식이다. - .then()의 콜백 함수 안에서 또 다른 Promise를 반환하면, 다음 .then()에서 해당 Promise의 결과값을 이어받아 처리할 수 있다.
- 위 코드에서
workB(resultA)를 return함
으로써, workB가 반환하는 Promise 객체가 다음.then()으로 전달
된다. 이러한 방식은 가독성이 높고, 비동기 작업 간의 흐름을 명확하게 구성할 수 있어 콜백 지옥보다 훨씬 효율적이다.