🐕 async/await
제너레이터 함수를 사용해 비동기 처리를 동기 처리처럼 동작하도록 구현할 수 있습니다.
제너레이터 함수가 뭐냐고요? 함수 제어권을 양도해줄 수 있는 함수입니다.
더 자세한 개념은 따로 찾아보세요
하여튼 제너레이터 함수를 이용해 비동기 처리를 동기 처리처럼 구현할 수 있는데
그렇게 하면 코드가 굉장히 장황해진다는 문제점이 있습니다.
코드가 굉장히 장황해지면 저같은 허수개발자들은 코드를 제대로 치지도 못하겠죠?
따라서 좀 더 간편하고 가독성 좋으면서도 비동기 처리를 동기 처리처럼
동작할 수 있게 도와주는 문법에 대한 필요성이 있었고
그를 위해 async/await 문법이 ES8(ECMAScript2017)에서 도입되었습니다.
async/await은 프로미스와 제너레이터함수를 기반으로 동작합니다.
마치 클래스가 사실 본질은 프로토타입처럼 동작하는 것처럼..
하여간 따라서 async/await은 프로미스 기반이기 때문에
당연하게도 프로미스 후속처리 메서드도 사용이 가능합니다.
즉 then / catch / finally 후속 처리 메서드를 사용 할 수 있다는 거죵
👻async 함수
async function fetchTodo() {
const url = 'https://jsonplaceholder.typicode.com/todos/1';
const response = await fetch(url);
const todo = await response.json();
console.log(todo);
}
fetchTodo();
//{ userId: 1, id: 1, title: 'delectus aut autem', completed: false }
await 키워드는 반드시 반드시!!!!!!!!!
async 함수 내부에서 사용해야만 합니다.
문법은 다음과 같습니다.
함수 앞에 async를 붙여주면 끝 너무 간편해서
프로미스를 공부한 내 인생 손해 본 느낌이지만
어차피 이것도 프로미스 기반이니 오히려 좋다는 마인드
async 함수가 명시적으로 프로미스를 반환하지 않더라도
async 함수는 암묵적으로 반환값을 resolve 하는 프로미스를 반환합니다.
🥶 각 함수별 async 문법 정리
async function foo(n) {
return n;
}
foo(1).then((v) => console.log(v));
const bar = async function (n) {
return n;
};
bar(2).then((v) => console.log(v));
const baz = async (n) => n;
baz(3).then((v) => console.log(v));
const obj = {
async foo(n) {
return n;
},
};
obj.foo(4).then((v) => console.log(v));
class MyClass {
async bar(n) {
return n;
}
}
const myClass = new MyClass();
myClass.bar(5).then((v) => console.log(v));
클래스의 constructor 메서드는 async 메서드가 될 수 없다는 특징이 있습니다.
왜냐면 클래스의 constructor 메서드는 항상 인스턴스를 반환해야하지만
async 메서드가 되면 프로미스를 반환하는 모순에 처하기 때문이에요
🌞await 키워드를 알아보자
await 키워드는 프로미스가 settled 상태가 될 때 까지 대기하다가
settled 상태가 되면 프로미스가 resolve한 처리 결과를 반환합니다.
await 키워드는 반드시 프로미스 앞에서 사용해야합니다.
const getGithubUserName = async (id) => {
const res = await fetch(`https://api.github.com/users/${id}`);
const { name } = await res.json();
console.log(name);
};
getGithubUserName('ungmo2'); // Ungmo Lee
await 키워드는 프로미스가 settled 상태가 될 때까지 대기한다.
따라서 fetch 함수가 수행한 HTTP 요청에 대한 서버의 응답이 도착해서
fetch 함수가 반환한 프로미스가 settled 상태가 될 때 까지 1은 대기하게됩니다.
그리고 프로미스가 settled 상태가 되면 프로미스가 resolve한 처리 결과가 res 변수에 할당됩니다.
모든 프로미스 await 키워드를 사용하는 것은 주의해야 합니다.
만약 비동기처리가 서로 연관이 없이 처리되는 것이라면
순차적으로 처리할 필요가 없으니까 await 키워드를 사용하는것이 오히려 손해겠죵?
async function foo() {
const res = await Promise.all([
new Promise((resolve) => setTimeout(() => resolve(1), 3000)),
new Promise((resolve) => setTimeout(() => resolve(3), 2000)),
new Promise((resolve) => setTimeout(() => resolve(5), 1000)),
]);
console.log(res);
}
foo();
프로미스 정적메서드 Promise.all을 이용해 한번에 처리해주는게 좋습니다.
async function foo(n) {
const a = await new Promise((resolve) => setTimeout(() => resolve(n), 3000));
const b = await new Promise((resolve) => setTimeout(() => resolve(a + 1), 2000));
const c = await new Promise((resolve) => setTimeout(() => resolve(b + 1), 1000));
console.log([a, b, c]);
}
foo(10);
[ 10, 11, 12 ]
[Done] exited with code=0 in 6.12 seconds
순차적으로 실행이 보장되는 걸 확인할 수 있습니다.
근데 Promise.all은 저 여러개의 프로미스 중 하나만 reject이 나도
모든 resolve값을 못쓰게 된다는 큰 문제점이 있습니다.
따라서 Promise.allSettled라는 메서드를 사용하세요 감사합니다.
const delayP = (ms, str) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([ms, str]);
}, Math.floor(Math.random() * ms));
});
};
async function um() {
let a1 = delayP(2000, '엄');
let a2 = delayP(4000, '준');
let a3 = delayP(2000, '식');
let a4 = delayP(3000, '이');
const results = await Promise.allSettled([a1, a2, a3, a4]);
results.map(async (ele) => {
await console.log(ele);
});
}
um();
이렇게 allSettled 메서드를 사용하면 객체형태로 값이 반환됩니다.
{ status: 'fulfilled', value: [ 2000, '엄' ] }
{ status: 'fulfilled', value: [ 4000, '준' ] }
{ status: 'fulfilled', value: [ 2000, '식' ] }
{ status: 'fulfilled', value: [ 3000, '이' ] }
이렇게요!
😋에러처리 하는 법
비동기 처리를 위한 콜백 패턴의 단점 중 가장 심각한 것은 에러처리이다.
에러는 호출자 방향으로 전파되는데 비동기 함수는 호출자가 명확하지 않다는 문제이다.
그러나!!!!!!!!!!!!!!!!!! async / await 에서 에러처리는 try...catch 문을 사용할 수 있습니다.
콜백 함수를 인수로 전달받는 비동기 함수와는 달리 프로미스를 반환하는 비동기 함수는
명시적으로 호출할 수 있기 때문에
const foo = async () => {
try {
const wrongUrl = 'https://wrong.url';
const response = await fetch(wrongUrl);
const data = await response.json();
console.log(data);
} catch (err) {
console.error(err);
}
};
foo(); // TypeError: fetch failed
async/await 에서는 에러처리를 위해 try...catch 문을 사용할 수 있습니다.
콜백 함수를 인수로 전달받는 비동기 함수와 달리
프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문에 호출자가 명확합니다.
따라서 foo 함수의 catch 문은 http 통신에서 발생한 네트워크 에러 뿐만 아니라
try 코드 블록 내의 모든 문에서 발생한 일반적인 에러까지 모두 캐치할 수 있다.
const foo = async () => {
const wrongUrl = 'https://wrong.url';
const response = await fetch(wrongUrl);
const data = await response.json();
console.log(data);
};
foo().then(console.log).catch(console.error); // TypeError: fetch failed
async 함수 내에서 catch 문을 사용해서 에러처리를 하지 않으면
async 함수는 발생한 에러를 reject 하는 프로미스를 반환합니다.
따라서 위와 같이
async 함수를 호출하고 Promise.prototype.catch 후속 처리 메서드를 사용해 에러를 캐치할 수도 있습니다.
🤢await은 정말 non-blocking일까?
이제서야 이해가 가는 저 포스트...
await은 분명히 함수 내부에서 블로킹을 하는것처럼 동작하는 느낌이 드는데
async-non block이라고 하니 의문이 드는것이죠
1. async 내부의 다음 코드들은 await을 만나는 시점에서 마치 블로킹을 하는것처럼 동작하는데? 블로킹아님?
2. 근데 실제로는 await은 async 내부 코드들의 실행을 멈추지 않음
3. 다만 async도 프로미스기반이라 암묵적으로 .then 메서드를 사용해 코드를 담아둠
그렇기 때문에 async의 await 키워드 역시 블로킹인게 아니라
프로미스를 기반으로 동작하는 non-blocking으로 동작한다는 것임
(왜냐면 then메서드의 콜백을 백그라운드로 보내는 작업을 마치면
then 메서드의 콜백의 처리가 끝나지 않았어도 다음 then 메서드를 실행하기 때문에 논블로킹이라는 것!!)
4. 따라서 await의 실행방식은 함수를 콜백이나 프로미스 체이닝으로 작성하는 것과 같은데!!
문법이 좀 더 간결하고 보기 편해진 것이다.
그래서 결국 async/await은 비동기함수를 블로킹 형태로 실행하는것처럼 보이지만..?
실제로 비동기함수의 반환값이 돌아올때까지 무한정 대기하는 동기/블로킹인것은 아니다!!
그러니 await 키워드를 사용한 코드는 일단 해결되지 않은 pending 상태의 프로미스를 반환하고
다른 코드를 실행해주다가 promise가 settled 상태로 바뀌면 다음 await 코드를 실행할 수 있는
이벤트가 되는 것이네요.
제너레이터 함수는 예전에 공부를 했지만..
동작과정이 너무 난해해서 이해하기가 싫은... 부분이 있네요..
하지만 async가 알아서해주죠...?
언젠간 공부해야겠어용...
https://www.youtube.com/watch?v=ZrdHtL1gcEI
이 유튜브를 추천드립니다.
🐶마치며
프로미스보다 훨씬 간편하고 좋은 문법인 것 같아요
프로미스를 이해한 다음에 보니 정말 좋네요
'javascript' 카테고리의 다른 글
계산된 속성 이름 (Computed property name) 문법을 보자 (0) | 2023.04.09 |
---|---|
node.js의 스레드 풀은 무엇인가.. (1) | 2023.03.29 |
node.js 환경에서 fetch API를 사용하는 방법 (0) | 2023.03.21 |
자바스크립트 this 디스합니다. 비트주세요 (0) | 2023.03.16 |
프로미스를 회처럼 날로 먹는 방법 (3) | 2023.03.15 |