주로 다음과 같은 작업들을 비동기적으로 처리하게 됩니다.
Ajax Web API 요청
서버쪽에서 데이터를 받와아야 할 때, 요청을 하고 서버에서 응답을 할 때 까지 대기를 해야 되기 때문에 작업을 비동기적으로 처리합니다.
파일 읽기
주로 서버 쪽에서 파일을 읽어야 하는 상황에는 비동기적으로 처리합니다.
암호화/복호화
암호화/복호화를 할 때에도 바로 처리가 되지 않고, 시간이 어느정도 걸리는 경우가 있기 때문에 비동기적으로 처리합니다.
작업 예약
단순히 어떤 작업을 몇초 후에 스케쥴링 해야 하는 상황에는, setTimeout 을 사용하여 비동기적으로 처리합니다.
비동기 작업을 다룰 때에는 callback 함수 외에도 Promise, 그리고 async/await 라는 문법을 사용하여 처리 할 수 있습니다.
01. Promise
프로미스는 비동기 작업을 조금 더 편하게 처리 할 수 있도록 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 => {
increaseAndPrint(n, n => {
increaseAndPrint(n, n => {
increaseAndPrint(n, n => {
increaseAndPrint(n, n => {
console.log('끝!');
});
});
});
});
});
코드 읽기가 복잡합니다. 이런 식의 코드를 Callback Hell (콜백지옥) 이라고 부릅니다.
비동기적으로 처리해야 하는 일이 많아질수록, 코드의 깊이가 계속 깊어지는 현상이 있는데,
Promise 를 사용하면 이렇게 코드의 깊이가 깊어지는 현상을 방지 할 수 있습니다.
const myPromise = new Promise((resolve, reject) => {
// 구현..
})
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((n) => {
console.log('result: ', n);
})
여기까지만 보면, 결국 함수를 전달하는건데, 뭐가 다르지 싶을수도 있습니다. 하지만, Promise 의 속성 중에는,
만약 then 내부에 넣은 함수에서 또 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);
})
.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)
.then(increaseAndPrint)
.catch(e => {
console.error(e);
});
Promise 를 사용하면, 비동기 작업의 개수가 많아져도 코드의 깊이가 깊어지지 않게 됩니다.
하지만 이것도 불편한점이 있습니다. 에러를 잡을 때 몇번째에서 발생했는지 알기 어렵고 특정 조건에 따라 분기를 나누는 작업도 어렵고,
특정 값을 공유해가면서 작업을 처리하기도 까다롭습니다.
이는 async/await 을 사용하면 깔끔하게 해결 할 수 있습니다.
02. async/await
async/await 문법은 ES8에 해당하는 문법으로서, Promise 를 더욱 쉽게 사용 할 수 있게 해줍니다.
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function process() {
console.log('안녕하세요!');
await sleep(1000); // 1초쉬고
console.log('반갑습니다!');
}
process();
async/await 문법을 사용할 때에는, 함수를 선언 할 때 함수의 앞부분에 async를 붙여줍니다.
그리고 Promise 의 앞부분에 await 을 넣어주면 해당 프로미스가 끝날때까지 기다렸다가 다음 작업을 수행 할 수 있습니다.
위 코드는 sleep 이라는 함수에 파라미터로 넣어준 시간 만큼 기다리는 Promise 를 만들고, 이를 process 함수에서 사용했습니다.
함수에서 async 를 사용하면, 해당 함수는 결과값으로 Promise 를 반환하게 됩니다. 따라서 다음과 같이 코드를 작성 할 수 있습니다.
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('작업이 끝났어요!');
});
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();
이번에는, 비동기 함수를 몇개 더 만들어보겠습니다.
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const getDog = async () => {
await sleep(1000);
return '멍멍이';
};
const getRabbit = async () => {
await sleep(500);
return '토끼';
};
const getTurtle = async () => {
await sleep(3000);
return '거북이';
};
async function process() {
const dog = await getDog();
console.log(dog);
const rabbit = await getRabbit();
console.log(rabbit);
const turtle = await getTurtle();
console.log(turtle);
}
process();
현재 위 코드에서는 getDog 는 1초, getRabbit 은 0.5초, getTurtle 은 3초가 걸리고 있습니다.
이 함수들을 process 함수에서 연달아 사용하게 되면서, process 함수가 실행되는 총 시간은 4.5초가 됩니다.
지금은 getDog -> getRabbit -> getTurtle 순서대로 실행이 되고 있는데, 하나가 끝나야 다음 작업이 시작하고 있습니다.
만약 동시에 작업을 시작하고 싶다면, Promise.all 을 사용하면 됩니다.
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const getDog = async () => {
await sleep(1000);
return '멍멍이';
};
const getRabbit = async () => {
await sleep(500);
return '토끼';
};
const getTurtle = async () => {
await sleep(3000);
return '거북이';
};
async function process() {
const results = await Promise.all([getDog(), getRabbit(), getTurtle()]);
console.log(results);
}
process();
만약에 여기서 배열 비구조화 할당 문법을 사용한다면 각 결과값을 따로 따로 추출해서 조회 할 수 있습니다.
async function process() {
const [dog, rabbit, turtle] = await Promise.all([
getDog(),
getRabbit(),
getTurtle()
]);
console.log(dog);
console.log(rabbit);
console.log(turtle);
}
process();
Promise.all 를 사용 할 때에는, 등록한 프로미스 중 하나라도 실패하면, 모든게 실패 된 것으로 간주합니다.
이번에는 Promise.race 라는 것에 대해서 알아보겠습니다. 이 함수는 Promise.all 과 달리, 여러개의 프로미스를 등록해서 실행했을 때
가장 빨리 끝난것 하나만의 결과값을 가져옵니다.
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const getDog = async () => {
await sleep(1000);
return '멍멍이';
};
const getRabbit = async () => {
await sleep(500);
return '토끼';
};
const getTurtle = async () => {
await sleep(3000);
return '거북이';
};
async function process() {
const first = await Promise.race([
getDog(),
getRabbit(),
getTurtle()
]);
console.log(first); // 토끼
}
process();
Promise.race 의 경우, 다른 Promise 가 먼저 성공하기 전에 가장 먼저 끝난 Promise 가 실패하면 이를 실패로 간주합니다.
따라서, 현재 위의 코드에서 getRabbit 에서 에러가 발생한다면 잡아낼 수 있지만, getTurtle 이나 getDog 에서 발생한 에러는 무시됩니다.
사실 race같은 경우는 사용할 일이 많지는 않습니다. 어떤 작업을 시작하고나서 도중에 작업을 취소처리하는 경우에 종종 사용되곤합니다.