📌 동기(Synchronous)와 비동기(Asynchronous)
예전 자바스크립트에 처음 입문 하던 때에 교수님께서 동기와 비동기의 차이를 설명하기 위해 카페에서 대기번호를 기다리는 손님들의 사진을 보여주셨다. 동기와 비동기의 차이를 설명하는데 있어 그 사진만큼 적절한게 없어 지금도 기억이 생생하다.

(최대한 그때와 비슷한 이미지로 찾아 가져왔다 출처: gobae.tistory.com)
손님들이 카운터 앞에 줄서서 기다리게 되면 그 뒤에 손님들은 앞의 손님이 커피를 받을 때까지 주문을 할 수 없다. 반면 손님에게 진동벨을 쥐어주고 메뉴가 나오는 순서에 따라 진동벨을 호출하게 되면 앞 손님이 커피 수령 유무와 상관 없이 주문한 커피가 나오는대로 가져갈 수 있다. 이렇듯 앞 전의 처리가 완료될 때까지 기다려 순차적으로 다음 동작을 진행하는 방식을 동기방식(Synchronous), 주문 순서와 상관 없이 응답 값을 전달 받는 방식을 비동기방식(Asynchronous)라고 한다.
자바스크립트는 인터프리터 언어다. 엔진 자체가 하나의 호출 스택만을 가지고 있고, 싱글 스레드 기반으로 운영되기 때문에 소스코드는 한 줄씩 읽혀 내려가며, 해당 소스에 대한 명령은 바로 처리된다. 즉 기본적으로 동기 방식을 사용하고 있다는 얘기다. 그렇다면 자바스크립트 기반의 구동 환경에서는 앞사람이 주문한 커피가 나올때까지 기다려야하는가? 만약 그렇다면 우리는 서버(카운터)에 요청한 api에 대한 응답(커피) 값이 돌아올때까지 다른 로직들을 대기시켜야 하고, 화면마다 긴 대기 시간을 갖게 될 것이다. 이러한 불필요한 대기 시간을 해결하기 위해 우리는 Promise 같은 비동기 함수를 사용한다.
📌 비동기(Asynchronous) 방식
1. 방식 과정
비동기 함수를 알아보기 전 브라우저 동작 방식에 대해 먼저 짚고 넘어가보자.
Heap : 우선 순위를 정하기 위해 만들어진 이진트리 자료구조. 동적 객체들의 참조 값을 갖고 있다.
Call Stack : 실행 컨텍스트들로 구성된 스택.
Web API : 웹 서버, 웹 브라우저를 클라이언트의 요구사항에 맞게 동작하도록 도와주는 애플리케이션 프로그래밍 인터페이스.
Event Loop : 콜 스택이 비었을 경우 태스크 큐의 콜백 함수를 처리.
Task Queue : Set으로 구현(Queue 아님). 태스크 중 실행 가능한 가장 오래된 태스크 호출.
만들어진 함수가 호출되면 해당 함수의 실행 컨텍스트(한 스코프의 시작과 끝) 들은 Call Stack에 쌓여 순차적으로 진행되게 된다. 실행 컨텍스트는 브라우저의 환경에 맞게 Web API에 담기게 되는데, 이때 해당 작업이 비동기적일 경우 Task Queue로 저장된다.
2. 마이크로 태스크(MicroTask)
앞 서 설명한 Task Queue에는 여러 task가 존재한다. task는 보통 MicroTask와 MacroTask 둘로 세분화되는데, 둘 다 비동기적 작업이라는 공통점을 갖고 있지만 어떤 역할을 하느냐에 따라 해당 역할들을 담아두는 Queue에 분류되어 들어가게 된다.

