헷갈리는 동기, 비동기 개념을 정리하는 포스팅 (3)
1) callback에 대한 이해🔗 : https://woojin.tistory.com/27
2) JS 초짜가 Promise 완벽하게 이해하기🔗 : https://woojin.tistory.com/28
이번 포스팅은, async / await 키워드의 사용법과 의미에 대해
無의 상태로 돌아가서 공부하고 정리해보고자 한다.
(* 오류에 대한 지적 환영합니다.)
첫째로는, async/await의 사용법, 의미에 대해서 자세히 다룰 것이고
둘째로는, await의 유무와 비동기 처리에 관계에 대해 이해해볼 것이다.
마지막으로는, 비동기 코드를 병렬 처리하는 .all(), .race() 메서드에 대해 알아볼 것이다.
(유튜브 채널 '드림코딩'의 영상 코드를 토대로 공부했다.)
https://www.youtube.com/watch?v=aoQSOZfz3vQ
async란? (+ 사용법)
async와 await은 비동기를 동기적으로 작성할 수 있는 키워드이다.
이게 도대체 뭔소린지 전혀 이해가 되지 않았다.
우선 Promise를 이해했으니,
Promise가 async로 대치되는 코드를 살펴보자.
// <<< Promise >>>
const fetchUser = () => {
return new Promise((resolve, reject) => {
// do network request in 10 secs ...
resolve('woojin');
});
}
const user = fetchUser();
user.then(console.log);
// <<< Async >>>
const fetchUserAsync = async () => {
// do network request in 10 secs ...
console.log('loading...');
return 'woojin';
}
const user = fetchUser();
user.then(console.log);
보다시피 Promise에서 resolve하던 것을,
async 키워드를 붙이면 그냥 return하면 된다!
이렇게 Promise, resolve를 통해 느껴지던 동기 코드와의 이질감을 해소한다.
(reject는? -> throw error, try catch 사용하면 되는데, 에러 핸들링에 대해서는 자세히 다루지 않겠다.)
await이란? (+ 사용법)
await은 기다려주는 키워드이다!
이렇게 간단한 키워드가 죽어라 이해가 안 되었는데, 추가 설명이 필요하기 때문이다. 바로바로..
"await은 Promise를 반환하는 함수에 쓰여야 한다."
이해하고 나니 너무 당연하다.
서버에 api를 요청해서 유저의 mbti 정보를 받아와서 infp가 맞는지 여부를 반환한다고 생각해보자.
서버에 유저 mbti를 요청하고 받아오는 동안 여부를 확인하는 코드는 동작하면 안 된다!
"mbti 알아올 때까지 기다려~" 라고 말해줘야 하는 상황.
그렇기에 Promise를 반환하고 .then 메서드를 사용한 것 아닌가?
체이닝은 왜 사용했는가?
서버에서 유저의 mbti를 받아온 다음, 서버에 다시 mbti를 보내고 해당 mbti의 특징을 받아온다고 해보자.
mbti를 받아온 다음 특징을 받아와야 하기 때문에
mbti().then(mbti => 특징받아오기()).then(특징 => console.log(특징));
이런 식으로 체이닝 한 것이 아닌가?
아래 코드를 통해 정확히 이해해보자.
const delay = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
}
const getApple = async () => {
await delay(4000);
return '🍎';
}
const getBanana = async () => {
await delay(2000);
return '🍌';
}
getApple().then(console.log);
getBanana().then(console.log);
delay는 단순히 시간을 지연시키는 함수이다.
getApple은 delay를 통해 4초 멈추고 🍎를 반환하고, getBanana는 2초 멈추고 🍌를 반환한다.
위 코드를 실행하면 getApple(), getBanana() 순서로 Promise를 호출하기는 하지만,
비동기적으로 delay 함수가 실행되어 🍌, 🍎 순서로 출력된다.
그럼 마치 mbti를 알아야 그 특성을 아는 것처럼,
사과를 받아야 바나나를 받을 수 있다고 가정하고 순서대로 호출해보자.
먼저 promise chaning을 사용하면 아래와 같다.
const pickFruits = () => {
return getApple().then(apple => {
return getBanana().then(banana => `${apple} + ${banana}`);
})
};
apple에 🍎를 받아와 다시 promise를 반환하는 함수인 getBanana에 넣는다.
어라라? 어딘가 익숙하지 않은가? callback 지옥이 떠오른다..
이런 식으로 chaining한다면 callback 지옥과 다를 것이 없다.
하지만 async/await을 사용하면 아래와 같다.
const pickFruits = async () => {
const apple = await getApple();
const banana = await getBanana();
return `${apple} + ${banana}`;
}
promise, .then()이 async와 await으로 대체된다.
본 코드는 4초 후 apple에 🍎를 저장하고, 다시 2초 후 banana에 🍌를 저장한다.
총 6초 후, '🍎 + 🍌'를 반환한다.
await 유뮤의 차이점
필자는 위 코드를 처음 봤을 때, await의 정확한 의미를 파악하지 못했다.
await이 있고 없고의 차이를 비교하면서 await의 정확한 용도를 파악할 수 있었다.
아직 await의 의미가 헷갈린다면, 아래 코드를 참고해보자.
const delayApple = (ms) => {
return new Promise(resolve => setTimeout(() => resolve('🍎'), ms));
}
const getApple = async () => {
a = await delayApple(10000); // await 사용
fruit = a + '🍌'
return fruit;
}
getApple().then(console.log);
* 참고로 setTimeout의 인자로 들어간 콜백 함수는 'resolve' 꼴이면 가능한데 'resolve('🍎') 꼴이면 안 된다.
resolve()는 함수라기보다 결과에 속하기 때문에, '() => resolve('🍎')' 꼴로 넣어줘야 한다.
위 코드에서, await 키워드 때문에 10초간 delayApple의 결과를 기다렸다가 a에 넣은 후에야
fruit = a + '🍌'가 실행된다. (해당 줄은 Promise를 반환하는 함수가 쓰인 것도 아니고 순서대로 실행하면 되므로 await을 넣을 이유가 없다!)
그렇다면 await이 없는 코드는 어떻게 작동할까?
const delayApple = (ms) => {
return new Promise(resolve => setTimeout(() => resolve('🍎'), ms));
}
const getApple = async () => {
a = delayApple(10000); // await 사용
// console.log(a)
fruit = a + '🍌'
return fruit;
}
getApple().then(console.log);
해당 코드의 출력은 '[object Promise]🍌' 이다.
그리고 10초 후에 프로그램이 종료된다.
delayApple은 10초 후에 resolve('🍎')를 가져오는데,
promise였다면 .then()을 통해 기다렸겠지만,
위 코드에서는 기다리는 코드가 없다!
따라서 a에는 [object Promise]라는 빈 껍데기가 할당된 채로 fruit = a + '🍌'가 실행되고 만다.
그리고 10초 후에 a에는 정상적으로 🍎가 저장되지만, 함수는 이미 리턴값을 반환했기에 의미가 없다.
그렇게 10초가 지나서야 프로그램이 종료된다.
여기서 위 코드의 주석처리된 console.log(a)를 실행하면 'Promise { <pending> }'라고 출력된다!
대기(pending)상태의 Promise 객체라는 것이다.
필자는 이렇게 async, await을 이해했다.
간단하게 정리하자면,
async는 promise를 반환하는 함수에 붙이기만 하면 되고,
await은 promise를 반환하는 함수를 사용할 때 그것을 기다려야하면(비동기적으로 처리해야하면) 앞에 붙인다.
다음에서는 .all() - (await 병렬 처리 후 동시 반환), .race() - (병렬 처리 후 우선 처리된 promise 반환)
이렇게 2개의 메서드에 대해 알아보자.
await 병렬 처리(동시 반환, 우선 처리된 Promise 반환)
const delay = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
}
const getApple = async () => {
await delay(4000);
return '🍎';
}
const getBanana = async () => {
await delay(2000);
return '🍌';
}
// .all() 메서드
const pickAllFruits = () => {
return Promise.all([getApple(), getBanana()]).then(fruits =>
fruits.join(' + ')
);
}
pickAllFruits().then(console.log);
위 코드는 .all() 메서드를 활용해 두 개의 Promise 객체를 동시에 호출한 것이다.
위에서 예로 든 'mbti 받아오기', '해당 mbti 특성 받아오기'는 순차적으로 실행되어야 한다.
하지만, 'mbti 받아오기'와 '성별 받아오기'는 순차적으로 실행될 필요가 없다!
이런 경우 위와 같이 .all()메서드를 활용하면 반환된 Promise 객체들이 배열에 저장된다.
(당연한 얘기지만 await이 아니라 그냥 Promise 코드에도 사용할 수 있다.)
const pickOnlyOne = () => {
return Promise.race([getApple(), getBanana()]);
}
pickOnlyOne().then(console.log);
.race() 메서드는 .all() 메서드와 동일하게 사용되는데, 가장 먼저 반환된 Promise 객체 한 개만 반환한다!
위 코드에서는 getApple()이 실행되는데 4초, getBanana()가 2초 걸리므로
🍌가 반환되어 출력된다.
Typescript로의 변환은 너무 간단해서 일부만 올려두겠다.
const delay = (ms: number): Promise<void> => {
return new Promise(resolve => setTimeout(resolve, ms));
}
const getApple = async (): Promise<string> => {
await delay(4000);
return '🍎';
}
const pickFruits = async (): Promise<string> => {
const apple: string = await getApple();
const banana: string = await getBanana();
return `${apple} + ${banana}`;
}
위 코드로의 변환이면, 본 포스팅에 쓰인 코드를 모두 변환할 수 있다.
이렇게 3편에 걸친 동기/비동기 포스팅이 끝이 났다.
1도 이해 안 가던 것들이 수많은 아티클을 정독하며 결국엔 이해되어 굉장히 뿌듯하고,
나만의 방식으로 이해한 내용을 정리해두었다는 점에 든든한 마음도 든다.
더군다가 정이 가지 않던 JS, TS와도 굉장히 친해져서 좋았다!
질문이나 태클 언제든 환영입니다.
감사합니다.