
먼저, TypeScript / JavaScript의 비동기처리를 살펴보기 전에, 싱글 스레드인 JavaScript가 어떻게 비동기를 처리할 수 있는지 짚고 넘어가야 할 것 같습니다.
먼저, JavaScript가 동작하기 위해서는, JavaScript Engine이 필요합니다. JavaScript Engine은 여러 종류가 있는데, 그 중 가장 유명한 engine인 Google의 V8를 기준으로 작성하려 합니다!
JIT(Just In Time) 컴파일러란?
기본적으로 실행 중에 컴파일을 합니다. 즉, 런타임에 컴파일을 진행하며 이미 변환된 코드를 캐시에 저장시켜, 캐시에 이미 있는 코드는 다시 변환하지 않고 바로 사용하는 것이죠.
저희는 비동기 처리 방식에 집중할 예정이라, 런타임의 JavaScript Engine가 주로 수행하는 역할만 보도록 하겠습니다. 🙂
JavaScript Engine은 런타임에 주로 Heap Memory 할당과 단일 스레드 Call Stack을 관리합니다.

JavaScript는 앞서 언급했듯이, 기본적으로 싱글스레드로 동작합니다. call stack에 쌓인 함수나 코드를 위에서부터 차례대로 실행하는 거죠. 말 그대로 자료구조 stack과 같이 동작합니다.
만약 아래와 같은 코드를 실행한다면, call stack에는 어떤 과정이 일어날까요?
function add(a, b) {
return a + b;
}
function average(a, b) {
return add(a, b) / 2;
}
let x = average(10, 20);
아래 그림과 같이 호출되는 순서대로 쌓아놓고, 차례대로 수행하는 모습을 볼 수 있습니다. (main()은 여기서 처음 실행시 생성되는 전역 실행 컨텍스트)

사실 JavaScript를 실행할 때에는, JavaScript Engine 외에도 관여하는 요소들이 있습니다. 바로, Wep API, Task Queue, Event Loop입니다.

setTimeout이나 HTTP 요청(ajax) 메서드, DOM 이벤트 등의 메서드를 지원해줍니다.여기서 중요한 것은 바로, Task Queue 입니다. JavaScript에서 비동기로 호출되는 함수들은 Call Stack이 아닌 Task Queue에 보내지게 됩니다.
아래 간단한 비동기 코드를 실행할 때, JavaScript 런타임 구성 요소들이 어떻게 동작하는 지 살펴보도록 하겠습니다.
function greet() {
return "Hello!"
}
function respond() {
return setTimeout(() => {
return "Hey!";
}, 1000))
}
greet()
respond()

greet() 함수가 쌓이고, 바로 실행되어 Hello! 라는 output을 반환하고 있습니다.respond()가 call stack에 쌓이고, responsd() 내에서 호출하고 있는 setTimeout()이 casll stack에 들어옵니다.
setTimeout함수는 JavaScript Engine이 아닌 Web API가 처리하므로, setTimeout의 callback 함수를 Web API에게 전달합니다.
Web API에서는 인자로 전달한 시간인 1000ms 동안 타이머가 실행됩니다.
setTimeout 함수는 call stack에서 제거됩니다.

1000ms 동안 실행되던 타이머가 종료되면, callback 함수는 Task Queue에 추가됩니다.