자바스크립트의 런타임 구조 (출처: whales.tistory.com)
마이크로 태스크(MicroTask) : Promise의 handler(then/catch/finally), async await function, process 함수, Object.observe 등을 처리
매크로 태스크(MacroTask) : timer, DOM 이벤트 콜백, I/O 등 처리
Call Stack이 비워지게 되면 Event Loop는 Task Queue의 task를 순차적으로 Call Stack에 올려보내게 된다. 이때 작업의 MicroTask는 MacroTask보다 더 높은 우선순위를 가지고 있다. 때문에 Call Stack이 비었을 경우 MicroTask의 작업이 먼저 진행되고, Event Loop는 Call Stack의 작업 유무를 계속해서 확인하면서 MicroTask Queue에 쌓여 있는 task들을 처리한다. MicroTask Queue의 태스크가 전부 처리되면 UI 랜더링 작업을 수행한 뒤 MacroTask를 하나씩 처리한다.
MicroTask -> UI 랜더링 -> MacroTask
예시를 살펴보자
⭐️ Promise
function test() {
console.log(test 시작)
new Promise(resolve => {
console.log(promise 시작)
resolve(true)
})
.then(() => console.log(promise 끝))
console.log(test 끝)
}
console.log(코드 시작)
test()
console.log(코드 끝)
해당 로직의 로그는 어떻게 보여질까?
정답은
코드 시작
test 시작
promise 시작
test 끝
코드 끝
promise 끝
이다.
앞서 얘기했듯 모든 실행 컨텍스트는 Call Stack 에 담겨 즉시 실행되게 된다. '코드 시작'을 시작으로 '코드 끝' 까지 Call Stack에 담겨 순차적으로 한 줄 씩 진행되게 된다. 그러나 Promise의 콜백 객체는 MicroTask Queue에 저장되기 때문에 Call Stack이 비워질떄까지 대기 상태가 되고, Call Stack의 '코드 끝'을 실행하고 나면 그제야 Event Loop를 통해 Queue의 담긴 'promise 끝'이 Call Stack으로 올라와 마지막으로 실행되는 것이다.
⭐️ Async Await
Async Await는 Promise 패턴의 단점을 개선하고자 ES8(2017)에 출시된 문법이다.
앞서 봤던 Promise의 콜백 함수처럼 Async 함수 역시 MicroTask로 구분된다. 함수가 실행되며 Await를 만나게 되면 진행을 멈추고 해당 Async 함수를 MicroTask Queue에 담게 된다. 이에 따라 해당 뒤의 로직은 Async 함수의 결과와 상관 없이 진행되게 되며 이는 Async Await가 비동기적으로 작동한다 라고 볼 수 있다.
예시를 보자.
test2(){
return Promise.resolve(true)
}
async test() {
console.log(2)
await test2()
console.log(3)
}
console.log(1)
test()
console.log(4)
test함수는 Async Await를 담고 있다. test2함수의 콜백을 만나는 시점에 test함수는 MicroTask에 담기게 되고, Call Stack이 비워질때까지 기다리게 된다.
때문에
1
2
4
3
의 순서로 담기게 된다.
⭐️ 최상위 레벨 Await 호출
ECMAScript 2022에서는 Async 없이도 Await를 사용해 비동기 호출이 가능하도록 기능이 확장되었다.
기존의 Async Await의 경우 모듈 초기화 전 변수에 접근이 가능해 undefined가 발생할 수 있다는 단점을 갖고 있었다.
let result
async function test() {
const response = await fetch('api')
result = await response.json()
}
test()
그러나 Await를 최상위 레벨에서 호출하게 되면 해당 Promise의 콜백이 완료되지 않은 경우 Await가 할당된 비동기 변수에 접근할 수 없게 되어 기존 단점을 보완할 수 있다.
const response = await fetch('api')
const result = await response.json()
외부 모듈에서 접근하는 경우 호출하는 모듈에 대한 비동기 처리를 promise.all()로 묶기 때문에 해당 모듈의 비동기 작업이 완료되기 전까지 결과에 접근 할 수 없다.
누군가 javascript는 동기인가요 비동기인가요?를 질문했을때 사람마다 각자의 이유로 다양한 대답을 내놓을 것이다. 몇몇의 커뮤니티나 블로그 글들만 찾아봐도 다양한 생각들이 있는 것 같다. 정답은 없다고 생각하지만 누군가 나에게 이런 질문을 물어본다면 나는 javascript는 동기 언어 입니다 라고 대답할 것 같다. 자바스크립트의 엔진은 하나의 동작만을 처리할 수 있는 싱글 쓰레드이며 해당 동작이 처리되기 전까지 대기해야하기 때문이다. 그럼에도 불구하고 우리가 비동기적인 작업이 가능한 것은 브라우저가 자바스크립트 엔진 뿐만이 아닌, Web API도 제공해주기 때문이다. Web API를 통해 우리는 타이머, HTTP 요청 등에 대한 처리를 비동기로 식별해 사용할 수 있다.