[Typescript] JavaScript의 비동기(Asynchronous)적 처리
1. 서두
이번 포스팅에서는 JavaScript의 비동기적 처리 방법에 대해 정리했다.
우리가 이용하는 JavaScript는 Single-Thread 언어이다. 그로인해 JavaScript는 동시에 한 가지의 작업만 처리할 수가 있어서 A라는 작업을 처리한 후 B라는 작업을 처리하려고 할 때, 만약 A라는 작업이 완료하는데 오랜 시간이 걸린다면 A가 처리될 때까지 기다린 후 B를 처리해야한다.
이러한 동기식 처리는 코드 한줄 한줄을 차례대로 실행하기 때문에 위와같은 상황에선 시간과 자원의 낭비 문제가 심각해진다. 그래서 JavaScript는 비동기적 처리가 가능하도록 설계되어 이러한 문제점을 해결했다.
2. 비동기(Asynchronous)적 처리란?
동기(Synchronous)적 처리는 말그대로 작업을 요청함과 동시에 해당 결과를 바로 받을 수 있고, 받아야만 하는 데이터 처리 방식을 말한다.
비동기(Asynchronous)적 처리는 동기적 처리와 반대로 작업을 요청한 후 해당 결과를 바로 받지 않아도 되는 데이터 처리 방식을 말한다.
실생활에서 볼 수 있는 예를 들자면
어떤 음식점에서 앞 손님의 주문을 받은 뒤 음식 제공을 완료하고 다음 손님의 주문을 받는다면 이는 동기적 처리이고,
모든 주문을 한꺼번에 받은 뒤 주문한 요리가 완성되는 대로 손님에게 음식을 제공한다면 이는 비동기적 처리하라고 할 수 있다.
3. JavaScript Runtime 구조
JavaScript의 비동기적 처리 과정을 이해하려면 우선 JavaScript Runtime의 구조를 알아야 할 필요가 있다.
- JavaScript Engine : JavaScript 코드를 실행하는 프로그램 또는 인터프리터이다. 하나의 Memory Heap과 하나의 Call Stack으로 이루어져있다 (Call Stack이 하나이기 때문에 JavaScript가 Single-Thread인 것이다.) Call Stack에 쌓인 함수들을 맨 위부터 처리하게 되며 처리가 완료된 함수는 Call Stack에서 Pop 된다.
- Memory Heap : 메모리 할당과 호출이 일어나는 구조화되지 않은 메모리 영역이다. JavaScript 코드에서 선언한 변수, 함수가 저장된다.
- Call Stack : 호출된 함수가 Stack 구조로 쌓이는 메모리 영역이다. 선입후출 구조이다.
- Web API : 웹브라우저, Node.js 등의 JavaScript Runtime에서 제공하는 API로 DOM, AJAX, Timeout, Animation, Promise 등의 비동기 작업을 수행한다.
- Event Loop : Call Stack과 CallBack Queue(Task Queue, Microtask Queue, Animation Frame)의 상태를 검사하여 Call Stack이 빈 상태가 되면 CallBack Queue에 보관된 첫번째 CallBack 함수를 Call Stack에 Push하는 작업을 반복적으로 처리한다. 이러한 반복적인 작업을 Tick이라고 한다.
- Task Queue : Macrotask Queue 라고도 불리며, Web API에서 비동기적으로 실행된 CallBack 함수를 Event Loop가 Call Stack으로 전달하기 전까지 보관하는 영역이다. 선입선출 구조이다.
- Microtask Queue : Promise 함수를 통해 비동기적으로 실행된 CallBack 함수를 Event Loop가 Call Stack으로 전달하기 전까지 보관하는 영역이다. 선입선출 구조이며, Task Queue보다 Event Loop에 대한 우선순위를 가진다.
- Animation Frame : Web API의 requestAnimationFrame( )를 호출하면 비동기적으로 실행되는 CallBack 함수를 Event Loop가 Call Stack으로 전달하기 전까지 보관하는 영역이다. 선입선출 구조이고 Event Loop에 대한 가장 마지막 우선순위를 가진다.
JavaScript Engine은 하나의 Thread에서 돌아간다. 그것은 즉 하나의 Stack을 가지고 있다는 것이고, 그렇기 때문에 동시에 하나의 작업만 처리할 수 있다는 의미이다.
그로인해 AJAX 통신을 한다거나 비동기적으로 이벤트를 처리하는 등의 작업은 Web API에서 처리하게 된다.
그런데 여기서 한가지 의문점은,
분명 JavaScript Engine은 동시에 하나의 작업만 처리할 수 있는데 어떻게 다수의 작업을 비동기로 처리할 수 있냐라는 것이다.
그것은 바로 Web API와 Event Loop와 CallBack Queue에 그 해답이 있다.
4. JavaScript의 비동기적 처리 과정
JavaScript Engine은 Web API와 Event Loop와 CallBack Queue를 통해 비동기 작업을 처리 할 수 있게 되는데,
JavaScript 비동기적 처리 과정의 전체적인 과정은 다음과 같다.
- JavaScript Engine은 Call Stack에 쌓여있는 함수를 지속적으로 처리한다. 처리가 완료되면 Call Stack에서 Pop한다.
- Event Loop는 Call Stack이 완전히 비었는지 지속적으로 검사한다.
- 비동기 함수가 호출 되면 JavaScript Engine은 CallBack 함수와 함께 Web API에 비동기 작업 요청을 전달하고 해당 비동기 함수를 Call Stack에서 Pop한다.
- Web API에 요청했던 비동기 작업이 완료되면 해당 비동기 작업의 CallBack 함수를 CallBack Queue에 Enqueue 한다.
- 만약 Event Loop가 Call Stack이 완전히 비었다는 것을 확인했다면, CallBack Queue의 첫 번째 CallBack 함수를 Call Stack으로 Push하고 CallBack Queue에서 Dequeue한다.
- 이러한 과정을 반복한다.
Event Loop는 Call Stack과 CallBack Queue(Task Queue, Microtask Queue, Animation Frame) 사이에서 지속적으로 Call Stack이 완전히 비었는지 확인하고, Call Stack이 완전히 비었다면 CallBack Queue에 보관된 CallBack 함수를 Call Stack으로 전달하고 해당 CallBack을 CallBack Queue에서 Dequeue하는 작업을 수행한다. 이런 반복작업을 Tick이라고 한다.
이때, Event Loop는 CallBack Queue 중 Microtask Queue를 우선적으로 검사하여 Call Stack으로 전달할 CallBack 함수가 있는지 검사한다. 이후 Microtask Queue가 완전히 빈 상태가 되면 Event Loop는 Task Queue를 검사하게 되고, 동일한 방법으로 작업을 수행한 뒤 Task queue가 완전히 빈 상태가 되면 Animation Frame을 확인하게 된다
(정리하자면 Event Loop가 우선적으로 검사하는 순서는 1. Microtask Queue, 2. Task Queue, 3. Animation Frame 이다.)
Event Loop에 검사 우선도가 존재하기 때문에 CallBack 함수 실행 순서에 대해 주의해야한다.
아래 링크에 Event Loop CallBack Queue 검사 우선도로 인한 CallBack 함수 실행 순서에 대한 예시 코드가 있으니 참고하길 바란다.
https://sculove.github.io/post/javascriptflow/#event-loop-%EC%99%80-queue
Web API는 JavaScript Runtime에서 제공하는 비동기 작업 수행을 위한 API이다. 비동기 함수가 호출이 되면 JavaScript Engine이 CallBack 함수와 함께 Web API에 작업요청을 전달하고 해당 비동기 함수는 Call Stack에서 제거된다. Web API는 요청받은 작업을 수행하고, 완료가 되면 기본적으로 CallBack Queue 중 Task Queue에 해당 작업의 CallBack 함수를 Enqueue하게 된다.
근데 만약 Web API에 요청한 작업이 Promise 함수라면 요청한 작업을 처리한뒤 해당 작업의 CallBack 함수를 CallBack Queue 중 Mircotask Queue에 Enqueue하게 되고,
요청한 작업이 requestAnimationFrame api 함수라면 해당 작업의 CallBack 함수를 CallBack Queue 중 Animtion Frame에 Enqueue한다
(이때 Animation Frame에 보관된 CallBack 함수들은 다음 프레임이 렌더링 되기전에 실행됨이 보장되고, Event Loop를 통해 Animation Frame 내의 모든 CallBack 함수들이 모두 Call Stack으로 전달되어 Animation Frame이 완전히 빈 상태가 되면 브라우저는 다음 프레임을 렌더링한다.)
전체적인 컨셉을 다시 요약하자면,
비동기 함수가 호출 됐을때 Web API를 통해 비동기 작업을 처리하고 작업이 완료되면 해당 비동기 함수의 CallBack 함수를 CallBack Queue에 잠깐 저장해뒀다가 Event Loop가 JavaScript Engine의 유휴상태 여부를 지속적으로 모니터링 하고 JavaScript Engine이 유휴상태일때 Event Loop가 CallBack Queue에 저장해둔 CallBack 함수를 하나씩 JavaScript Engine에게 전달하여 처리한다는 것이다.