ES8(= ES2017) 이전 버전의 JavaScript에서는 비동기 프로그래밍을 위해 콜백 함수나
Promise
문법을 사용하곤 했다. 특히 ES6(= ES2015) 버전의 JavaScript에서 등장한 Promise
문법은 기존에 사용하던 콜백 함수의 한계점들을 많이 해결하였다. 그러나 콜백 함수 기반의 비동기 프로그래밍에 익숙하지 않은 사람들에게 Promise
문법은 여전히 낯설 수밖에 없었다. 결국 Promise
문법도 콜백 함수 기반이었기 때문이다. 그러다가 ES8(= ES2017) 버전의 JavaScript가 등장하면서부터 비동기 프로그래밍을 위한 새로운 문법이 등장했다. 바로 async
, await
문법이다. 이 문법은 기존에 사용하던 콜백 함수나 Promise
문법에 비해 동기 프로그래밍과 거의 흡사하게 비동기 프로그래밍을 할 수 있도록 하였다. 이로 인해 비동기 프로그래밍에 익숙하지 않은 사람들도 비동기 프로그래밍에 조금 더 쉽게 다가갈 수 있게 되었다. 그렇다면 async
, await
문법에 대해 한 번 본격적으로 알아보도록 하자.1. async, await 기본 문법
함수 선언 시
function
앞에 async
키워드를 붙이면 async
함수가 된다.async
함수를 호출한 결과에 해당하는 반환 값은 반드시 Promise
객체이다. 만약 async
함수가 Promise
객체가 아닌 값을 반환하면 그 값으로 이행되는 Promise
객체를 생성하여 대체한다. 즉, 다음 예시에서 foo
와 bar
는 완전히 동일하다.그리고
async
함수 내에서 캐치되지 않은 예외가 발생하면 그 예외로 거부되는 Promise
객체를 생성하여 반환한다. (async
와 await
를 사용할 때의 예외 처리에 관해서는 뒷부분에서 자세히 다룬다.) 즉, 다음 예시에서 foo
와 bar
는 완전히 동일하다.그런데 지금까지는
await
키워드를 사용하지 않는 async
함수에 대해서만 다루었다. 그러니 이번에는 await
키워드를 사용하는 async
함수도 한 번 알아보자. 그러려면 먼저 await
키워드에 대한 기본 지식이 있어야 한다. 하나씩 알아보도록 하자.먼저,
await
키워드는 async
함수에서만 사용할 수 있다. 즉, sync 함수에서는 await
키워드를 사용할 수 없다. 다음으로, await
키워드의 뒤에 오는 값은 반드시 Promise
객체이다. 만약 Promise
객체가 아닌 값이 await
키워드의 뒤에 온다면 그 값으로 이행되는 Promise
객체를 생성하여 대체한다. 즉, 다음 예시에서 foo
와 bar
는 완전히 동일하다.await
키워드를 사용하면 await
키워드의 뒤에 오는 Promise
객체가 이행되거나 거부될 때까지 기다린다(코드의 실행이 중단된다). 만약 해당 Promise
객체가 이행된다면 그 이행 결괏값이 await
키워드 부분을 대체함과 동시에 코드의 실행이 재개된다. 하지만 해당 Promise
객체가 거부된다면 예외가 발생하게 된다. (async
와 await
를 사용할 때의 예외 처리에 관해서는 뒷부분에서 자세히 다룬다.) 예시는 다음과 같다.사실상 이 정도가
async
, await
기본 문법의 전부이다. 콜백 함수와 Promise
에 대한 이해만 전제되어 있다면 추가로 공부할 게 그리 많지 않은 것이다. 그렇다면 이제 async
, await
를 활용하는 실제 예시들을 한 번 살펴보자.2. async, await 활용 예시
먼저, 다음과 같이 두 개의 함수를 정의하자. 하나는 2초가 지난 후에 이행되는
Promise
객체를 반환하는 resolveAfter2Seconds
함수이고, 다른 하나는 1초가 지난 후에 이행되는 Promise
객체를 반환하는 resolveAfter1Second
함수이다.그리고 위 두 함수를 적절히 호출하는 네 종류의 함수를 다음과 같이 정의하자. 우리는 이 네 종류의 함수를 통해
async
, await
가 어떤 흐름으로 코드를 실행시키는지 알아볼 것이다. 그러니 각 함수의 호출 결과를 아래에서 보기 전에 먼저 한 번 예측해보기 바란다.각 함수의 호출 결과는 다음과 같다.
다음으로, 한 학생의 정보를 가져오고 그 학생의 학급 정보를 다시 가져오는 코드를 생각해보자. 이를 위해 먼저 다음과 같은 두 종류의 함수를 정의하자. 하나는 특정 학생의 정보를 가져오는
fetchStudent
함수, 다른 하나는 특정 학급의 정보를 가져오는 fetchClass
함수이다. 이 함수들을 호출하면 각각 학생의 정보와 학급의 정보를 가져오도록 하는 Promise
객체가 반환된다.그러면 이제 위 함수들을 이용하여 한 학생의 정보를 가져오고 그 학생의 학급 정보를 다시 가져오는 코드를 작성해보면 다음과 같다.
눈치가 빠르다면, 동기 프로그래밍을 할 때와 거의 유사한 방식으로 코드를 작성하였음을 알 수 있을 것이다. 이것이 바로
async
, await
의 강점이다. 코드가 작성된 순서대로 실행이 되는 것이 인간의 뇌에는 가장 직관적이다. 만약 Promise
기반으로 위 코드를 작성하고자 했다면 콜백 함수가 두 번 중첩되는 형태가 될 것이기 때문에 코드를 읽기가 어려워질 것이다.3. async, await 예외 처리 방식 (vs Promise)
그렇다면
async
, await
를 사용할 때는 예외 처리를 어떤 식으로 하면 될까? 만약 Promise
기반으로 비동기 코드를 작성했다면, 예외가 발생하는 경우 거부된 Promise
객체에 대해 catch
메소드를 호출하면 예외 처리가 가능하였다. 다음 예시처럼 말이다.위의 코드를
async
, await
를 사용하여 다시 작성해보면 다음과 같다. 앞서 보여준 예시 코드와 동일한 것이다.await
키워드의 뒤에는 Promise
객체가 오는데, 그 Promise
객체가 이행이 된다면 예외가 발생하지 않고 코드가 정상적으로 실행될 것이다. 그러나 그 Promise
객체가 거부된다면 그것은 곧 예외의 발생으로 이어진다. 따라서 동기 코드를 작성할 때와 마찬가지로 예외가 발생할 수 있는 부분을 try catch
블록으로 감싸면 예외 처리가 가능해진다.4. 참고: async, await 실행 흐름 (동작 원리)
async
함수의 코드는 0개 이상의 await
키워드에 의해 분할된 것으로 생각할 수 있다. 만약 await
키워드가 없다면 모든 코드가 중단 없이 동기적으로 실행될 것이다. 그러나 await
키워드가 두 개라면, 첫 번째 await
키워드까지가 첫 번째 코드 덩어리, 첫 번째 await
키워드부터 두 번째 await
키워드까지가 두 번째 코드 덩어리, 두 번째 await
키워드부터 마지막까지가 세 번째 코드 덩어리가 되는 것이다.그렇다면
await
키워드를 마주칠 때 일어나는 일은 무엇일까? 앞에서는 await
키워드의 뒤에 오는 Promise
객체가 이행되거나 거부될 때까지 코드를 중단시키는 역할이라고만 설명하였다. 그러나 여기서 의문점이 생길 수 있다. "기다리지 않고 다른 작업을 하는 게 비동기라고 하지 않았나? 근데 기다리면 동기와 다를 게 뭐지?" 같은 식으로 말이다. 좋은 의문이다. await
키워드를 마주치는 순간 일어나는 일을 간략하게 설명하자면 다음과 같다.A
라는 함수를 실행하던 도중 await promiseB
키워드를 마주치면, 그 함수는 (promiseB
와는 다른) Promise
객체(promiseA
라고 하자)를 하나 새로 만들어서 즉시 반환하고 A
의 스택 프레임이 스택에서 즉시 팝 된다. 즉, 마냥 기다리지 않고 A
를 호출한 곳으로 다시 돌아가서 다음 코드를 실행할 수 있게 하는 것이다. 이때 promiseA
는 A
의 실행 결과(반환 값, 예외 발생 여부)에 따라 이행 또는 거부되는 Promise
객체이다. 이후 시간이 흘러 promiseB
객체가 이행 또는 거부되면, 아까 중단시켰던 코드부터 다시 실행이 되도록 A
의 스택 프레임을 다시 스택에 푸시한다. 이는 마치 콜백 함수가 실행되는 메커니즘과 비슷하다고 볼 수 있다. 그렇게 A
의 실행을 마치고 나면 그 결과에 따라 promiseA
가 이행 또는 거부될 것이고, 만약 promiseA
도 await
대상이었다면 마찬가지 메커니즘으로 그 부분부터 다시 코드가 실행될 것이다. 이것이 async
함수 내에서 await
키워드의 역할이다.결국,
await
키워드가 중단시키는 코드라는 것은 해당 await
키워드가 존재하는 함수 내의 코드인 것이다(모든 코드가 아니라). 그곳에서의 코드만 실행을 중단시킨 뒤, 해당 함수를 호출한 곳으로 다시 돌아가서 다음 코드를 실행한다. 이러한 원리로, 마냥 기다리지 않고 다른 작업을 수행할 수 있게 되는 것이다. 다음 예시를 참고하자.본 글은 아래 링크의 내용을 참고하여 학습한 내용을 나름대로 정리한 글임을 밝힙니다.