콜백 함수 시리즈
2019. 06. 08 수정
1. async / await
JS 프로그래밍을 하다보면 비동기 방식을 많이 사용하게 되는데요, 비동기 호출 후 이를 처리하는 콜백 함수의 개념은 매우 중요합니다.
즉, 비동기 프로그래밍에서 콜백 함수는 반드시 사용해야 하는 부분이죠.
그런데 콜백 함수가 깊어지면 코드가 복잡해지게 되므로( 가독성이 안좋아지므로 ) ES6에서 Promise를 도입했습니다.
( 콜백함수에 대한 내용은 여기를 , Promise에 대한 내용은 여기를 참고해주세요. )
하지만 Promise를 사용해도 여전히 코드가 복잡했는데요.
그래서 ES8에서 async와 await을 도입했고, 덕분에 비동기 코드를 동기적으로 깔끔하게 처리할 수 있게 되었습니다.
그렇다고 항상 async/await이 옳다고는 할 수 없습니다.
- 콜백의 깊이가 깊지 않을 때는 작성하기 간편한 콜백함수를 호출하거나, Promise를 사용하는 것이 더 나은 방법일 수도 있습니다.
- async/await은 Promise를 사용하기 때문에 Promise를 알아야 하고, async/await이 할 수 없는 동작을 Promise로 해결할 수 있는 경우도 있습니다.
이제 비동기 코드를 깔끔하게 작성할 수 있도록 도와주는 async/await에 대해 알아보도록 하겠습니다.
2. 예시로 보는 기본문법
1) async / await 키워드
콜백함수를 사용하기 위해서는 async와 await 키워드를 사용합니다.
async function foo(){
await someAsyncFunction(){...}
await anotherAsyncFunction(){...}
}
- 함수 이름 앞에 async 키워드
- 호출할 비동기 함수 앞에 await 키워드를 사용합니다.
핵심은 await이며, async는 단지 선언용입니다.
즉, 함수 앞에 async가 선언되어 있어야만 await이 적용됩니다.
위의 예제는 someAsyncFunction, anotherAsyncFunction 두 함수가 비동기 코드일지라도 async / await이 적용되면,
항상 someAsyncFunction -> anotherAsyncFunction 순서대로 함수가 실행됩니다.
이처럼 비동키 코드를 동기적으로 수행하게 해주는 것이 async / await입니다.
이 때 async/await은 Promise 방식을 사용하기 때문에 someAsyncFunction과 anotherAsyncFunction 함수는 Promise를 리턴해야 합니다.
이제 본격적으로 async/await이 정말로 Promise 방식을 사용하는지 살펴보면서, async/await 코딩 방법에 대해 알아보도록 하겠습니다.
2) 비동기 함수가 Promise를 사용하지 않는 예제 ( async / await 적용이 안된 예제 )
async function test(){
await foo(1, 2000)
await foo(2, 500)
await foo(3, 1000)
}
function foo(num, sec){
setTimeout( function(){
console.log(num);
}, sec);
}
test();
위의 예제는 비동기함수 setTimeout() 함수를 호출할 때 async/await을 사용해서 test() 함수 실행 결과 1,2,3 순서대로 응답되기를 기대한 것입니다.
그러나 1, 2, 3 순서대로 호출되지 않고, 이벤트가 끝난 순서대로 호출되었습니다.
즉, asycn/await으로 비동기 코드를 동기식으로 바꾸고 싶었지만 비동기 코드가 그대로 수행이 되었죠.
문법은 이게 맞는데 왜 그런걸까요?
다음 코드를 보겠습니다.
3) 비동기 함수에 Promise를 사용하는 예제 ( async / await이 적용되어 동기적으로 출력 )
async function test(){
await foo(1, 2000)
await foo(2, 500)
await foo(3, 1000)
}
function foo(num, sec){
return new Promise(function(resolve, reject){
setTimeout( function(){
console.log(num);
resolve("async는 Promise방식을 사용합니다.");
}, sec);
});
}
test();
이번에는 foo() 함수가 Promise를 반환하도록 수정했습니다.
그랬더니 의도대로 비동기 함수들이 순서대로 1, 2, 3을 응답했습니다!
그 이유는 await은 Promise를 받기 때문입니다.
즉, 비동기 함수 foo() 실행 결과 Promise를 리턴하면 await은 이를 받아 동기적으로 수행하게 해줍니다.
이로써 async/await은 Promise 방식을 따른다는 것을 확인했네요.
4) test함수를 Promise로 바꾸기
이번에는 async/await과 Promise 중 어느 것이 깔끔한 표현인지(누가 더 가독성이 좋은지) 비교하기 위해 위의 예제 test() 함수를 Promise로 바꿔 표현해보도록 하겠습니다.
function test(){
foo(1,2000)
.then( () => {
return foo(2,500)
})
.then( () => {
return foo(3,1000)
})
}
function foo(num, sec){
return new Promise(function(resolve, reject){
setTimeout( function(){
console.log(num);
resolve("async는 Promise방식을 사용합니다.");
}, sec);
});
}
test();
결과는 "3) 비동기 함수에 Promise를 사용하는 예제"와 같이 1, 2, 3 순서대로 출력됩니다.
코드가 복잡해질수록 async/await과 Promise 중 어느 방식이 더 가독성이 좋을까요?
사람마다 느끼는 것은 다르겠지만... 저는 async/await 방식이 더 간결해 보이네요.
5) async/await과 Promise 섞어쓰기
이번에는 async/await이 Promise를 사용한다는 점을 이용하여 Promise와 섞어서 사용해보도록 하겠습니다.
async function test(){
await foo(1, 2000)
await foo(2, 500).then( () => {
foo(3, 1000)
})
}
function foo(num, sec){
return new Promise(function(resolve, reject){
setTimeout( function(){
console.log(num);
resolve("async는 Promise방식을 사용합니다.");
}, sec);
});
}
test();
async/await과 Promise를 혼합해서 활용할 수 있다는 점을 보여주는 코드입니다.
그 이유는 async/await이 Promise를 반환하기 때문이죠.
3. 더 자세한 문법
앞의 예제들을 통해 async/await 사용법에 대해 알아보았습니다.
이번에는 async/await을 사용할 때 주의해야 할 문법과 표현에 대해 알아보도록 하겠습니다.
1) await은 aysnc함수 안에 포함되어야 합니다.
예제 - 올바르지 못한 문법
async function test(){
function goo(){
await foo(1, 2000)
}
await foo(2, 500)
await foo(3, 1000)
}
function foo(num, sec){
return new Promise(function(resolve, reject){
setTimeout( function(){
console.log(num);
resolve("async는 Promise방식을 사용합니다.");
}, sec);
});
}
test();
위의 예제를 실행하면 위와 같이 "await은 async 함수에서만 유효하다."는 에러가 발생합니다.
test() 함수 앞에 async를 선언했는데 무엇이 문제일까요?
test() 함수를 수정해보도록 하겠습니다.
예제 - 실행이 되긴 되는 방법 ( 올바른 결과 x )
async function test(){
async function goo(){
await foo(1, 2000)
}
goo()
await foo(2, 500)
await foo(3, 1000)
}
foo(1,2000) 함수의 await이 동작하기를 바라며 goo() 함수 앞에 async 키워드를 선언했습니다.
실행을 해보면 문제가 없어서 async/await이 적용된 것 같지만, 결과는 1,2,3이 순서대로 출력되지 않았습니다.
그 이유를 알아보기 위해 아래의 예제들을 더 살펴보도록 하겠습니다.
2) async 함수안에 async 함수
예제 - 실험1
// 코드 A
async function test(){
async function goo(){
await foo(1, 100)
}
goo()
await foo(2, 500)
await foo(3, 1000)
}
// 코드 B
async function test(){
async function goo(){
await foo(1, 700)
}
goo()
await foo(2, 500)
await foo(3, 1000)
}
코드 A에서 foo(1, 100) 함수를 호출했더니 순서대로 호출됐습니다.
그런데 코드 B에서 foo(1, 700)을 호출했더니 출력 순서가 또 바뀌었습니다.
이 예제를 통해 알 수 있는 것은 goo()함수와 나머지 비동기 함수가 경쟁을 하고 있다는 것입니다.
예제 - 실험2
async function test(){
async function goo(){
await foo(1, 700)
}
goo()
await foo(2, 500)
await foo(3, 100)
}
이번에는 0.5초와 0.1초로 설정하여 foo() 함수를 호출해보도록 하겠습니다.
여기서 눈여겨 볼 점은 goo() 함수와 나머지 비동기 함수는 경쟁을 하고 있지만, foo(2, 500) 함수와 foo(3, 100) 함수는 호출 순서를 유지하고 있다는 것입니다.
예제 - await으로 호출하면 해결 !
async function test(){
async function goo(){
await foo(1, 2000)
}
await goo()
await foo(2, 500)
await foo(3, 1000)
}
이제 정상적인 결과를 위해, goo() 함수에 await 키워드를 작성했습니다.
그러면 의도대로 1, 2, 3이 응답됩니다.
지금까지 예제들의 결론은 async 함수(test) 내부에 async 함수(goo)를 선언했을 때, goo함수 내부에 await을 선언하더라도 goo함수를 호출할 때 await을 선언해야 한다는 것입니다.
async/await을 사용하던 중 궁금해서 이것저것 실험했던 내용들을 주저리 주저리 설명했는데, 결론적으로는 바로 위의 코드만 기억하시면 될 것 같습니다.
3) async의 여러 표현
async의 기본 표현은 다음과 같았습니다.
async function test(){
await foo(1, 1000)
}
이를 즉시 실행 함수로도 표현할 수 있으며 다음과 같습니다.
(async function test(){
await foo(1, 1000)
})()
arrow 함수로도 표현할 수 있습니다.
let test = async () => {
await foo(1, 1000)
}
async 함수는 위와 같이 즉시 실행함수, arrow 함수로 표현이 모두 가능합니다 !
이상으로 async와 await에 대해 알아보았습니다.
비동기 함수를 다룰 때 사용하는 기법으로 3가지가 있는데요.
- callback 함수를 익명함수로 사용하는 방법
- Promise를 사용하는 방법
- async/await를 사용하는 방법
각각 장단점이 있으니 때에 따라 알맞게 사용하시면 좋을 것 같습니다.