이 전에 배운 콜백 함수는 비동기 처리를 할 때 콜백 지옥에 빠질 수 있다는 단점이 있었다. 콜백 지옥을 해결하기 위해 각 동작을 독립적인 함수로 만들어도 코드의 가독성이 떨어지고 재사용이 불가능하다는 단점이 생겼다.
ES6에서는 비동기 처리를 위한 또 다른 방법으로 프로미스(Promise)를 도입했다. 프로미스는 콜백이 가진 단점을 보완하면서 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.
프로미스는 자바스크립트 비동기 처리에 사용되는 객체이다.
아래 코드에 new Promise
에 전달되는 함수는 executor(실행 함수)라고 부른다. new Promise
가 만들어질 때 자동으로 실행되며 결과를 최종적으로 만들어내는 제작 코드를 포함한다.
let promise = new Promise(function(resolve, reject){
// executor
// 원하는 일 처리!
})
executor의 인수 resolve와 reject는 자바스크립트에서 자체 제공하는 콜백이다. executor의 처리가 끝나면 처리 성공 여부에 따라 resolve나 reject를 호출한다.
👉 콜백 중 하나는 반드시 호출해야한다.
- resolve(value) : 일이 성공적으로 끝난 경우 그 결과를 나타내는 value와 함께 호출
- reject(error) : 에러 발생 시 에러 객체를 나타내는 error와 함께 호출
📌 아래 코드처럼 resolve(data)의 data나 return한 값은 then에서 파라미터로 쓸 수 있다!
return new Promise((resolve, rejects) => { fs.readFile(filePath, "utf8", (err, data) => { if(err){ rejects(err); }else{ resolve(data); } }) });
new Promise
생성자가 반환하는 promise 객체는 다음과 같은 내부 프로퍼티를 갖는다.
pending(대기)
이었다가 resolve가 호출되면 fulfilled(이행)
, reject가 호출되면 rejected(실패)
로 변한다.undefined
이었다가 resolve(value)가 호출되면 value
로, reject(error)가 호출되면 error
로 변한다.👇 예시
let promise = new Promise(function(resolve, reject) {
// 프라미스가 만들어지면 executor 함수는 자동으로 실행됩니다.
// 1초 뒤에 일이 성공적으로 끝났다는 신호가 전달되면서 result는 '완료'가 됩니다.
setTimeout(() => resolve("완료"), 1000);
});
let promise = new Promise(function(resolve, reject) {
// 1초 뒤에 에러와 함께 실행이 종료되었다는 신호를 보냅니다.
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});
then의 리턴값은 프로미스 객체이다.
.then
의 첫 번째 인수는 프라미스가 이행되었을 때 실행되는 함수이고 여기서 실행 결과를 받는다..then
의 두 번째 인수는 프라미스가 거부되었을 때 실행되는 함수이고 여기서 에러를 받는다.promise.then(
function(result) { /* 결과(result)를 다룬다 */ },
function(error) { /* 에러(error)를 다룬다 */ }
);
✅ 프로미스가 성공적으로 이행됐을 경우
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("완료!"), 1000);
});
// resolve 함수는 .then의 첫 번째 함수(인수)를 실행한다.
promise.then(
result => alert(result), // 1초 후 "완료!"를 출력
error => alert(error) // 실행되지 않음
);
❎ 프로미스가 거부된 경우
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("에러 발생!")), 1000);
});
// reject 함수는 .then의 두 번째 함수를 실행한다.
promise.then(
result => alert(result), // 실행되지 않음
error => alert(error) // 1초 후 "Error: 에러 발생!"을 출력
);
✅ 작업이 성공적으로 처리된 경우만 다루고 싶다면
let promise = new Promise(resolve => {
setTimeout(() => resolve("완료!"), 1000);
});
promise.then(alert); // 1초 뒤 "완료!" 출력
.catch
는 프라미스에서 발생한 모든 에러를 다룬다. reject()가 호출되거나 에러가 던져지면 .catch
에서 이를 처리한다.
에러가 발생한 경우만 다루고 싶다면 .then(null, errorHandlingFunction)
같이 null을 첫 번째 인수로 전달하거나 .catch(errorHandlingFunction)
를 쓰면 된다.
👉 .catch
는 .then
에 null을 전달하는 것과 동일하게 작동한다.
💡 참고
reject됐을 때 catch가 없으면 상태는 rejected, catch를 쓰면 fulfilled가 된다.
프라미스 체이닝은 result가 .then
핸들러의 체인(사슬)을 통해 전달된다는 점에서 착안한 아이디어이다. 아래 코드는 다음과 같은 순서로 실행된다.
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return result * 2;
}).then(function(result) {
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
프라미스 체이닝이 가능한 이유는 promise.then을 호출하면 프라미스가 반환되기 때문이다. 반환된 프라미스엔 당연히 .then을 호출할 수 있다.
핸들러(.then(handler) / then의 전달인자)가 값을 반환하면 이 값이 프로미스의 result가 된다.
async와 await를 사용하면 프라미스를 좀 더 편하게 사용할 수 있다.
function 앞에 async
를 붙이면 해당 함수는 항상 프로미스를 반환한다. 프로미스가 아닌 것은 프로미스로 감싸서 반환한다.
아래 코드를 실행하면 result가 1인 이행 프로미스를 반환한다.
async function f() {
return 1;
}
f().then(alert); // 1
await는 async 함수 안에서만 동작한다. 자바스크립트는 await 키워드를 만나면 프로미스가 처리될 때까지 기다리고 결과는 그 이후 반환된다.
프라미스가 처리되길 기다리는 동안엔 엔진이 다른 일(다른 스크립트를 실행, 이벤트 처리 등)을 할 수 있기 때문에, CPU 리소스가 낭비되지 않는다.
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000)
});
let result = await promise; // 프라미스가 이행될 때까지 기다림
alert(result); // "완료!"
}
f();
🚨 주의
일반 함수에는await
를 사용할 수 없다. async 함수에만 사용해야 한다.
프라미스가 정상적으로 이행되면 await promise는 프로미스 객체의 result에 저장된 값을 반환한다. 반면 프로미스가 거부되면 마치 throw문을 작성한 것처럼 에러가 던져진다.
아래 두 개의 코드는 동일하게 작동한다.
async function f() {
await Promise.reject(new Error("에러 발생!"));
}
async function f() {
throw new Error("에러 발생!");
}
await가 던진 에러는 throw가 던진 에러를 잡을 때처럼 try..catch를 사용해 잡을 수 있다.
async function f() {
try {
let response = await fetch('http://유효하지-않은-주소');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}
f();
try..catch가 없으면 아래 예시의 async 함수 f()를 호출해 만든 프로미스가 거부 상태가 됩니다. f()에 .catch를 추가하면 거부된 프라미스를 처리할 수 있다.