게시: 2020년 12월 28일
JavaScript는 매우 유연하고 편리한 비동기식 언어이다. 글쓴이도 JavaScript 생태계를 매우 좋아하며 ReactJS, Node.js 등을 자주 사용하고 있다. (최근에는 Go로 갈아타려고 공부중이만..크흠) 정말 편리하고 좋은 언어인 JavaScript에서 가장 불편한 점이 있다면 바로 Callback 지옥일 것이다. 비동기 언어를 동기식으로 처리하려다 보니 Callback 안에 Callback 안에 Callback 안에 Callback이... 이러한 Callback 지옥은 코드 가독성을 떨어뜨려 협업에서 치명적인 것은 물론이거니와... 자신이 짠 코드마저 2주 뒤에 다시 보면 암호 해석을 해야하는 상황이 발생하게 된다. 이러한 문제점에 대한 해결책으로 나온 것이 Promise이고 이 마저도 코드 가독성을 떨어뜨리기 시작하자 async/await를 함께 사용하는 처리가 대세가 되고 있다.
JavaScript의 Callback 지옥을 해결하기 위한 첫 번째 방안인 Promise이다. Promise 내부에 코드를 작성해 코드가 정상적으로 작동한다면 resolve, 비정상적으로 작동한다면 reject로 코드 작성자가 지정할 수 있으며, 해당 Promise를 할당 받은 변수에서 .then().catch().finally 등으로 결과 값을 처리할 수 있다.
2초가 지난 후 0 ~ 9까지의 랜덤한 수를 생성한 후 짝수면 값을 반환하고 홀수면 에러를 반환하는 코드를 Promise를 사용해서 작성해보자.
const isRandomNumberEven = new Promise((resolve, reject) => {
setTimeout(() => {
let random = Math.floor(Math.random() * 10);
if (random % 2 === 0) {
resolve(random);
} else {
reject(new Error("The random number is odd"));
}
}, 2000);
});
isRandomNumberEven
.then(random => {
// 짝수이면
console.log(`random number is ${random}`);
})
.catch(error => {
// 홀수이면
console.log(error);
})
.finally(() => {
// 짝수이든 홀수이든
console.log(
"The handler is called when the promise is settled, whether fulfilled or rejected."
);
});
Promise를 사용하며 주의해야할 점은 Promise가 생성되자마자 Promise 내부 코드가 실행된다는 점이다. 위의 코드에서 Promise의 내부 코드는 isRandomNumberEven.then()... 부분에 실행되는 것이 아니고 new Promise()... 부분에서 바로 실행된다. isRandomNumberEven.then()... 부분에서는 그저 Promise의 내부 코드가 동작한 결과만을 가지고 있을 뿐이다. 이를 항상 생각하면서 불필요하게 Promise가 동작하지 않게 주의해야 한다.
위의 Promise를 사용하면 비동기식으로 동작하는 코드들을 정리할 수 있을 것 같았지만... Promise가 코드 내에서 굉장히 많이 사용되면서 오히려 가독성이 떨어지는 상황이 발생했다. Callback 지옥을 해결하니 Promise 지옥이 펼쳐진 셈이다. 그래서 나온 Callback 지옥을 해결하기 위한 두 번째 해결책이 async/await 이다. 주의해야할 점은 await는 async 함수 내부에서만 사용 가능하다는 점이다. 사용법은 정말 간단하다.
먼저, async 함수의 사용법이다. 함수 선언 앞에 async만 붙여주면 된다. 그 후 처리 방법은 Promise.then().catch().finally()와 동일하다.
async function func() {
return 1;
}
func().then(alert); // 1
const fun = async () => {
return 2;
};
fun().then(alert); // 2
async 함수 내에서 Promise의 값이 반환될 때까지 기다리는 코드이다.
function resolveAfter2Seconds() {
console.log("starting slow promise");
return new Promise(resolve => {
setTimeout(function () {
resolve("slow");
console.log("slow promise is done");
}, 2000);
});
}
function resolveAfter1Second() {
console.log("starting fast promise");
return new Promise(resolve => {
setTimeout(function () {
resolve("fast");
console.log("fast promise is done");
}, 1000);
});
}
아래의 sequentialStart() 함수에서는 slow와 fast 모두 await가 걸려있기 때문에 동기적으로 처리되어 모든 값이 나오기 위해 3초가 소모된다.
async function sequentialStart() {
console.log("==SEQUENTIAL START==");
// 1. Execution gets here almost instantly
const slow = await resolveAfter2Seconds();
console.log(slow); // 2. this runs 2 seconds after 1.
const fast = await resolveAfter1Second();
console.log(fast); // 3. this runs 3 seconds after 1.
}
sequentialStart();
// after 2 seconds, logs "slow", then after 1 more second, "fast"
아래의 concurrentStart() 함수에서는 slow와 fast가 비동기적으로 실행됐으나 await slow가 먼저 실행되므로 fast는 실행이 완료된 후 기다렸다가 출력된다.
async function concurrentStart() {
console.log("==CONCURRENT START with await==");
const slow = resolveAfter2Seconds(); // starts timer immediately
const fast = resolveAfter1Second(); // starts timer immediately
// 1. Execution gets here almost instantly
console.log(await slow); // 2. this runs 2 seconds after 1.
console.log(await fast); // 3. this runs 2 seconds after 1., immediately after 2., since fast is already resolved
}
concurrentStart();
// after 2 seconds, logs "slow" and then "fast"
아래의 concurrentPromise() 함수에서는 위의 2-2.1 에서 알아봤던 Promise.all(iterable) Method를 사용하여 Promise들을 비동기적으로 실행한 것이다. Promise들 중 가장 오래 걸리는 Promise가 종료된 후 모든 결과값이 반환된다.
function concurrentPromise() {
console.log("==CONCURRENT START with Promise.all==");
return Promise.all([resolveAfter2Seconds(), resolveAfter1Second()]).then(
messages => {
console.log(messages[0]); // slow
console.log(messages[1]); // fast
}
);
}
concurrentPromise();
// same as concurrentStart
아래의 paraller 함수는 각각의 Promise의 반환값을 비동기적으로 반환한 후 가장 오래 걸리는 Promise가 종료되는 것을 await를 사용해서 동기적으로 처리했다.
async function parallel() {
console.log("==PARALLEL with await Promise.all==");
// Start 2 "jobs" in parallel and wait for both of them to complete
await Promise.all([
(async () => console.log(await resolveAfter2Seconds()))(),
(async () => console.log(await resolveAfter1Second()))(),
]);
}
parallel();
// truly parallel: after 1 second, logs "fast", then after 1 more second, "slow"
기존의 Promise의 경우 예외처리를 .catch() 체인으로 진행했지만 async/await에서는 예외처리를 try-catch-finally 구문으로 진행할 수 있다.
// 기존 방식
function getProcessedData(url) {
return downloadData(url) // returns a promise
.catch(e => {
return downloadFallbackData(url); // returns a promise
})
.then(v => {
return processDataInWorker(v); // returns a promise
});
}
// async/await 방식
async function getProcessedData(url) {
let v;
try {
v = await downloadData(url);
} catch (e) {
v = await downloadFallbackData(url);
}
return processDataInWorker(v);
}
의미전달을 명확히 하고 싶고 정확한 정보만을 기록하고 싶은 마음에 검색들을 진행한 후 포스트를 작성했다. 작성하다보니 최대한 쉽게 쓰고 싶었지만 딱딱한 전달체를 벗어나기는 힘든 것 같다. 남들에게 말로 설명할 때랑 글로 설명할 때는 확연히 다르다는 걸 매번 깨닫는 것 같다. 이어서 Promise와 async/await는 JavaScript에서 없어서는 안될 비동기의 꽃이라고 불리는 처리 방식이므로 JavaScript 생태계에 머무는 사람이라면 반드시 숙지하고 있어야할 부분이다. 나도 이를 정리함으로써 한 번 더 유용성과 활용 방식에 대해서 배울 수 있었다. 이 포스트가 미래의 나 또는 JavaScript를 처음 시작하거나 Callback 지옥에서 헤어나오지 못하는 사람들에게 도움이 됐으면 한다.