쿼카러버의 기술 블로그
[Javascript 비동기 처리 뿌시기 (4/4)] 비동기 처리 끝판왕 async & await 이해하기 본문
[Javascript 비동기 처리 뿌시기 (4/4)] 비동기 처리 끝판왕 async & await 이해하기
quokkalover 2022. 1. 25. 23:29Javascript 비동기 처리 뿌시기 시리즈에서 필자가 추천하는 학습방법은 아래와 같다.
학습방법
step1) 비동기 처리의 기본 개념을 이해하기 위한 Core개념 익히기 https://etloveguitar.tistory.com/84
step2) 콜백함수와 콜백 지옥의 맛을 보기 (https://etloveguitar.tistory.com/85)
step3) Promise로 살짝 콜백 지옥 해결하고 유연한 코드 짜기 (https://etloveguitar.tistory.com/86)
step4) Async & Await를 이해하고 더 우아하게 Promise활용하기
물론 이미 알고 있는 개념이면 이 시리즈를 전부 보지 않아도 되지만, 최대한 쉽게, 그리고 예시와 함께 설명했으니 복습차원에서 보는것도 추천한다. 한 개념만 보면 대충 뭔지 알겠다가도, 개념들을 연속적으로 이해하지 못하면 결국 벽에 부딪힌다. 따라서, 지금 필요한 특정 개념보다는 전반적인 흐름을 가지고 공부하는 것을 추천한다.
본 글의 제목에서 알듯이 이번 글은 4탄이다. 비동기 처리의 끝판왕인 async & await에 대해 지금 당장 알아보자.
async가 뭐야 ???
async
키워드는 함수를 선언할 때 붙이는 경우를 많이 보게 된다. async와 함께 await도 많이 보이게 되는데, async
와 await
라는 특별한 문법을 사용하면 프라미스를 좀 더 편하게 사용할 수 있다.
async 함수는 Promise
와 굉장히 밀접한 연관을 가지고 있는데, Promise
의 단점을 보완하고, 개발자가 읽기 좋은 코드를 작성할 수 있게 도와주는 비교적 최근에 나온 문법이다.
기존에 작성하던 executor
로부터 몇 가지 규칙만 적용한다면 new Promise(…)
를 리턴하는 함수를 async
함수로 손쉽게 변환할 수 있다.
async
키워드가 붙은 함수를 async 함수로, async
가 없는 함수는 일반 함수라고 부를 수 있다.
async 함수
async
키워드부터 알아보자. async
는 function 앞에 위치한다.
async function f() {
return 1;
}
function 앞에 async
를 붙이면 해당 함수는 항상 Promise를 반환한다. Promise가 아닌 값을 반환하더라도 이행 상태의 프라미스(resolved promise)로 값을 감싸 이행된(fulfilled) Promise가 반환되도록 한다.
직접 호출해보자.
async function f() {
return 1;
}
f().then((result) => { console.log(result); }); // 1
위 코드와 다르게 명시적으로 Promise를 반환하는 것도 가능하다.
async function f() {
return Promise.resolve(1);
}
f().then((result) => { console.log(result); }); // 1
위 예시에서 설명하고자 하는 포인트는 async가 붙은 함수는 반드시 Promise 객체를 반환하고, Promise 객체가 아닌 것은 Promise로 감싸서 반환한다.
이제 async와 함께 동작하는 await
에 대해서 알아보자.
await
자바스크립트는 await
키워드를 만나면 프라미스가 처리(settled)될 때까지 기다린다. 결과는 그 이후 반환된다.
한번 예시를 통해서 살펴보자
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000)
});
let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)
console.log(result); // "완료!"
}
f();
위 코드를 실행하면 1초 기다렸다가 완료!가 출력된다.
즉 함수를 호출하고 함수 본문이 실행되는 도중에 await이 있는 부분에서 실행이 잠시 ‘중단'되었다가 Promise가 처리되면 실행이 재개된다. 이 때 Promise객체의 result 값이 result에 할당된다.
반대로
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000)
});
let result = promise; // 프라미스가 이행될 때까지 기다림 (*)
console.log(result); // "완료!"
}
f();
await이 없으면
Promise { <pending> }
가 출력된다.
즉, await는 말 그대로 프라미스가 처리될 때 까지 함수 실행을 기다리게 만든다.
Promise가 처리되길 기다리는 동안에는 Javascript 엔진이 다른 일을 할 수 있기 떄문에 CPU리소스가 낭비되지 않는다.
await는 promise.then보다 좀 더 세련되게 프라미스의 result 값을 얻을 수 있도록 해주는 문법이다. 개인적으로는 더 가독성도 좋고, 쓰기도 쉽다고 생각한다.
위 내용을 조금 더 정리해보면 async함수를 사용하는 방법은 아래와 같다고 할 수 있다.
- 함수에
async
키위드를 붙인다. new Promise...
부분을 없애고executor
본문 내용만 남긴다.resolve(value);
부분을return value;
로 변경한다.reject(new Error(…));
부분을throw new Error(…);
로 수정한다.
코드 예시
앞 장에서 age가 20보다 높은지 낮은지를 기준으로 다른 결과를 리턴하는 Promise를 생성해보았는데, 이를 async와 await을 활용해서 한번 다시 구현해보면 아래와 같다.
function setTimeoutPromise(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), ms);
});
}
async function startAsync(age) {
if (age > 20) return `${age} success`;
else throw new Error(`${age} is not over 20`);
}
async function startAsyncJobs() {
await setTimeoutPromise(1000);
const promise1 = startAsync(25);
try {
const value = await promise1;
console.log(value);
} catch (e) {
console.error(e);
}
const promise2 = startAsync(15);
try {
const value = await promise2;
console.log(value);
} catch (e) {
console.error(e);
}
}
startAsyncJobs();
- await는 promise가 완료될 때 까지 기다린다. 따라서, setTimeoutPromise의 executor에서 resolve함수가 호출될 때까지 기다린다. 그 시간동안 startAsyncJobs의 진행은 멈춰있다.
- 해당
Promise
에서reject
가 발생한다면 예외가 발생한다. 이 예외 처리를 하기 위해try-catch
구문을 사용했다.- resolve로 넘긴 값은 try절 안의 await을 통해 리턴된다. 즉
${age} success
가value
로 들어온다. reject
로 넘긴 에러(async 함수 내에서는throw
한 에러)는catch
절로 넘어갑니다. 이로써 본래 해왔던 에러 처리 하듯이 진행할 수 있다.
- resolve로 넘긴 값은 try절 안의 await을 통해 리턴된다. 즉
await
은 then
과 catch
의 동작을 모두 자기 나름대로 처리한다. 그래서 async 함수 내에서 then
, catch
메소드의 존재를 잊게 할 수 있다.
await을 async함수에서만 쓸 수 있는 이유
비동기로 시작한 작업의 특징은, 그로부터 파생된 모든 작업 또한 비동기 작업으로 간주할 수 있다.
동기 환경에서 비동기 작업을 마냥 기다리는 게 의미가 없는 이유는, 그럴 바에야 그냥 동기 코드를 사용하면 되기 때문이다.
반면 비동기 환경에서 비동기 작업의 결과를 기다리겠다는 것은 의미가 있다.
예를 들어 생일 날짜를 가져오는 비동기 작업인 fetchBirthday 함수가 있는데, 이 함수의 결과로 나오는 생일을 알아야 생일 파티를 제때에 해줄 수 있는 것처럼, 마냥 기다리는 게 정답일 때도 있다. 그 때 await
을 사용하는 것이다.
하여튼 비동기는 동작 특성상 실제 작업과 그 작업의 후속조치를 따로 분리시킬 수 밖에 없는데, (그래서 then
, catch
등을 썼는데) async
와 await
을 쓰면 하나의 흐름 속에서 코딩할 수 있게 해준다.
Promise.all 여러 비동기 동작을 한꺼번에 기다리기
function setTimeoutPromise(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), ms);
});
}
async function fetchAge(id) {
await setTimeoutPromise(1000);
console.log(`${id} 사원 데이터 받아오기 완료!`);
return parseInt(Math.random() * 20, 10) + 25;
}
async function startAsyncJobs() {
let ages = [];
for (let i = 0; i < 10; i++) {
let age = await fetchAge(i);
ages.push(age);
}
console.log(
`평균 나이는? ==> ${
ages.reduce((prev, current) => prev + current, 0) / ages.length
}`
);
}
startAsyncJobs();
위에서 처럼 값을 다 받아온 뒤에 평균 나이를 계산해야 하는 경우에는 위처럼 실행하면 평균값을 계산할 수는 있지만 비동기로 실행한게 아무 의미가 없어진다. 1초에 작업 하나씩 수행하고 있기 때문이다. 이는 for 안에 await가 들어가 있기 때문이다.
이제 아래처럼 코드를 바꿔보자
async function startAsyncJobs() {
let promises = [];
for (let i = 0; i < 10; i++) {
promises.push(fetchAge(i));
}
let ages = await Promise.all(promises);
console.log(
`평균 나이는? ==> ${
ages.reduce((prev, current) => prev + current, 0) / ages.length
}`
);
}
startAsyncJobs();
위처럼 코드를 실행하면 1초만에 해결된다. 즉 Promise의 배열을 받아서 하나의 특별한 Promise를 새로 생성하고, 배열로 받은 모든 비동기 작업이 성공했다면 내부적으로 resolve를 호출하고, 하나라도 실패하면 reject를 호출한다.
기타 재미 있는 사실들
then
, catch
메소드들은 사실은 새로운 Promise
객체를 만든다
리턴값이 Promise 라는 점은 우리가 메소드 체이닝을 할 수 있을 때부터 눈치챘을 수도 있다. 새롭게 만들어진 Promise 는 정해진 규칙이 있다. 하지만 우리는 그냥 순차적으로 뭔가 잘 되겠지 하며 사용하면 대개 큰 문제는 없다.
finally
로 이행/거부 상관 없는 동작을 지정해줄 수 있다.
Promise 는 finally
라는 메소드도 있습니다. 이 함수는 Promise 가 fulfilled, rejected 에 상관없이 가장 마지막으로 실행됩니다.
then
의 두 번째 인자로 onRejected 를 받을 수 있다.
then
만 쓰더라도 reject 된 Promise 에 대한 처리를 할 수 있다.
const throwError = new Promise((resolve, reject) => {
throw new Error("error");
});
throwError.then(
() => console.log("throwError success"),
() => console.log("throwError catched")
);
!!!! 또한 기다리는 작업이 있어야 비동기가 비동기처럼 보인다 !!!!
기다리기만 하면 되는 작업을 비동기로 하면 좋은 것이지, 싱글스레드로 돌아가는 자바스크립트에서 돌아가는 코드 자체가 빡세다면 비동기처럼 동작하지 않는다. 이를 이해하기 위해 내가 오랫동안 풀지 못했던 문제 예시를 소개하겠다.
function sleep(ms) {
const wakeUpTime = Date.now() + ms;
while (Date.now() < wakeUpTime) {}
}
let hello = async function() {
sleep(3000)
let a = () => { console.log("async"); }
a()
let b = 0;
for (let i=0; i < 1000000000; i++) {
b += i;
}
console.log("b", b);
return b
};
async function logItems() {
let resultItems = hello();
console.log("in", resultItems); // [1,2,3]
return resultItems
}
logItems()
console.log("haha");
위 함수의 출력을 한번 직접 예상해보고 실제 출력과 비교해보자.
바보같게도 내 처음 예상 답은 아래와 같았다.
// 내 처음 예상 출력
// "haha"
// in Promise { Pending }
// async
// b 499999999067109000
뭐 물론 내가 바보라서 못풀었을 수도 있지만,
실제 답:
// 실제 출력
// async
// b 499999999067109000
// in Promise { 499999999067109000 }
// haha
나와 비슷하거나 이상하다 싶으면 위에서 말한 빡센 작업을 하면 소용 없다 ⇒ 이게 힌트다.
끝까지 답이 왜이렇게 나오는지 이유를 찾지못했다면 댓글을 남겨주면 그 댓글에 답을 남기도록 하겠다. (직접 생각해보고 풀어보자는 의미)
이제 시리즈를 마친다. 정말 많은 블로그 글들의 도움을 받아 나만의 방식으로 정리해봤다.
비동기처리에 대해 이제 조금 맛봤으니 한번 실제 내 개발환경에 열심히 적용해보자~~
참고자료
https://joshua1988.github.io/web-development/javascript/javascript-asynchronous-operation/
https://it-eldorado.tistory.com/86
https://developer.mozilla.org/ko/docs/Web/JavaScript/EventLoophttps://velog.io/@thms200/Event-Loop-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84https://www.youtube.com/watch?v=m0icCqHY39U&t=369s
https://helloworldjavascript.net/pages/285-async.html
https://stackoverflow.com/questions/35447235/who-or-which-thread-owns-the-javascript-event-loop
https://www.educative.io/edpresso/what-is-an-event-loop-in-javascripthttps://ko.javascript.info/async-await