첫 비동기 글로 돌아가기
이전 글
비동기 시리즈 마지막이다.
지금까지
1. 동기와 비동기 작업을 구분하고,
2. 자바스크립트에서 비동기 작업을 실행할 수 있는 이유
3. 비동기 작업의 콜백헬 및 에러 처리
4. 3번의 개선을 위한 Promise 객체
5. 비동기 병렬 처리를 위한 Promise Static 메서드
6. 마이크로 태스트 큐
까지 배웠다!.
하지만 비동기 작업이 처리될 때까지 동기적으로 기다릴 수 있는 방법에 대해서 배우지 않았다.
setTimeout(() => console.log("Hey"), 0);
console.log("Hello World");
위의 코드에서, 비동기 작업의 콜백이 실행되기까지 기다려준 뒤에..
"Hey" "Hello World" 순으로 출력할 수 있는 방법을 현재는 모른다.
async await이 전역 컨텍스트에서도 모든 것을 기다리게 하는 최고의 마술은 아니다.
그러나, 즉시실행함수로 entry point의 코드들을 감싸는 등의 약간의 꼼수를 더한다면 비슷하게 동작할 수 있다.
무슨 말인지 잘 모르겠다면 패스!
아무튼 async await을 배워보자.
async/await
async await은 프로미스를 기반으로 동작한다.
async await을 이용하면 프로미스의 then/catch/finally 메서드, 콜백함수로 비동기 처리를 할 필요가 없다.
비동기 작업을 마치 동기처럼 작동하도록 할 수 있다.
then/catch/finally의 프로토타입 메서드를
후속처리메서드(프로미스 내부 비동기 작업이 끝난 뒤 처리하는)라고 부른다.
async함수에서는 await으로 해당 프로미스가 끝날 때까지 동기적으로 기다려준다.
그래서 callback을 통한 후속 처리가 필요 없다는 것이다.
물론, async 함수의 실행 결과도 항상 프로미스다!
async 함수
async 키워드를 이용해 async 함수를 만들 수 있다.
async 함수 내부에서는, await 키워드를 사용할 수 있다.
또한, await 키워드는 반드시 async 함수 내부에서만 사용할 수 있다.
async 함수는 Promise 객체의 then/catch/finally처럼 명시적으로 프로미스를 반환하지 않더라도,
암묵적으로 반환값을 resolve하는 프로미스를 반환한다.
await 키워드
async 함수 내에서는, 실행해야 하는 프로미스 앞에 await 키워드를 붙일 수 있다.
이 await 키워드가 붙은 프로미스는 비동기로 처리하는 작업이다.
await 덕분에 작업이 끝날 때 까지 기다릴 수 있다.
await 키워드는 프로미스 상태가 settled(fullfilled or rejected)로 변했을 때까지 기다려준다.
이 특징을 가지고 async 함수 내의 코드는 동기(코드가 작성된 순서대로) 실행을 설계할 수 있다.
음.. 내가 봐도 무슨 말인지 이해하기 어려우니 간단한 예시로 살펴보자.
async await 없이 내부에서 비동기 작업을 수행하는 init 함수
function init() {
const res = fetch(`https://api.github.com/users/abc1234`);
res.then((data) => console.log(data));
console.log("hello");
}
init();
async 함수가 아닌 함수 내부에서 fetch 라는 비동기 작업을 수행한다.
함수 실행 컨텍스트의 모든 코드가 실행되고 난 뒤, 마이크로 태스크 큐에 담긴 then 콜백이 실행된다.
hello가 먼저 출력되고, 콜백 함수에 의해 data가 출력된다.
동일한 init 함수에 async await을 추가시키자.
async function init() {
const res = await fetch(`https://api.github.com/users/abc1234`);
res.then((data) => console.log(data));
console.log("hello");
}
init();
then이 없다는 오류가 발생하는 이유?
asnyc await을 이용하면 then 처리를 할 필요가 없기 때문이다.
(= await fetch~가 promise를 반환하는 것이 아니라, 값을 반환하기 때문이다. 라고 이해하는 것이 더 정확하다.)
그러므로 then의 콜백 내용을, await 바로 아래 선언해보자.
async function init() {
const res = await fetch(`https://api.github.com/users/abc1234`);
console.log(res);
console.log("hello");
}
init();
이전과는 다른 결과다.
데이터가 먼저 출력되고, hello가 출력되었다!
비동기작업 fetch를 포함했으나, 코드를 작성한 순서대로 동기적으로 작동하고 있다.
배운 내용을 바탕으로 코드의 수행 과정을 살펴보면,
- fetch 작업(fetch는 내부적으로 Promise 객체를 이용.)의 상태가 settled가 될 때 까지 await이 기다려주고,
- 결과를 res 변수에 담아준다.
- 그리고 순차적으로 res 식별자의 내용이 출력되고, "hello"가 출력된다.
이제 async함수를 이용하면 비동기작업을 포함하고도 동기적인 동작을 설계할 수 있겠다.
asnyc 함수를 이용해서 원하는 순서대로 출력하기
async function init() {
const res = await fetch(`https://api.github.com/users/abc1234`);
console.log(res);
console.log("Data Loaded Finish");
}
init();
console.log("End");
위의 결과를 바탕으로 이어질 코드 결과를 예측해보자.
당연히, End가 먼저 출력된다.
await 키워드는 async 함수 내에서 Promise만을 기다려주기 때문이다.
사실, 전역 컨텍스트를 async 함수로 다룰 수 있는 방법이 없다.
그래서 꼼수(?)를 생각해보면
전역 컨텍스트에 선언한 코드들을 한번 더 async 함수 안에 묶어서 실행하면 되지 않을까?
async function init() {
const res = await fetch(`https://api.github.com/users/abc1234`);
console.log(res);
console.log("Data Loaded Finish");
}
async function main() {
await init();
console.log("End");
}
main();
이처럼 할 init(), console.log를 async main 함수에 넣어주고,
실제로 전역에서는 main 함수만을 실행하는 방법이다.
async 함수 내에서 await 키워드를 이용하면 동기적 실행이 가능하다는 점을 바탕으로, 한번 더 감싸주었다.
이제 원했던 결과대로 순서대로 출력이 가능해졌다.
async await에 더불어 Promise.all 응용하기
이전 포스팅에서 다뤘던 프로미스 정적 메서드로는 resolve,reject 외에 all,race,allSettled가 있었다.
이제 async await를 배웠으니 이 정적 메서드를 더 잘 사용할 수 있다.
이어질 코드를 이해하기 위해 가정을 세워보자.
학생 A,B,C가 각자 시험에 응시한다.
학생 A,B,C가 모두 마킹을 끝냈다면, 감독관에게 조기 하교를 요청할 수 있다.
하지만, 학생 A,B,C 중 단 한명이라도 시험에 응시중이라면, 조기 하교를 요청할 수 없다.
학생 A,B,C는 각자 시험에 응시하므로, 비동기적으로 시험에 응시하는 셈이다.
(동기적으로 응시함이란, A 시험 응시가 끝나고 B가 응시하며, B가 시험을 마쳐야 C가 시험을 보는 상황이다.)
학생 A,B,C 중 누가 시험 마킹을 마지막으로 마칠 것인지 알 수 없는 상황이다.
그러므로 Promise.all을 이용하면 모두 마친 시점에 하교 요청을 할 수 있다.
const takeAnExam = (order) => new Promise((resolve) => setTimeout(() => resolve(order), Math.random() * 1000));
async function init() {
const finished = await Promise.all([takeAnExam("A"), takeAnExam("B"), takeAnExam("C")]);
console.log(finished);
if (finished) console.log("모든 응시자가 시험을 마쳤습니다.");
}
init();
takeAnExam 함수는 order를 인자로 받아서 0.000초 ~1.000초 뒤 실행되는 타이머 함수를 통해 Promise의 resolve를 반환한다.
랜덤 시간이 지나면 A,B,C 학생이 마킹을 완료하며,
마지막 학생까지 완료했다면 all 메서드에서 A,B,C학생의 resolve 데이터를 담은 배열이 반환된다.
배열이 반환되면 모든 학생이 마킹을 완료한 것이다.
모든 응시자가 시험을 마쳤다는 메시지를 출력하자.
이처럼 3개의 프로미스를 병렬 실행하며 모두 완료된 시점을 all 메서드를 통해 포착할 수 있다.
그리고 그 시점까지 await 키워드로 기다릴 수 있다.
모두 완료가 된 시점에서, 이후 실행하고자 하는 명령은 await 키워드가 담긴 코드 바로 아래에 적으면 된다.
생각해보면 이와 같은 예시가 현실에도 많다.
닭갈비 집에서 모두 식사를 완료해야 밥을 비빈다던지,
술자리에서 모두 술잔을 비워야 다음 술잔을 채운다던지..
7개의 글로 나눠 작성한 비동기를 마치며..
동기와 비동기의 작동하는 방법부터 콜백 헬에서 벗어나고 에러를 처리하며 병렬 처리를 도입하고
마치 동기로 작동하는 작업처럼 사용해야 하는 모든 상황을 겪으면서 아주 고생을 했다...
심지어 비동기는 디버깅도 답이 없다.
싫다고 미뤄봤자 앞으로 계속 괴롭힐 것이기 때문에.. 날 잡고 공부하자고 마음먹고 열심히 공부했고
나와 같은 어려움을 겪는 분들에게 도움이 되고자 열심히 정리해봤다.
내용에 오류가 없겠다고는 말 못하겠다. 혹시 의문이 생기면 직접 찾아보며 더 학습하시길!
자바스크립트에서 비동기와 동기로 상당한 고생을 겪고 있다면
동기 비동기를 비교한 포스팅부터 이 포스팅까지 쭉 읽어보면, 아마 큰 흐름을 잡는데에는 도움이 될 듯 하다.
참고한 자료
'JavaScript > theory' 카테고리의 다른 글
[함수형] 클로저 Closure에 대해 알아보자. (0) | 2022.01.06 |
---|---|
[객체] 자바스크립트에서 객체를 생성하는 다양한 방법 (0) | 2022.01.04 |
[자바스크립트] 마이크로 태스크 큐와 프로미스 (0) | 2021.08.22 |
[자바스크립트] 프로미스를 이용한 비동기 작업 병렬 처리 (0) | 2021.08.22 |
[자바스크립트] 프로미스 객체 (0) | 2021.08.22 |