async/await
구문의 특징
async/await
은 비동기 코드 작성을 단순화한다.async
함수는 암시적으로 프라미스를 만들고 반환한다.async
함수 내부await
은Promise
확정 전까지 대기하는 지점을 표시한다.async
함수가Promise
확정을 기다리는 동안 스레드는 다른 코드를 실행할 수 있다.for
,a + b
,try/catch/finally
등 다양한 식이나 구문 안에await
이 포함될 수 있다.await
이 포함되는 경우 비동기식이 된다.
Promise
가 거절 (reject) 되는 경우, 예외이다.Promise
가 이행 (resolve, fulfill) 되는 경우, 이행 값이await
표현식의 결과가 된다.
resolve
와 reject
가 await
과 만났을 때 예제 코드
resolve
를 await
으로 잡은 경우
function resolveAfter1Seconds() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("resolved value");
}, 1000);
});
}
async function asyncCall() {
console.log("calling");
const result = await resolveAfter1Seconds();
console.log(result); // resolved value
}
asyncCall();
- 이행된 (fulfilled) 값이
result
로 할당된다.
reject
를 await
으로 잡은 경우
function rejectAfter1Seconds() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("rejected value");
}, 1000);
});
}
async function asyncCall() {
console.log("calling");
const result = await rejectAfter1Seconds();
console.log(result); // Uncaught (in promise) rejected value
}
asyncCall();
reject
가 호출되면, 예외를 던진다.
async
함수의 특징들과 예제 코드
async
함수가 반환하는 것은 Promise
객체이다.
async function asyncCall() {
return "HELLO";
}
asyncCall(); // Promise {<fulfilled>: 'HELLO'}
- 함수의 결과로 단순히
HELLO
문자열을 반환하지 않고, 이행된Promise
가HELLO
문자열을 갖고 있게 한다.
async
함수의 동기 부분에서 발생한 예외는 거부로 전환된다.
async function asyncCall() {
throw new Error("에러입니다.");
}
asyncCall(); // Promise {<rejected>: Error: 에러입니다.
// at asyncCall (<anonymous>:2:9)
// at <anonymous>:5:1}
- 동기 부분에서 예외가 발생하더라도
Promise
실행자의reject
처럼 처리된다.
async
, await
의 거부는 예외로 전환되고, try/catch
로 잡을 수 있다.
const delayedFail = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("failed"));
}, 800);
});
};
const example = async () => {
try {
await delayedFail();
console.log("Done"); // 실행되지 않음
} catch (error) {
console.log("Caught: ", error);
}
};
example();
/*
Caught: Error: failed
at <anonymous>:4:14
*/
for
문에 await
넣기가 가능하다.
const fetches = async () => {
const arr = new Array(100).fill(100).map((e, i) => i);
const results = [];
for (const e of arr) {
const response = await fetch(`/correct-json?arr=${e}`);
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
results.push(await response.json());
}
return results;
};
fetches().then((res) => console.log(res));
for
문에await
을 넣어 마치 동기 코드처럼 실행시킬 수 있다.- 논리 흐름을 분할할 필요 없이
await
을 사용하여 비동기 결과를 대기처리 하는 방식으로 작성 가능하다.
thenable
을 반환하면 Promise
가 아니어도 await
이 가능하다.
const asyncFoo = async () => {
const thenable = () => {
return {
then(f) {
return f("HELLO");
},
};
};
return await thenable();
};
asyncFoo().then((res) => {
console.log("res", res);
return res;
});
await
은thenable
을 처리한다.Promise
는thenable
에 속한다.
throw
와 reject
비교해보기
const delay = (ms, value) =>
new Promise((resolve) => setTimeout(resolve, ms, value));
const delayedFailureWithReject = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("failed 1"));
}, 800);
});
const delayedFailureWithThrow = async () => {
await delay(800);
throw new Error("failed 2");
};
const failureWithReject = async () => {
try {
await delayedFailureWithReject();
console.log("Done");
} catch (error) {
console.error("failureWithReject Caught:", error);
}
};
const failureWithThrow = () => {
delayedFailureWithThrow()
.then(() => {
console.log("Done");
})
.catch((error) => {
console.error("failureWithThrow Caught:", error);
});
};
failureWithReject(); // failureWithReject Caught: Error: failed 1 at <anonymous>:7:14
failureWithThrow(); // failureWithThrow Caught: Error: failed 2 at delayedFailureWithThrow (<anonymous>:13:9)
async
를 이용하고throw
로 던지는 것과Promise
객체를 생성하고 실행자에서reject
를 이용하는 것과 별 차이가 없다.
async
함수에서 API 병렬 호출처리 하기
async
함수 내에서await
을 사용하면, 일반적으로는 마치 동기함수처럼 API 를 1개씩 호출하게 된다.- 이럴 때
Promise.all([])
을 이용하여 병렬 호출처리가 가능하다.
동기식 호출 예제
const fetches = async () => {
const arr = new Array(100).fill(100).map((e, i) => `/correct-json?arr=${i}`);
const responses = [];
for (const e of arr) {
responses.push(await fetch(e));
}
const jsons = [];
for (const response of responses) {
jsons.push(await response.json());
}
return jsons;
};
fetches().then((res) => console.log(res));
for
문에서await
을 이용한다.
비동기식 호출 예제
const fetches = async () => {
const arr = new Array(100).fill(100).map((e, i) => `/correct-json?arr=${i}`);
const responses = await Promise.all(arr.map((e) => fetch(e)));
return await Promise.all(responses.map((e) => e.json()));
};
fetches().then((res) => console.log(res));
/*
(100) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
*/
- 비동기 작업을 한번에 요청하고,
Promise.all()
이 배열 안에 들어있는Promise
객체가 이행될 때까지 대기한다. Promise.all([Promise, Promise, Promise])
을 수행하면, 배열 내부에 있는Promise
객체들이 전부 이행될 때까지 대기하는 것이다.
동기식 호출과 비동기식 호출 그림으로 보기
- 동기식 호출로
Promise.all()
을 사용하지 않았다. - 동기식으로 수행한 경우 약
0.05s
정도의 수행시간이 걸렸다. - 작업 바에서 일직선으로 작업이 수행되고 있는 것을 볼 수 있다.
- 비동기식 호출로
Promise.all()
을 사용했다. - 비동기식으로 수행한 경우 약
0.01s
정도의 수행시간이 걸렸다 - 작업 바에서 여러 줄로 병렬적으로 작업이 수행되고 있는 것을 볼 수 있다.
반환에는 await
이 필요 없다.
const fetches = async () => {
const arr = new Array(100).fill(100).map((e, i) => `/correct-json?arr=${i}`);
const responses = await Promise.all(arr.map((e) => fetch(e)));
return await Promise.all(responses.map((e) => e.json()));
};
fetches().then((res) => console.log(res));
위와 같이 작성한 코드는 사실
const fetches = async () => {
const arr = new Array(100).fill(100).map((e, i) => `/correct-json?arr=${i}`);
const responses = await Promise.all(arr.map((e) => fetch(e)));
return Promise.all(responses.map((e) => e.json()));
};
fetches().then((res) => console.log(res));
이 코드와 같다.
- 그 이유는 이미
fetches().then()
에서await
을 하고있기 때문이다. return await f()
는return await await f()
와 비슷하다.
return await f()
는 return f()
보다 느리다.
const thenableResolve = (value) => {
return {
then(onFulfilled) {
onFulfilled(value);
},
};
};
const a = async () => await thenableResolve("a");
const b = async () => thenableResolve("b");
a().then((value) => console.log(value));
b().then((value) => console.log(value));
/*
결과:
b
a
*/
- 위의 코드예제 결과는
b
가 출력된 뒤a
가 출력된다.- 이미
async
로 인해Promise
확인 계층이 하나 존재하는 상태인데,a
의 경우에는await
때문에Promise
확인 계층이 하나 더 추가되기 때문이다.
- 이미
const a = async () => await Promise.resolve("a");
const b = async () => Promise.resolve("b");
a().then((value) => console.log(value));
b().then((value) => console.log(value));
- ES2020 에는 추가 비동기 틱을 제거하도록 최적화되어 있다.
- 그래서 위의 코드의 결과에서
a
,b
를 순서대로 출력한다.
결론은
return await
을 사용하지말자.return
이 더 좋다.
async
함수와 await
키워드가 사용될 수 없는 곳
const fetches = async () => {
const arr = new Array(100).fill(100).map((e, i) => `/correct-json?arr=${i}`);
const filtered = arr.filter(async (e) => false);
console.log("filtered", filtered);
};
fetches();
/*
결과:
(100) ['/correct-json?arr=0', '/correct-json?arr=1', ...]
*/
filter
메서드 내부async
함수에서는 아무런 원소도 필터링되지 않는다.filter
는true
혹은false
값을 받아 배열을 필터링하고 싶지만,async
함수의 결과는 항상Promise
객체이다.
async/await
은 강력한 패턴이지만 작성중인 함수가 콜백함수라면, 콜백의 반환 값이 사용되는 방식을 고려해야 한다.async/await
을 남용하는 함정에 빠졌는지 다시 한번 생각해보자.
반응형
'자바스크립트 > 모던 자바스크립트' 카테고리의 다른 글
모던 자바스크립트, 템플릿 리터럴과 템플릿 태그 함수 (0) | 2023.03.08 |
---|---|
모던 자바스크립트, 비동기 버전의 이터레이터, 이터러블, 제너레이터 (0) | 2023.02.28 |
모던 자바스크립트, 제너레이터 (Generator) (0) | 2023.02.28 |
모던 자바스크립트, 이터러블 (iterable) 과 이터레이터 (iterator) (0) | 2023.02.28 |
모던 자바스크립트, 프라미스 2 - 유틸 메서드와 작업 패턴 그리고 안티 패턴 (0) | 2023.02.15 |