Event Loop는 call stack이 비어 있을 경우, Task Queue에 있는 첫 번째 항목을 call stack에 추가합니다.
한 마디로 정리하자면, JavaScript 런타임에는 JavaScript Engine만이 아닌 다른 구성 요소도 있고, 그 구성 요소 중 Web API 라는 친구가 있기에 비동기 처리가 가능하다는 것입니다!
이제 동작 방식을 알았으니, 비동기 작업을 어떻게 실행할 수 있는지 코드 위주로 알아보겠습니다.
아까, 비동기 작업 실행 과정 속에서 setTimeout의 callback 함수가 언급됐었는데 callback 함수가 대체 뭔지 살펴보겠습니다.
Callback 함수란? 다른 함수의 인자로 전달되는 함수입니다.
저희는 두 개의 숫자와 연산 방식만 함수에 전달하면, 계산기처럼 결과를 반환하는 함수를 만들어야 합니다. 아래와 같은 방식을 사용할 수 있겠죠.
const operator = (operator: string, a: number, b: number): number => {
if (operator === '+')
return a + b;
else if (operator === '-')
return a - b;
}
하지만 위와 같은 방식은 유지 보수하기에도 어렵고, 연산 방식 개수에 따라 if문을 사용해야 합니다. 한 번 callback 함수를 이용해 봅시다.
const add = (a: number, b: number): number => {
return a + b
}
const subtract = (a: number, b: number): number => {
return a - b
}
type Operator = (a: number, b: number) => number
const operator = (operator: Operator, a: number, b: number): number => {
return operator(a, b)
}
const sum: number = operator(add, 1, 3)
const difference: number = operator(subtract, 3, 1)
Operator 라는 callback 함수의 type 선언operator 함수에 operator 라는 함수 인자로 add 또는 subtract 함수를 전달합니다.operator 함수 내에서 전달 받은 함수를 호출하면 끝!이런 게 바로 callback 함수입니다.
그렇다면, callback 함수를 이용해 어떻게 비동기 작업을 수행할 수 있는 걸까요? 바로, 아까 봤던 setTimeout 함수를 사용하면 비동기 작업을 수행할 수 있습니다.
setTimeout() 은 일정 시간 기다린 후에 어떤 코드를 실행하기 위해 사용하고, 첫 번째 인자로 실행할 함수(callback)를 받고, 두 번째 인자로는 지연 시간(ms 단위)을 받습니다. 이 함수는 앞서 JavaScript 런타임에 관여하는 Web API가 제공하는 함수입니다.
아래는 setTimeout()과 callback 함수를 이용한 비동기 작업 코드입니다.
function asyncFunction1(callback: () => void): void {
setTimeout(() => {
console.log("비동기 함수 완료");
callback();
}, 1000);
}
console.log("시작");
asyncFunction1(() => {
console.log("모든 비동기 함수 완료");
});
console.log("끝");
setTimeout() 수행 -> 1ms 동안 타이머 실행결과적으로, call stack에 들어가 있는 함수가 실행되는 동안 setTimeout()의 타이머가 실행되고, 그 후에 setTimeout의 callback 함수가 실행되는 것을 볼 수 있었습니다.
하지만, callback 함수로 비동기 작업을 실행하는 것은 한계가 있습니다. 바로 일명 'callback 지옥' 이라는 현상 때문인데요.
바로 다음과 같은 코드를 일컫는 말입니다.
function asyncFunction1(callback: () => void): void {
setTimeout(() => {
console.log("첫 번째 비동기 함수 완료");
callback();
}, 1000);
}
function asyncFunction2(callback: () => void): void {
setTimeout(() => {
console.log("두 번째 비동기 함수 완료");
callback();
}, 1000);
}
function asyncFunction3(callback: () => void): void {
setTimeout(() => {
console.log("세 번째 비동기 함수 완료");
callback();
}, 1000);
}
console.log("시작");
asyncFunction1(() => {
asyncFunction2(() => {
asyncFunction3(() => {
console.log("모든 비동기 함수 완료");
});
});
});
딱 보기에도 복잡해 보입니다.😵💫
asyncFunction1()의 callback 함수는 asyncFunction2()이고,asyncFunction2()의 callback 함수는 asyncFunction3()이고,asyncFunction3()의 callback 함수는 () => {console.log("모든 비동기 함수 완료")} 입니다.시작
(1초 후) 첫 번째 비동기 함수 완료
(2초 후) 두 번째 비동기 함수 완료
(3초 후) 세 번째 비동기 함수 완료
(3초 후) 모든 비동기 함수 완료
이것보다 더 많은 작업을 연달아서 해야 한다면 기하급수적으로 코드 양이 늘어날 것이고, 이 코드를 유지보수하는 생각을 한다면.. 😱 아주 끔찍할 것입니다.
이런 callback 지옥을 끝낼 수 있는 것이 있는데요, 바로 Promise라는 친구입니다. 다음 글에서 이어서 작성하도록 하겠습니다!
gif 출처: https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif