
이번엔 자바스크립트에서 비동기 처리를 위해 가장 많이 사용되는 방식 중 하나인 Promise에 대해 이야기해보려고 한다. 사실 예전에는 콜백을 사용해서 비동기 작업을 처리했지만, 콜백이 많아지면 코드가 정말 지저분해지고, 무슨 일이 먼저 실행되는지도 헷갈리게 된다. 이걸 보통 "콜백 지옥"이라고 부르기도 한다.
Promise는 비동기 작업의 완료나 실패를 처리하기 위한 객체다. 단순히 "성공했을 때 뭐 하고, 실패했을 땐 이거 하고"를 좀 더 체계적이고 읽기 좋은 방식으로 다루기 위한 개념이라고 보면 된다.
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("작업 성공!");
} else {
reject("작업 실패!");
}
});
promise
.then((result) => {
console.log(result);
})
.catch((err) => {
console.error(err);
});
Promise는 세 가지 상태를 가진다:
Pending : 초기 상태, 아직 성공도 실패도 하지 않은 상태Fulfilled : 작업이 성공적으로 완료된 상태 → resolve() 호출됨Rejected : 작업이 실패한 상태 → reject() 호출됨한 번 Fulfilled나 Rejected 상태가 되면, 그 이후로는 절대 다른 상태로 바뀌지 않는다. 이게 Promise가 "결과를 약속한다"는 의미이기도 하다.
예전 방식은 보통 이런 식이었다:
function fetchData(callback) {
setTimeout(() => {
callback("데이터 도착!");
}, 1000);
}
fetchData((data) => {
console.log(data);
});
하지만 이런 방식으로 비동기 작업이 중첩되면 아래처럼 된다:
doSomething((result) => {
doAnotherThing(result, (next) => {
doFinalThing(next, (final) => {
console.log(final);
});
});
});
이게 바로 콜백 지옥이다. 들여쓰기도 계속 깊어지고, 오류 처리도 어렵고, 가독성도 최악이다. 그래서 나온 해결책이 바로 Promise다.
Promise를 사용하면 이렇게 바꿀 수 있다:
doSomething()
.then(doAnotherThing)
.then(doFinalThing)
.then(console.log)
.catch(console.error);
있다. 아무리 좋아 보여도 완벽한 해결책은 아니다. 크게 두 가지 문제를 정리해볼 수 있다.
단일 체인에서는 .catch() 하나로 커버가 되지만, 중첩된 비동기 흐름이 생기면 어디서 예외가 발생했는지 명확하게 처리하기가 어렵다. 예를 들어, 중간의 then() 안에서 발생한 오류와 마지막에서 발생한 오류를 구분하기 어렵다.
fetch("/data")
.then((res) => res.json())
.then((data) => {
// 여기서 오류가 나면 catch에서 잡지만
throw new Error("중간 오류");
})
.catch((err) => {
// 어디서 발생한 오류인지 구체적으로 파악하기 어렵다
console.error(err);
});
then() 체인을 너무 많이 쓰다 보면 결국 그 자체가 지저분해지기도 한다. 들여쓰기는 줄어들 수 있지만, 순차 로직이 많을수록 가독성은 여전히 떨어진다.
그래서 나온 게 바로 async/await. 이건 다음에 따로 정리하겠지만, 결론적으로 Promise를 쓰면서 생긴 한계를 좀 더 자연스럽게 풀어준다.
Promise는 비동기 처리를 훨씬 깔끔하고 체계적으로 만들었다.resolve와 reject를 통해 성공과 실패를 다룬다.그래서 진짜로 실무에선 보통 Promise를 기본 단위로 쓰되, 최종적으로는 async/await와 함께 쓰는 경우가 많다. 다음 글에선 그 얘기도 해보자!