동기 처리: 작업이 끝날 때까지 기다리는 동안 중지 상태가 되기 때문에 다른 작업을 할 수 없다. 수행하던 작업이 끝나야 비로소 그 다음 예정된 작업을 할 수 있다.
비동기 처리: 흐름이 멈추지 않기 때문에 동시에 여러 가지 작업을 처리할 수 있고, 기다리는 과정에서 다른 함수도 호출할 수 있다.
function work() {
const start = Date.now();
for (let i = 0; i < 1000000000; i++) {}
const end = Date.now();
console.log(end - start + 'ms');
}
work();
console.log('다음 작업');
여기서 Date.now()는 현재 시간을 숫자 형태로 가져오는 자바스크립트 내장 함수이다. 위 work 함수는 1,000,000,000 번 루프를 돌고 이 작업이 얼마나 걸렸는지 알려준다.
work() 함수가 호출되면, for문이 돌아갈 때는 다른 작업은 처리하지 않고 온전히 for문만 실행된다. for문을 마쳐야 비로소 다른 작업을 할 수 있다.
만약 이 작업이 진행되는 동안 다른 작업도 하고 싶다면 비동기 형태로 전환하여야 하는데, 이를 위해서 setTimeout이라는 함수를 사용한다.
function work() {
setTimeout(() => {
const start = Date.now();
for (let i = 0; i < 1000000000; i++) {}
const end = Date.now();
console.log(end - start + 'ms');
}, 0);
}
console.log('작업 시작!');
work();
console.log('다음 작업');
setTimeout 함수는 첫 번째 파라미터에 넣은 함수를 두 번째 파라미터에 넣은 시간(ms 단위)이 흐른 후 호출해 준다. 지금은 0을 넣었으므로, 이 함수는 바로 실행이 된다(0ms 이후에 실행한다는 의미이지만 실제로는 4ms 이후에 실행된다). setTimeout을 사용하면 우리가 정한 작업이 백그라운드에서 수행되기 때문에 기존의 코드 흐름을 거의 막지 않고 동시에 다른 작업을 진행할 수 있다.
결과물을 보면, for 루프가 돌아가는 동안 다음 작업도 실행되고 있고, for 루프가 끝나고 나서 몇 ms 걸렸는지가 출력됐다.
만약에 work() 함수가 끝난 다음에 다른 작업을 처리하고 싶다면 콜백 함수를 파라미터로 전달해주면 된다. 콜백 함수란, 어떤 함수의 매개변수로 함수를 넘기고 어떤 행위나 작업이 완료된 직후에 콜백 함수(매개변수로 받은 함수)를 호출하는 것이다. 즉 함수 내에서 매개변수 함수를 실행하는 것이다.
function work(callback) {
setTimeout(() => {
const start = Date.now();
for (let i = 0; i < 1000000000; i++) {}
const end = Date.now();
callback(end - start + 'ms'); // 매개변수의 함수(콜백 함수) 호출
}, 0);
}
console.log('작업 시작!');
work((endTime) => {
console.log(endTime);
console.log('작업이 끝났어요!')
});
console.log('다음 작업');
하단에서 work() 함수를 호출할 때 매개변수로 endTime 함수 자체를 전달하는 것을 볼 수 있다. work() 함수가 실행되면 실행문 안에서 work()의 매개변수인 callback이 호출된다.
비동기적으로 처리하는 작업들은 다음과 같다.
비동기 작업을 다룰 때에는 콜백 함수 외에도 Promise, async/await라는 문법을 사용하여 처리할 수 있다.
프로미스는 비동기 작업을 더 편리하게 처리할 수 있도록 ES6에 도입된 기능이다.
아래 코드는 숫자 n을 파라미터로 받아와서 다섯번에 걸쳐 1초마다 1씩 더해서 출력하는 작업을 setTimeout 으로 구현한 것이다.
function increaseAndPrint(n, callback) {
setTimeout(() => {
const increased = n + 1;
console.log(increased);
if (callback) {
callback(increased);
}
}, 1000);
}
increaseAndPrint(0, n => { // 1
increaseAndPrint(n, n => { // 2
increaseAndPrint(n, n => { // 3
increaseAndPrint(n, n => { // 4
increaseAndPrint(n, n => { // 5
console.log('끝!'); // 끝!
});
});
});
});
});
위와 같은 식의 코드를 Callback Hell(콜백지옥)이라고 부른다. 이처럼 비동기적으로 처리해야 하는 일이 많아질수록 코드의 깊이가 계속 깊어지는 현상이 발생하는데, Promise를 사용하면 코드의 깊이가 깊어지는 현상을 방지할 수 있다.
Promise는 다음과 같이 만든다.
const myPromise = new Promise((resolve, reject) => {
// 구현..
})
Promise는 성공할 수도 있고, 실패할 수도 있다. 성공하는 상황에서는 resolve를 호출해 주면 되고, 실패할 때는 reject를 호출해 주면 된다.
1) 1초 뒤에 성공시키는 상황(실패 고려 X)
const myPromise = new Promise((resolve, reject) => { setTimeout(() => { resolve(1); }, 1000); }); myPromise.then(n => { console.log(n); });
resolve를 호출할 때 특정 값을 파라미터로 넣어주면, 이 값을 작업이 끝나고 나서 사용할 수 있다. 작업이 끝나고 나서 또 다른 작업을 해야할 때는 Promise 뒤에.then(...)을 붙여서 사용하면 된다.
2) 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를 통하여 실패했을 시 수행할 작업을 설정할 수 있다.
3) Promise를 만드는 함수 작성
Promise의 속성 중에는 then 내부에 넣은 함수에서 또 Promise를 리턴하게 되는 경우를 연달아서 사용할 수 있다. 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(n => { return increaseAndPrint(n); }) .then(n => { return increaseAndPrint(n); }) .then(n => { return increaseAndPrint(n); }) .then(n => { return increaseAndPrint(n); }) .catch(e => { console.error(e); });위 코드를 이렇게 정리할 수 있다.
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) .catch(e => { console.error(e); });실행 결과
async/await 문법은 Promise를 더욱 쉽게 사용할 수 있게 해준다.
async/await 문법을 사용할 때는 함수 선언 시 앞부분에 async키워드를 붙여준다. 그리고 Promise의 앞부분에 awiat을 넣어 주면 해당 프로미스가 끝날 때까지 기다렸다가 다음 작업을 수행할 수 있다.
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function process() {
console.log('안녕하세요!');
await sleep(1000); // 1초 쉬고
console.log('반갑습니다!');
}
process().then(() => {
console.log('작업이 끝났어요!!');
});
위 코드에서는 sleep이라는 함수를 만들어서 파라미터로 넣어준 시간만큼 기다리는 Promise를 만들고, 이를 process함수에서 사용하였다. 함수에서 async를 사용하면, 해당 함수는 결괏값으로 Promise를 반환하게 된다. 따라서 process().then(() => { ...과 같은 코드를 작성할 수 있다.
결과
async 함수에서 에러를 발생시킬 때는 throw를 사용하고, 에러를 잡아낼 때에는 try/catch문을 사용한다.
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function makeError() {
await sleep(1000);
const error = new Error();
throw error;
}
async function process() {
try {
await makeError();
} catch (e) {
console.error(e);
}
}
process();