velopert님의 모던 자바스크립트 강의자료를 참고하여 작성했습니다.
우선 프로그래밍에서 동기(sync)와 비동기(async)의 정의를 이해할 필요가 있다.
동기 : 이전 작업의 실행이 끝나야 다음 작업 실행을 시작한다.
비동기 : 이전 작업의 실행과 무관하게 다음 작업을 실행한다.
동기와 비동기를 설명할 때 대표적인 예시로 은행과, 카페를 예시로 들곤한다. 은행에서는 사람들에게 번호표를 나눠주고, 이전 사람의 은행 일이 끝날 때 까지는 계속 대기해야 한다. 반면에 카페에서는 주문과 함게 진동벨을 받게 되고 진동벨이 울리기 전에 대기하면서 개인 작업을 할 수 있다.
싱글스레드인 자바스크립트는 기본적으로 동기식이지만, API에 요청을 보낼 때 응답이 올 때까지 마냥 기다리기만 할 수 없기 때문에 비동기 처리가 필요하다.
하지만 비동기 처리를 할 경우, 의도하지 않은 순서로 함수가 실행될 수 있기 때문에 원하는 부분에서 동기 방식으로 변환을 해줘야 한다.
첫번째 방식이 지난번에 살펴본 콜백함수이다. 콜백함수는 함수안에서 또 다른 함수를 호출하는 것인데 여러 함수를 순서대로 호출할 필요가 있을 경우 콜백지옥을 경험하게 되는 문제점이 있다.
숫자 n 을 파라미터로 받아와서 다섯번에 걸쳐 1초마다 1씩 더해서 출력하는 작업을 setTimeout 으로 구현해보자.
function increaseAndPrint(n, callback) {
setTimeout(() => {
const increased = n + 1;
console.log(increased);
if (callback) {
callback(increased);
}
}, 1000);
}
increaseAndPrint(0, n => {
increaseAndPrint(n, n => {
increaseAndPrint(n, n => {
increaseAndPrint(n, n => {
increaseAndPrint(n, n => {
console.log('끝!');
});
});
});
});
});
비동기를 동기 방식으로 변환하는 두 번째 방법은 Promise객체를 사용하는 것이다. 이번 글에서는 Promise에 대해 알아보자.
Promise는 성공할 수도 있고, 실패할 수 도 있다. 성공할 때는 resolve 함수를 호출하고, 실패할때는 reject 함수를 호출한다.
const myPromise = new Promise((resolve, reject) => {
// 구현..
})
1초 뒤 성공하는 상황을 구현해 보자
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
myPromise.then(n => {
console.log(n);
});
resolve 를 호출 할 때 특정 값을 파라미터로 넣어주면, 이 값을 작업이 끝나고 나서 사용 할 수 있다. 작업이 끝나고 나서 또 다른 작업을 해야 할 때에는 Promise 뒤에 .then(...) 을 붙여서 사용하면 된다.
이번에는 1초뒤 실패하는 상황을 만들어보자
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error());
}, 1000);
});
myPromise
.then(n => {
console.log(n);
})
.catch(error => {
console.log(error);
});
실패하는 상황에서는 reject
를 사용하고 .catch
를 통하여 실패했을시 수행할 작업을 설정할 수 있다.
이제 Promise를 만드는 함수를 작성해 보자.
function increaseAndPrint(n) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const value = n + 1;
if (value === 5) {
const error = new Error();
error.name = 'ValueIsFiveError';
reject(error);
return;
console.log(value);
resolve(value);
}, 1000);
});
}
increaseAndPrint(0)
.then(increaseAndPrint)
.then(increaseAndPrint)
.then(increaseAndPrint)
.then(increaseAndPrint)
.then(increaseAndPrint)
.catch(e => {
console.error(e);
});
Promise 를 사용하면, 비동기 작업의 개수가 많아져도 코드의 깊이가 깊어지지 않게 된다.
프로미스 객체는 생성자를 사용해서 만들 수 있다.
생성자의 인자로 executor라는 함수를 이용한다.
new Promise( /*executor*/ );
executor함수는 resolve와 reject를 인자로 가진다.
(resolve, reject) ⇒ {...}
여기서 resolve와 reject는 각각 함수이다.
new Promise( /*executor*/ (resolve, reject) => {...} );
생성자를 통해 프로미스 객체를 만드는 순간 pending(대기)
상태라고 한다.
new Promise((resolve, reject) => {}); // pending
executor함수 인자중 하나인 resolve함수를 실행하면, fulfilled(이행) 상태가 된다.
new Promise((resolve, reject) => {
//pending
//... 비동기 처리
resolve(); // fulfilled
});
executor함수 인자중 하나인 reject함수를 실행하면, rejected(거부) 상태가 된다.
new Promise((resolve, reject) => {
reject(); // rejected
});
p라는 프로미스 객체는 1초 후에 fulfilled된다.
또한 fulfilled된 시점에 p.then안에 설정한 callback함수가 실행된다.
const p = new Promise((resolve, reject) => {
/*pending*/
setTimeout(() => {
resolve(); /* fulfilled */
}, 1000);
});
p.then(() => {
/* resolve 된 이후에 실행됨*/
/* callback */
console.log('1000ms후에 fulfilled 됩니다.');
});
then을 설정하는 시점을 명확히하고 함수의 실행과 동시에 프로미스 객체를 만들면서 pending이 시작하도록 하기 위해 프로미스 객체를 생성하면서 리턴하는 함수 p를 만들어 p실행과 동시에 then을 설정한다.
function p() {
return new Promise((resolve, reject) => {
/*pending*/
setTimeout(() => {
resolve(); /* fulfilled */
}, 1000);
});
}
//원하는 시점에 프로미스 객체를 생성하고 콜백함수도 호출할 수 있다.
p().then(() => {
console.log('1000ms후에 fulfilled 됩니다.');
});
이번엔 reject상황도 만들어보자. reject는 then이라니라 catch를통해 콜백함수를 처리한다.
function p() {
return new Promise((resolve, reject) => {
/*pending*/
setTimeout(() => {
reject(); /* rejected */
}, 1000);
});
}
//원하는 시점에 프로미스 객체를 생성하고 콜백함수도 호출할 수 있다.
p()
.then(() => {
console.log('1000ms후에 fulfilled 됩니다.');
})
.catch(() => {
console.log('1000ms후에 rejected 됩니다.');
});
excecutor의 resolve함수에 인자를 넣어 실행하면, then의 callback함수의 인자로도 받을 수가 있다.
function p() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('hello');
}, 1000);
});
}
//원하는 시점에 프로미스 객체를 생성하고 콜백함수도 호출할 수 있다.
p().then(message => {
console.log('1000ms후에 fulfilled 됩니다.', message);
});
마찬가지로 excecutor의 reject함수에 인자를 넣어 실행하면, catch의 callback함수의 인자로도 받을 수가 있다.
function p() {
return new Promise((resolve, reject) => {
/*pending*/
setTimeout(() => {
reject('error');
}, 1000);
});
}
//원하는 시점에 프로미스 객체를 생성하고 콜백함수도 호출할 수 있다.
p()
.then(() => {
console.log('1000ms후에 fulfilled 됩니다.');
})
.catch(reason => {
console.log('1000ms후에 rejected 됩니다.', reason);
});
하지만 일반적으로는 error 문자열이 대신에 객체를만들어서 던진다.
function p() {
return new Promise((resolve, reject) => {
/*pending*/
setTimeout(() => {
reject(new Error('error');
}, 1000);
});
}
//원하는 시점에 프로미스 객체를 생성하고 콜백함수도 호출할 수 있다.
p()
.then(() => {
console.log('1000ms후에 fulfilled 됩니다.');
})
.catch(e => {
console.log('1000ms후에 rejected 됩니다.', e);
});
추가적으로 fulfilled 되거나 rejected된 후에 최종적으로 실행할 것이 있다면, .finally()
를 설정하고, 함수를 인자로 넣는다.
function p() {
return new Promise((resolve, reject) => {
/*pending*/
setTimeout(() => {
reject(new Error('error');
}, 1000);
});
}
//원하는 시점에 프로미스 객체를 생성하고 콜백함수도 호출할 수 있다.
p()
.then(() => {
console.log('1000ms후에 fulfilled 됩니다.');
})
.catch(e => {
console.log('1000ms후에 rejected 됩니다.', e);
})
.finally(() => {
console.log('end');
});
하지만, 이것도 불편한점이 있긴 하다. 에러를 잡을 때 몇번째에서 발생했는지 알아내기도 어렵고 특정 조건에 따라 분기를 나누는 작업도 어렵고, 특정 값을 공유해가면서 작업을 처리하기도 까다로운데 async/await 을 사용하면, 이러한 문제점을 깔끔하게 해결 할 수 있다.