본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.
async
와 await
은 비동기 관련하여 ES8(ES2017)에 추가된 비교적 최신 스펙의 문법이다. 이를 이용하면 기존의 프라미스 기반의 코드를 조금 더 편하게 사용할 수 있다.
async
는 function
앞에 위치한다.
async function f() {
return 1;
}
함수 앞에 async
를 붙이게 되면 해당 함수는 항상 프라미스를 반환한다. 프라미스가 아닌 값을 반환하더라도 이행 상태의 프라미스(resolved promise)
로 감싸 이행된 프라미스가 반환되도록 한다. 물론 명시적으로 프라미스를 반환하는 것도 가능한데 아래 두 코드의 결과는 동일하다.
async function f1() {
return 1;
}
async function f2() {
return Promise.resolve(1);
}
f1().then(console.log); // 1
f2().then(console.log); // 1
async
가 붙은 함수는 반드시 프라미스를 반환하고, 프라미스 아닌 것은 프라미스로 감싸서 반환한다.
await
은 항상 async function
안에서만 동작한다. 자바스크립트는 await
키워드를 만나면 프라미스가 처리(settled
)될 때까지 기다리고 처리가 완료되면 그 이후에 값을 반환한다.
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('done'), 1000)
});
// 1초 뒤에 프라미스가 이행되기 까지 대기 후 반환
let result = await promise;
// 따라서 이 시점에서 바로 값을 사용 가능
console.log(result); // done
}
f();
await
은 그 뜻 그대로 프라미스가 처리될 때 까지 함수 실행을 기다리게 한다. 프라미스가 처리되고 나서 그 결과와 함께 실행이 재개된다. 프라미스가 처리되길 기다리는 동안에는 엔진이 다른 일을 할 수 있기 때문에 리소스가 낭비되지 않는 장점이 있다.
await
는 promise.then(...)
과 같은 역할을 하지만 코드의 흐름을 동기 방식으로 작성하더라도 비동기를 처리할 수 있다는 장점이 있다.
프라미스 체이닝에서 구현했던 showAvatar()
코드를 async/await
을 사용해 다시 작성해보자.
async function showAvatar() {
// JSON 파싱
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
// github 사용자 정보 처리
let githubResponse = awaitfetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();
// 아바타 렌더링
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = 'avatar';
document.body.append(img);
// 3초 대기
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
프라미스 역시 콜백 방식에 비해 코드가 깔끔하다는 장점이 있었지만 async/await
방식인 이보다 더 가독성이 뛰어나다. 코드의 작성 흐름 자체도 대부분이 익숙한 동기 방식과 동일하기에 작성 역시 더 편하다는 장점도 있다.
이때 다시 한 번 주의해야 할 것이 있다. await
은 async function
내부에서만 작동한다고 했다. 따라서 await
은 최상위 레벨 코드에서는 사용할 수 없다.
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json(); // Error
만약 최상위 레벨에서 await
을 사용하고자 한다면 다음과 같이 IIFE 방식으로 익명 async function
을 사용해 코드를 감싸는 방법이 있다.
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
})();
Node
와 유사한 자바스크립트 런타임 환경은Deno
의 경우엔 자체적으로top-level-await
를 지원하기 때문에await
를 최상위 코드에서도 사용할 수 있다.
또한 await
는 프라미스에 then
핸들러가 그러했던 것처럼 Thenable
객체를 처리할 수 있다.
class Thenable {
constructor(num) {
this.num = num;
}
then (resolve, reject) {
console.log(resolve);
setTimeout(() => resolve(this.num*2), 1000);
}
};
async function f() {
let result = await new Thenable(1);
console.log(result);
}
f();
또한 class
의 메서드 역시 async
를 사용하여 async
클래스 메서드로 만들어 줄 수 있다.
class Waiter {
async wait() {
return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(console.log); // 1
프라미스가 정삭적으로 이행되면 await promise
는 프라미스 객체의 result
에 저장된 값을 반환한다. 반면 프라미스가 거부되면 마치 throw
문을 작성한 것처럼 에러가 던져진다. 따라서 아래의 두 코드는 동일하다.
async function f1() {
await Promise.reject(new Error('error'));
}
async function f2() {
throw new Error('error');
}
실제 상황에서는 프라미스가 거부 되기 전에 약간의 시간이 지체되는 경우가 있다. 이런 경우엔 await
가 에러를 던지기 전에 지연이 발생한다. await
가 던지는 에러는 throw
가 던지는 에러를 잡을 때 처럼 try...catch
구문으로 잡을 수 있다.
앞서 다룬 것과 동일하게 try
블록에서 발생하는 모든 에러는 catch
블록에서 잡을 수 있다.
async function f() {
try {
let response = await fetch('http://wrong');
let user = await response.json();
} catch (err) {
// fetch와 response.json()에서 발생하는
// 모든 에러를 여기서 잡을 수 있다
console.log(err);
}
}
f();
또는 try...catch
구문을 사용하지 않고 프라미스의 catch
핸들러를 사용하는 것도 가능하다. 거부 상태가 된 프라미스가 있을때 .catch
핸들러를 추가하면 거부된 프라미스를 손쉽게 처리할 수 있다.
async function f() {
let response = await fetch('http://wrong');
}
f().catch(console.log); // TypeError: failed to fetch
이처럼 async/await
을 사용하면 await
가 대기를 처리해주기 때문에 .then
이 거의 필요하지 않다. 또한 .catch
핸들러 대신 일반적인 try...catch
구문으로 에러를 처리할 수 있다는 장점 또한 생긴다. 항상 그러한 것은 아니지만 promise.then
을 사용하는 것보다 async/await
을 사용하는 것이 대개 편리한 경우가 많다.
여러 개의 프라미스가 모두 처리되길 기다려야 하는 상황이라면 이 프라미스들을 Promise.all
로 감싸고 여기에 await
을 붙여 사용할 수 있다.
// async function ... 이라고 가정
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
)];
results
는 Promise.all
이 처리를 완료할 때까지 대기한 후 그 결과를 저장한다. 만약 실패한 프라미스로 에러가 전파되면 보통 에러와 마찬가지로 Promise.all
로 전파되는 것 역시 동일하다. 이때 생긴 예외 또한 try...catch
로 감싸 잡을 수 있다.
크롬 개발자 도구에는
top-level-await
가 적용되어 있어async function
을 선언하지 않더라도await
이 정상 작동한다. (참고링크)