Promise 비동기
Promise
Promise 는 비동기 작업의 단위 입니다. 지금까지 이야기했던 것들은 자바스크립트에서 동시에 여러 가지 작업을 할 수 있다는 개념을 비동기로 은근슬쩍 설명했고, 지금부터는 Promise 를 통해 어떻게 비동기 작업들을 쉽게 관리할 수 있는지를 본격적으로 알아보겠습니다.
기본 사용법
우선 Promise 로 관리할 비동기 작업을 만들 때에는, Promise 에서 요구하는 방법대로 만들어야 합니다. 여러가지 방법이 있지만 제일 정석적인 방법은 new Promise(...) 하는 것입니다. 아래 예제 코드를 봐주세요.
const promise1 = new Promise((resolve, reject) => {
// 비동기 작업
});
문법적으로 보충 설명해보겠습니다.
- 변수의 이름은 promise1 이며, const 로 선언했기 때문에 재할당이 되지 않습니다. 하나의 변수로 끝까지 해당 Promise 를 관리하는 것이 가독성도 좋고 유지보수도 하기 좋습니다.
- new Promise(...) 로 Promise 객체를 새롭게 만들었습니다. 생성자는 함수이므로 괄호() 를 써서 함수를 호출하는 것과 동일한 모습입니다.
- 생성자는 특별한 함수 하나를 인자로 받습니다. (여기서 인자로 들어가는 함수의 형태는 화살표 함수입니다.)
이 특별한 함수를 공식 문서에서는 executor 라는 이름으로 부릅니다. 이 함수에 대해 더 자세히 설명하면 다음과 같습니다.
- executor는 첫번째 인수로 resolve 이며, 두번째 인수로 reject 를 받습니다. (이름은 우리가 정의하는 거라서 아무렇게나 해도 사실 상관은 없지만, 국룰을 따르도록 합시다.)
- resolve 는 executor 내에서 호출할 수 있는 또 다른 함수입니다. resolve 를 호출하게 된다면 “이 비동기 작업이 성공했어!” 라는 뜻입니다.
- reject 또한 executor 내에서 호출할 수 있는 또 다른 함수입니다. reject 를 호출하게 된다면 “이 비동기 작업이 실패했어…” 라는 뜻입니다.
으악! 생성자 내에 함수 내에 또 다른 함수를 호출한다니 이게 무슨 말입니까! 이게 콜백보다 훨씬 복잡하면 복잡했지 결코 간단해보이지는 않는데요? 아니에요.. 편하다구요… 진짜 믿어주세요. async-await 까지 가야 Promise의 진면모가 드러난답니다. 하여튼 우리는 promise1 이라는 Promise 객체를 얻게 되었습니다!
Promise 의 특징은 new Promise(...) 하는 순간 여기에 할당된 비동기 작업은 바로 시작됩니다. 비동기 작업의 특징은 작업이 언제 끝날지 모르기 때문에 일단 배를 떠나보낸다고 이야기했습니다. 그럼 그 이후에 이 작업이 성공하거나 실패하는 순간에 우리가 또 뒷처리를 해줘야겠죠? Promise 가 끝나고 난 다음의 동작을 우리가 설정해줄 수 있는데, 그것이 바로 then 메소드와 catch 메소드입니다.
- then 메소드는 해당 Promise 가 성공했을 때의 동작을 지정합니다. 인자로 함수를 받습니다.
- catch 메소드는 해당 Promise 가 실패했을 때의 동작을 지정합니다. 인자로 함수를 받습니다.
- 위 함수들은 체인 형태로 활용할 수 있습니다. (연속적으로 호출할 수 있습니다. 아래 예제에서 확인하도록 합니다.)
executor 로 새로운 Promise 를 만든 다음 then 과 catch 를 이용하여 후속 동작까지 지정해줘야 어느정도 제대로 돌아가는 Promise를 만나보실 수 있습니다. 그래서 우선 설명을 엄청 나열했구요, 이제 드디어 예시 코드를 확인해봅시다.
const promise1 = new Promise((resolve, reject) => {
resolve();
});
promise1
.then(() => {
console.log("then!");
})
.catch(() => {
console.log("catch!");
});
이것의 실행 결과는 다음과 같습니다.
then!
then 에 함수를 넣어주었고, 연속적으로 catch 에도 함수를 넣어줬습니다. 이 Promise 에서는 바로 resolve 가 호출되었기 때문에 성공으로 간주하여 then 에 있는 동작만 실행됩니다. 이제 아래와 같은 코드를 쓴다면 어떻게 될까요? resolve 부분을 reject 로 수정했습니다.
const promise1 = new Promise((resolve, reject) => {
reject();
});
promise1
.then(() => {
console.log("then!");
})
.catch(() => {
console.log("catch!");
});
이것의 실행 결과는 다음과 같습니다.
catch!
예상대로 catch! 만 출력됩니다.
위 예제에서는 비동기 작업이라고 해도 뭔가 기다리는 작업은 하나도 하지 않았습니다. 기다리는 작업은 아까 얘기했듯이 인터넷으로부터 데이터를 가져오는 작업이라든지, 파일을 읽고 쓰는 작업을 의미합니다. 기다리는 작업이 하나도 없다면 비동기를 쓸 이유는 없습니다. 위 예제들은 어디까지나 Promise 의 동작 방식을 설명하기 위한 예제임을 반드시 기억해주세요.
재사용하기
new Promise(...) 를 하는 순간 비동기 작업이 시작되는데, 비슷한 비동기 작업을 수행할 때마다 매번 new Promise(...) 를 해줘야 할까요? 그렇지 않습니다. 그럴 때는 그냥 new Promise(...) 한 것을 그대로 리턴하는 함수를 만들어 사용하면 됩니다. 아래 함수는 age 인자를 받아서 그 값에 따라 resolve 또는 reject를 호출합니다.
function startAsync(age) {
return new Promise((resolve, reject) => {
if (age > 20) resolve();
else reject();
});
}
setTimeout(() => {
const promise1 = startAsync(25);
promise1
.then(() => {
console.log("1 then!");
})
.catch(() => {
console.log("1 catch!");
});
const promise2 = startAsync(15);
promise2
.then(() => {
console.log("2 then!");
})
.catch(() => {
console.log("2 catch!");
});
}, 1000);
이것의 실행 결과는 다음과 같습니다.
1 then!
2 catch!
이제 startAsync 함수를 호출하는 순간 new Promise(...) 가 실행하게 되어 비동기 작업이 시작됩니다. 비동기 작업이 성공할지 실패할지는 장담할 수 없으므로 이전 예제와 동일하게 then 과 catch 로 후속 동작을 지정해 두었습니다.
promise1 은 성공하고, promise2 는 실패하도록 만들었는데요, 그래서 promise1 에서는 catch 으로 등록했던 함수는 실행되지 않고 then 으로 지정한 동작만 수행합니다. promise2 는 반대로 then 동작은 수행하지 않고 catch 동작만 수행합니다.
작업 결과를 전달하기
우리는 resolve, reject 함수에 인자를 전달함으로써 then 및 catch 함수에서 비동기 작업으로부터 정보를 얻을 수 있습니다. 바로 아래 예제를 보시죠.
function startAsync(age) {
return new Promise((resolve, reject) => {
if (age > 20) resolve(`${age} success`);
else reject(new Error(`${age} is not over 20`));
});
}
setTimeout(() => {
const promise1 = startAsync(25);
promise1
.then((value) => {
console.log(value);
})
.catch((error) => {
console.error(error);
});
const promise2 = startAsync(15);
promise2
.then((value) => {
console.log(value);
})
.catch((error) => {
console.error(error);
});
}, 1000);
이것의 실행 결과는 다음과 같습니다.
25 success
Error: 15 is not over 20
at /home/taehoon/Desktop/playground-nodejs/index.js:4:17
at new Promise (<anonymous>)
at startAsync (/home/taehoon/Desktop/playground-nodejs/index.js:2:10)
at Timeout._onTimeout (/home/taehoon/Desktop/playground-nodejs/index.js:17:20)
at listOnTimeout (internal/timers.js:554:17)
at processTimers (internal/timers.js:497:7)
기타 고려사항
이외 executor 를 만들때 조금 고려해야 할 부분은 다음과 같습니다. 이는 좀 더 자세한 내용이므로 빨리 async – await 의 매력을 느끼고 싶다면 넘어가도 좋습니다.
- executor 내부에서 에러가 throw 된다면 해당 에러로 reject 가 수행됩니다.
- executor 의 리턴 값은 무시됩니다.
- 첫 번째 reject 혹은 resolve 만 유효합니다. (두 번째부터는 무시됩니다. 이미 해당 함수가 호출되었다면 throw 또한 무시됩니다.)
아래 간단한 예시들로 살펴봅시다.
// catch 로 연결됩니다.
const throwError = new Promise((resolve, reject) => {
throw Error("error");
});
throwError
.then(() => console.log("throwError success"))
.catch(() => console.log("throwError catched"));
// 아무런 영향이 없습니다.
const ret = new Promise((resolve, reject) => {
return "returned";
});
ret
.then(() => console.log("ret success"))
.catch(() => console.log("ret catched"));
// resolve 만 됩니다.
const several1 = new Promise((resolve, reject) => {
resolve();
reject();
});
several1
.then(() => console.log("several1 success"))
.catch(() => console.log("several1 catched"));
// reject 만 됩니다.
const several2 = new Promise((resolve, reject) => {
reject();
resolve();
});
several2
.then(() => console.log("several2 success"))
.catch(() => console.log("several2 catched"));
// resolve 만 됩니다.
const several3 = new Promise((resolve, reject) => {
resolve();
throw new Error("error");
});
several3
.then(() => console.log("several3 success"))
.catch(() => console.log("several3 catched"));
아래는 그 결과입니다.
several1 success
several3 success
throwError catched
several2 catched
어차피 첫 번째 resolve, reject 만 영향을 주기 때문에, 한 번 해당 함수가 호출되면 바로 return 을 하여 비동기 작업을 빠져나가는 것이 여러모로 정신 건강에 도움이 됩니다. 아래는 이전에 작성했던 startAsync 함수를 살짝 손을 본 겁니다.
function startAsync(age) {
return new Promise((resolve, reject) => {
if (age > 20) {
// 뭔가를 합니다.
return resolve(`${age} success`);
}
// 또 뭔가를 합니다.
return reject(new Error(`${age} is not over 20`));
// 여기 있는 코드는 "명확하게" 실행되지 않습니다.
});
}
간단한 동작 원리와 의의
지금까지의 설명을 (이해하기 쉬운 형태로 왜곡하여) 그림으로 표현하자면 아래와 같습니다.

