Promise resolve와 fulfilled의 차이: 호출하는 함수와 도달한 상태를 구분하기
빠른 답
resolve는 Promise의 상태 이름이 아니라 Promise 생성자 안에서 제공되는 함수입니다.fulfilled는 Promise가 성공 값으로 완료된 상태입니다.resolve(value)에 일반 값을 넘기면 대개fulfilled가 되지만, rejected Promise나 실패하는 thenable을 넘기면rejected가 될 수 있습니다.resolve호출은 동기 코드에서 일어나도then콜백은 현재 실행 흐름이 끝난 뒤 microtask로 실행됩니다.
목차
한눈에 비교
resolve와 fulfilled가 헷갈리는 이유는 둘이 자주 이어서 관찰되기 때문입니다. 예제에서 resolve("완료")를 호출하면 then에서 "완료"를 받으므로, resolve가 곧 fulfilled처럼 보입니다. 하지만 Promise 의미론에서는 함수 호출과 상태 확정을 나누어 보는 편이 더 정확합니다.
resolve는 Promise에게 “이 값으로 결과를 정해 달라”고 요청하는 함수에 가깝습니다. 전달된 값이 일반 값이면 성공 상태로 끝나지만, 다른 Promise나 thenable이면 그 대상의 최종 결과를 따라갈 수 있습니다.
시간 흐름으로 이해하기
이 흐름에서 중요한 차이는 resolve 호출과 then 콜백 실행이 같은 순간이 아니라는 점입니다. 실행자 함수는 Promise를 만들 때 즉시 실행되지만, then에 넘긴 콜백은 현재 콜스택이 비워진 뒤 실행됩니다.
그래서 콘솔 로그를 찍어 보면 resolve를 먼저 호출했는데도 then 로그가 뒤늦게 출력됩니다. 이는 버그라기보다 Promise가 콜백 실행을 microtask로 미루는 방식 때문입니다.
Promise의 상태와 값 구분하기
Promise에는 대표적으로 세 가지 상태가 있습니다.
pending: 아직 성공 값이나 실패 이유가 정해지지 않은 상태fulfilled: 성공 값으로 완료된 상태rejected: 실패 이유로 완료된 상태
여기서 fulfilled와 rejected는 둘 다 더 이상 pending이 아닌 완료 상태입니다. 둘을 함께 묶어 말할 때는 settled라는 표현을 씁니다.
fulfilled: 성공 값이 있는 완료 상태rejected: 실패 이유가 있는 완료 상태settled:fulfilled또는rejected로 확정된 상태
반면 resolve는 상태 이름이 아닙니다. Promise 생성자에 넘기는 실행자 함수가 받는 인자 중 하나입니다.
const promise = new Promise((resolve, reject) => {
resolve("완료");
});
promise.then((value) => {
console.log(value);
});
이 코드에서 resolve("완료")는 "완료"라는 값을 Promise 결과로 넘깁니다. 그 결과 Promise가 성공 값으로 완료되면 상태를 fulfilled라고 부릅니다.
resolve를 호출해도 fulfilled가 아닐 수 있는 경우
일반 값을 넘기는 예제만 보면 resolve는 항상 성공으로 이어지는 것처럼 보입니다.
const normalValue = new Promise((resolve) => {
resolve("일반 값");
});
normalValue.then((value) => {
console.log("normalValue:", value);
});
출력은 다음과 같습니다.
normalValue: 일반 값
하지만 resolve에 다른 Promise를 넘기면 이야기가 달라집니다. 바깥 Promise는 안쪽 Promise의 결과를 따라갑니다.
const fulfilledPromise = new Promise((resolve) => {
resolve(Promise.resolve("이미 성공한 Promise"));
});
const rejectedPromise = new Promise((resolve) => {
resolve(Promise.reject(new Error("안쪽 Promise 실패")));
});
fulfilledPromise.then((value) => {
console.log("fulfilledPromise:", value);
});
rejectedPromise.catch((error) => {
console.log("rejectedPromise:", error.message);
});
예상 출력은 다음과 같습니다.
fulfilledPromise: 이미 성공한 Promise
rejectedPromise: 안쪽 Promise 실패
두 번째 Promise는 분명히 resolve(...)를 호출했습니다. 그런데 최종 상태는 fulfilled가 아니라 rejected입니다. resolve에 전달한 값이 rejected Promise였고, 바깥 Promise가 그 실패 결과를 따라갔기 때문입니다.
이 차이는 thenable에서도 나타납니다. thenable은 then 메서드를 가진 객체를 말합니다. Promise는 이런 객체를 받으면 일반 객체처럼 바로 성공 값으로 처리하지 않고, 그 then 메서드를 통해 결과를 동화하려고 합니다.
const failingThenable = {
then(resolve, reject) {
reject(new Error("thenable 실패"));
},
};
Promise.resolve(failingThenable).catch((error) => {
console.log("catch:", error.message);
});
출력은 다음과 같습니다.
catch: thenable 실패
따라서 resolve를 “성공시키는 함수”라고만 기억하면 일부 상황을 설명하기 어렵습니다. 일반 값에서는 성공으로 관찰되는 경우가 많지만, Promise나 thenable을 넘기면 그 대상의 결과가 최종 상태를 결정합니다.
콘솔 출력으로 보는 실행 순서
then이 언제 실행되는지도 자주 헷갈리는 지점입니다. 아래 코드는 resolve 호출 시점과 then 콜백 실행 시점을 분리해서 보여줍니다.
console.log("1. script start");
const promise = new Promise((resolve) => {
console.log("2. inside executor");
resolve("완료");
console.log("3. after resolve");
});
promise.then((value) => {
console.log("5. then:", value);
});
console.log("4. script end");
실제 출력 순서는 다음과 같습니다.
1. script start
2. inside executor
3. after resolve
4. script end
5. then: 완료
resolve("완료")가 호출된 뒤에도 3. after resolve와 4. script end가 먼저 출력됩니다. Promise의 결과가 정해지는 것과 then 콜백이 실행되는 것은 같은 동작이 아니기 때문입니다.
이 순서를 디버깅할 때 놓치면, resolve 다음 줄에서 값이 아직 반영되지 않은 것처럼 보이는 상황을 오해하기 쉽습니다. Promise 결과에 의존하는 코드는 then, catch, await 흐름 안에서 확인하는 편이 상태를 추적하기 쉽습니다.
Node.js에서 같은 예제를 반복 실행하려면 간단한 스크립트를 둘 수 있습니다.
{
"scripts": {
"promise:order": "node promise-order.js"
}
}
promise-order.js에 앞의 코드를 넣고 실행하면 다음처럼 확인할 수 있습니다.
npm run promise:order
> promise:order
> node promise-order.js
1. script start
2. inside executor
3. after resolve
4. script end
5. then: 완료
이 출력에서 볼 부분은 resolve라는 단어보다 로그 순서입니다. 실행자 함수 내부는 즉시 실행되고, then 콜백은 동기 코드가 끝난 뒤 실행됩니다.
resolve, reject, then, catch를 함께 읽는 법
Promise를 직접 만들 때는 성공 값과 실패 이유를 분리해서 생각하면 코드가 읽기 쉬워집니다. 값을 정상적으로 만들 수 있으면 resolve(value)를 호출하고, 작업을 완료할 수 없는 경우에는 reject(error)를 호출합니다.
function parseNumberAsync(input) {
return new Promise((resolve, reject) => {
const number = Number(input);
if (Number.isNaN(number)) {
reject(new Error(`숫자로 바꿀 수 없습니다: ${input}`));
return;
}
resolve(number);
});
}
parseNumberAsync("42")
.then((value) => {
console.log("성공:", value);
})
.catch((error) => {
console.log("실패:", error.message);
});
resolve(number)로 전달한 값은 then의 첫 번째 콜백에서 받습니다. 반대로 reject(error)로 전달한 실패 이유는 catch에서 받습니다.
async/await를 쓰면 resolve와 reject가 코드에 직접 보이지 않을 수 있습니다. 그래도 상태 의미는 같습니다. fulfilled 값은 await 표현식의 결과가 되고, rejected 이유는 예외처럼 catch 블록으로 이동합니다.
async function run(input) {
try {
const value = await parseNumberAsync(input);
console.log("await 성공:", value);
} catch (error) {
console.log("await 실패:", error.message);
}
}
run("abc");
예상 출력은 다음과 같습니다.
await 실패: 숫자로 바꿀 수 없습니다: abc
await는 Promise를 없애는 문법이 아니라 Promise 결과를 더 동기 코드처럼 읽게 해 주는 문법입니다. 내부적으로는 fulfilled 값과 rejected 이유를 계속 구분합니다.
자주 나오는 오해
첫 번째 오해는 resolve를 호출하면 then이 즉시 실행된다는 생각입니다. 실제로는 현재 동기 코드가 끝난 뒤 microtask 큐에서 then 콜백이 실행됩니다. 콘솔 출력 순서가 예상과 다르면 먼저 이 지점을 확인해 볼 만합니다.
두 번째 오해는 resolve가 항상 fulfilled를 만든다는 생각입니다. 일반 값을 넘기면 보통 그렇게 보이지만, rejected Promise나 실패하는 thenable을 넘기면 바깥 Promise도 rejected가 될 수 있습니다.
세 번째 오해는 fulfilled가 “성공과 실패를 포함한 완료”라는 생각입니다. Promise에서 성공과 실패를 모두 포함해 더 이상 pending이 아닌 상태를 말할 때는 settled를 씁니다. fulfilled는 그중 성공 값이 있는 상태만 가리킵니다.
네 번째 오해는 Promise가 모든 비동기 오류를 자동으로 잡아 준다는 생각입니다. Promise 실행자 함수 안에서 동기적으로 던진 오류는 rejected 상태로 이어질 수 있지만, setTimeout 같은 별도 비동기 콜백 안에서 던진 오류는 직접 reject로 연결해야 합니다.
new Promise((resolve, reject) => {
setTimeout(() => {
try {
throw new Error("타이머 안에서 실패");
} catch (error) {
reject(error);
}
}, 0);
}).catch((error) => {
console.log("잡은 오류:", error.message);
});
출력은 다음과 같습니다.
잡은 오류: 타이머 안에서 실패
Promise 체인으로 실패를 전달하려면 실패 이유가 reject 또는 rejected Promise로 이어져야 합니다. 비동기 콜백 내부에서 발생한 오류가 자동으로 바깥 Promise에 연결되는 것은 아닙니다.
면접에서 설명할 때의 기준
면접에서는 resolve와 fulfilled를 같은 단어처럼 묶기보다 함수와 상태를 나누어 설명하면 충분합니다.
resolve는 Promise 생성자에서 제공되는 함수이고, 전달한 값으로 Promise 해결 절차를 시작합니다. 그 결과 Promise가 성공 값으로 완료되면 그 상태를 fulfilled라고 부릅니다. 다만 resolve에 rejected Promise나 실패하는 thenable을 넘기면 그 결과를 따라갈 수 있으므로, 최종 상태가 항상 fulfilled라고 말하기는 어렵습니다.
실행 순서까지 함께 말하면 Promise의 동작을 더 분명하게 설명할 수 있습니다. resolve가 호출되어도 then 콜백은 즉시 실행되지 않고 microtask 큐에서 실행됩니다. 그래서 동기 로그가 먼저 출력되고, then의 로그는 나중에 출력됩니다.
Promise 질문은 단어 정의만 묻는 것처럼 보여도 실제로는 값, 상태, 오류, 실행 시점이 함께 얽혀 있는 경우가 많습니다. resolve는 함수, fulfilled는 성공 상태, rejected는 실패 상태, then 콜백은 microtask에서 실행된다는 네 가지 축을 나누어 보면 대부분의 예제가 같은 흐름으로 읽힙니다.
원문 참고
https://www.maeil-mail.kr/question/73
댓글
댓글 쓰기