
자바스크립트는 기본적으로 소스 코드가 작성된 순서대로 처리하는데 이런 방식을 동기 처리 방식이라고 한다. 동기 처리 방식은 다음과 같이 함수가 호출되는 순서대로 실행되어 앞의 함수가 실행이 완료된 후에 다음 함수가 실행되는 처리 방식으로 싱글 스레드 방식이라고도 한다.
<script>
// 동기 방식 처리
function run01() {
console.log("run01");
}
function run02() {
console.log("run02");
}
function run03() {
console.log("run03");
}
run01();
run02();
run03();
</script>
자바스크립트는 한 번에 하나만 처리하는 싱글 스레드(Single Thread) 방식으로 실행되는 프로그래밍 언어로 시간이 많이 걸리는 작업을 별도로 처리해 싱글 스레드의 단점을 보완하고 있다. 예를 들어 서버에서 자료를 받아 오거나 DB나 파일에서 자료를 읽어오는 경우에 해당 작업이 끝날 때 까지 대기하지 않고 다음 작업이 바로 실행되도록 설계되어 있다. 하지만 상황에 따라서 이러한 실행 순서를 조정해서 호출되는 순서 또는 원하는 순서대로 실행해야 한다면 어떻게 해야 할까?
<script>
// 비동기 방식 처리를 해야 한다면?
function order() {
console.log(“음식을 주문 받음");
}
function make() {
setTimeout(function() {
console.log("음식 조리 중...");
}, 3000);
}
function complete() {
console.log("조리 완료");
}
order();
make();
complete();
</script>
<script>
// 콜백 함수(Callback function) 방식
function order() {
console.log("음식을 주문 받음");
}
function make(callback) {
setTimeout(function() {
console.log("음식 조리 중...");
callback();
}, 3000);
}
function complete() {
console.log("조리 완료");
}
order();
make(complete);
</script>
자바스크립트에서는 여러 함수를 작성하여 실행하게 되는데 실행 시간이 다른 함수를 원하는 처리 순서에 따라서 실행되도록 프로그래밍 하는 것을 비동기 처리라고 한다.
예를 들어 서버에서 자료를 받아 오거나 DB에서 자료를 받아와서 출력하는 프로그램을 작성한다면 서버 또는 DB에서 자료를 받아 오는 시간이 많이 소요된다 하더라도 자료를 받아 오는 함수의 실행이 완료된 후에 필요한 데이터를 출력할 수 있기 때문에 자료를 모두 받아 올 때까지 기다렸다가 화면에 출력하는 함수가 실행되어야 한다. 그러므로 이런 경우에 비동기 처리를 해야 한다.
자바스크립트에서 사용하는 비동기 처리 방식에는 다음과 같은 3가지 방식이 있다.
콜백 함수(Callback function), 프로미스(Promise), async/await 방식을 사용한다.
콜백 함수 방식은 전통적으로 사용되던 비동기 처리 방식이며 프로미스(Promise) 방식은
콜백 함수의 단점인 콜백 지옥(Callback Hell)을 보완하고자 ECMAScript 2015(ES6)에서
도입된 방식이며 async/await 방식은 프로미스를 보완하기 위해서 ECMAScript 2017에서
도입된 방식이다.

Promise 방식은 비동기 처리에서 콜백 지옥을 만들지 않기 위해서 ECMAScript 2015에서 도입된 기능이다. Promise 객체와 콜백 함수를 사용해 실행 순서를 제어하는 방식으로 어떤 작업이 성공했을 때 실행할 콜백 함수와 실패 했을 때 실행할 콜백 함수를 등록해 처리하는 방식으로 비동기로 처리해야 할 작업이 많아져도 Promise를 사용하면 코드의 깊이가 깊어지는 현상을 방지할 수 있다.
<script>
// Promise 객체를 만드는 코드 - 제작 코드(Producing code)라고 함
const isLike = true;
const order = new Promise((resolve, reject) => {
if(isLike) { // 작업이 성공할 때는 resolve() 함수 호출
// resolve() 함수의 인수는 소비 코드의 then() 메서드의 콜백 함수로 전달됨
resolve('음식을 주문함');
} else { // 작업이 실패할 때는 reject() 함수 호출
// reject() 함수의 인수는 소비 코드로 catch() 메서드의 콜백 함수로 전달됨
reject('음식을 주문하지 않음');
}
});
// Promise를 사용하는 코드 - 소비 코드(Consuming code)라고 함
order
.then(result => {
console.log(result);
return "- " + '맛나게 해주세요';
})
// 추가적으로 다른 작업이 필요할 때 연속해서 then() 메서드를 호출
.then(result => console.log(result))
.catch(err => console.log(err))
// 성공과 실패에 상관없이 실행되는 finally() 메서드 - 생략 가능
.finally(() => console.log("종료"));
</script>
Promise를 사용하는 코드 - 소비 코드(Consuming code)라고 함
비동기로 처리할 작업이 많아져도 콜백 지옥처럼 코드가 깊어지지는 않지만 에러가 발생하면 어디서 발생했는지 찾기가 어렵고 조건에 따라서 분기하거나 작업을 나누는 것이 어렵고 어떤 값을 공유하면서 처리하는 것도 난해하다.
<script>
// Promise 방식으로 콜백 지옥 벗어나기
// Promise 객체를 만드는 코드 - 제작 코드(Producing code)라고 함
const incrementNum = (n) => {
return new Promise((resolve, reject) => {
console.log(n);
setTimeout(() => {
const num = n + 1;
if(num > 5) {
const err = new Error();
err.name = 'num의 값이 5를 초과함';
reject(err);
return;
}
resolve(num);
}, 500);
});
}
incrementNum(1)
.then(n => incrementNum(n))
.then(n => incrementNum(n))
.then(n => incrementNum(n))
.then(n => incrementNum(n))
.then(n => incrementNum(n))
.then(n => incrementNum(n))
.catch(err => console.error(err))
.finally(() => console.log('종료'));
</script>
async/await 방식은 Promise를 쉽게 사용할 수 있도록 하는 기능이다. 함수를 선언할 때 다음과 같이 함수 앞에 async 예약어를 사용하면 그 함수 안에서 await 예약어를 사용해서 Promise를 반환하는 함수가 작업을 완료할 때까지 기다렸다가 다음 작업을 실행할 수 있도록 할 수 있다.
<script>
// async/await 방식
function order() {
console.log("음식을 주문 받음");
}
function make() {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve("음식을 조리 중...");
}, 3000);
});
}
function complete() {
console.log("조리 완료");
}
(async function init() {
order();
await make().then(console.log);
complete();
})();
</script>
위에서 order() 함수를 정의할 때 함수 앞에 async를 붙였기 때문에 아래와 같이 order() 함수를 호출하면 Promise 객체가 반환된다. 또한 Promise 객체 앞에 await을 사용했기 때문에 이 함수가 실행을 완료할 때까지 기다렸다가 다음 작업을 실행한다.
아래와 같이 Promise 객체 앞에 await이 붙으면 Promise를 반환하는 것이 아니라 Promise 안에서 호출한 resolve() 함수의 인수로 전달한 데이터가 반환된다.
<script>
// async/await 방식
/* 함수를 선언할 때 함수 앞에 async 예약어를 붙이면
* 이 함수는 실행한 결과 값으로 Promise 객체를 반환 한다.
**/
async function order(menu) {
console.log(`${menu}을 주문 받음`);
return menu;
}
async function make(menu) {
console.log(`${menu}을 조리 중...`);
return menu;
}
async function complete(menu) {
return `${menu} 조리 완료`;
}
/* 다음과 같이 함수 앞에 async 예약어를 사용하면 이 함수 안에서 Promise를
* 반환하는 함수를 호출할 때 await 예약어를 사용해 비동기 처리를 할 수 있다.
**/
(async function init() {
const step1 = await order('갈비탕');
const step2 = await make(step1);
complete(step2).then(console.log);
})().
then(() => {
console.log("주문 처리 완료");
});
</script>
<script>
// Promise 방식을 async/await 방식으로 변경하기
const incrementNum = (n, ms) => {
return new Promise((resolve, reject) => {
console.log(n);
setTimeout(() => {
const num = n + 1;
if(num > 5) {
const err = new Error();
err.name = 'num의 값이 5를 초과함';
reject(err);
return;
}
resolve(num);
}, ms);
});
}
/* 다음과 같이 함수 앞에 async 예약어를 사용하면 이 함수 안에서 Promise를
* 반환하는 함수를 호출할 때 await 예약어를 사용해 비동기 처리를 할 수 있다.
**/
async function increment() {
const step1 = await incrementNum(1, 1000);
const step2 = await incrementNum(step1, 2000);
const step3 = await incrementNum(step2, 500);
const step4 = await incrementNum(step3, 1000);
const step5 = await incrementNum(step4, 2000);
const step6 = await incrementNum(step5, 1000);
const step7 = await incrementNum(step6, 500);
}
increment()
.then()
.catch(console.log);
</script>