왼쪽은 단순한 비동기 작업을 나타내었고 오른쪽은 Promise 가 어떤 구조를 가지고 있는지를 나타내었습니다.
Promise는 세 가지 상태를 지닙니다. 바로 대기(pending), 이행(fulfilled), 거부(rejected) 이며 이행 상태일 때 then, 거부 상태일 때 catch 로 등록한 동작들이 실행됩니다. 지금까지 “성공” 이라고 이야기한 것들은 사실 “이행 상태“와 연계된 것이고, “실패“는 “거부“와 동일한 상태인 것입니다. 이러한 상태들이 실제로 어떤지 우리가 직접 확인할 수 있을까요? 아쉽게도 그런 방법은 없습니다. 자바스크립트를 실행하는 브라우저 혹은 Node.js 에서 알아서 관리하는 녀석들이라 베일에 싸여 있습니다.
Promise 의 의의를 한마디로 이야기해보죠. Promise 는 비동기 작업을 생성/시작하는 부분(new Promise(...))과 작업 이후의 동작 지정 부분(then, catch)을 분리함으로써 기존의 러프한 비동기 작업보다 유연한 설계를 가능토록 합니다.
요약
지금까지 열심히 달려왔습니다. Promise 를 만드는 순간 비동기 작업이 시작되며, 비동기 작업을 성공으로 간주하고 싶을 때 resolve를 호출하고, 실패라 간주하고 싶다면 reject 함수를 호출합니다. 이 비동기 작업이 성공했을 때 후속 조치를 지정하고 싶다면 then으로, 실패 시의 후속 조치는 catch 로 지정하는 것까지 함께 살펴보았습니다.