기본 콘텐츠로 건너뛰기

Promise resolve와 fulfilled의 차이: 호출하는 함수와 도달한 상태를 구분하기

Promise resolve와 fulfilled의 차이: 호출하는 함수와 도달한 상태를 구분하기

빠른 답

  • resolve는 Promise의 상태 이름이 아니라 Promise 생성자 안에서 제공되는 함수입니다.
  • fulfilled는 Promise가 성공 값으로 완료된 상태입니다.
  • resolve(value)에 일반 값을 넘기면 대개 fulfilled가 되지만, rejected Promise나 실패하는 thenable을 넘기면 rejected가 될 수 있습니다.
  • resolve 호출은 동기 코드에서 일어나도 then 콜백은 현재 실행 흐름이 끝난 뒤 microtask로 실행됩니다.

한눈에 비교

개념
resolve 는 호출하는 함수이고, fulfilled 는 Promise가 가질 수 있는 상태입니다.
위치
resolve 는 new Promise((resolve, reject) => {}) 의 실행자 함수에 인자로 들어오고, fulfilled 는 Promise 내부 상태를 설명할 때 쓰입니다.
역할
resolve 는 전달된 값으로 Promise 해결 절차를 시작하고, fulfilled 는 그 절차가 성공 값으로 끝난 상태를 뜻합니다.
값 처리
resolve(1) 은 1 로 이행되지만, resolve(Promise.reject(error)) 는 바깥 Promise도 거부될 수 있습니다.
실행 시점
resolve 호출은 지금 실행 중인 코드에서 발생하고, then 콜백은 microtask 큐를 거쳐 나중에 실행됩니다.
오류 의미
fulfilled 는 성공 값이 있는 상태이고, 실패 이유가 있는 상태는 rejected 입니다.

resolvefulfilled가 헷갈리는 이유는 둘이 자주 이어서 관찰되기 때문입니다. 예제에서 resolve("완료")를 호출하면 then에서 "완료"를 받으므로, resolve가 곧 fulfilled처럼 보입니다. 하지만 Promise 의미론에서는 함수 호출과 상태 확정을 나누어 보는 편이 더 정확합니다.

resolve는 Promise에게 “이 값으로 결과를 정해 달라”고 요청하는 함수에 가깝습니다. 전달된 값이 일반 값이면 성공 상태로 끝나지만, 다른 Promise나 thenable이면 그 대상의 최종 결과를 따라갈 수 있습니다.

시간 흐름으로 이해하기

Promise 생성
new Promise 가 호출되고 실행자 함수가 즉시 실행됩니다.
resolve 호출
실행자 안에서 resolve(value) 가 호출되면 Promise 해결 절차가 시작됩니다.
값 확인
전달된 값이 일반 값인지, Promise인지, thenable인지 확인합니다.
상태 확정
일반 값이면 fulfilled , 실패한 Promise를 따르면 rejected 가 될 수 있습니다.
콜백 실행
등록된 then 또는 catch 콜백은 microtask 큐에서 실행됩니다.

이 흐름에서 중요한 차이는 resolve 호출과 then 콜백 실행이 같은 순간이 아니라는 점입니다. 실행자 함수는 Promise를 만들 때 즉시 실행되지만, then에 넘긴 콜백은 현재 콜스택이 비워진 뒤 실행됩니다.

그래서 콘솔 로그를 찍어 보면 resolve를 먼저 호출했는데도 then 로그가 뒤늦게 출력됩니다. 이는 버그라기보다 Promise가 콜백 실행을 microtask로 미루는 방식 때문입니다.

Promise의 상태와 값 구분하기

Promise에는 대표적으로 세 가지 상태가 있습니다.

  • pending: 아직 성공 값이나 실패 이유가 정해지지 않은 상태
  • fulfilled: 성공 값으로 완료된 상태
  • rejected: 실패 이유로 완료된 상태

여기서 fulfilledrejected는 둘 다 더 이상 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 resolve4. 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를 쓰면 resolvereject가 코드에 직접 보이지 않을 수 있습니다. 그래도 상태 의미는 같습니다. 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에 연결되는 것은 아닙니다.

면접에서 설명할 때의 기준

면접에서는 resolvefulfilled를 같은 단어처럼 묶기보다 함수와 상태를 나누어 설명하면 충분합니다.

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

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...