JavaScript는 싱글 스레드 언어이다.
'싱글'스레드는 한 번에 하나의 작업만 수행이 가능하다.
반면 Java나 Python은 멀티 스레드를 지원하여 원하는 코드 로직을 동시에 수행시키는 멀티 작업이 가능하다.
그런데 웹 어플리케이션에서는 네트워크 요청이나 이벤트 처리, 타이머와 같은 작업을 멀티로 처리해야 하는 경우가 많다.
만일 싱글 스레드로 브라우저 동작이 한 번에 하나씩 수행되게 되면, 우리가 파일을 다운로드 받을 동안 브라우저는 파일을 다 받을 때까지 웹서핑도 못하고 멈춰 대기해야 할 것이다.
따라서 파일 다운, 네트워크 요청, 타이머, 애니메이션 등 이러한 오래 걸리고 반복적인 작업들은 자바스크립트 엔진이 아닌 브라우저 내부의 멀티 스레드인 Web APIs
에서 비동기 + 논블로킹
으로 처리된다.
비동기 + 논블로킹(Async + Non blocking)은 메인 스레드가 작업을 다른 곳에 요청하여 대신 실행하고, 그 작업이 완료되면 이벤트나 콜백 함수를 받아 결과를 실행하는 방식을 말한다.
쉽게 말해, 파일 다운로드 요청 작업을 백그라운드 작업으로 전이하여 동시에 처리가 가능하도록 한 것으로 이해하면 된다.
싱글 스레드인 자바스크립트의 작업을 멀티 스레드로 돌려 작업을 동시에 처리시키게 하던가, 또는 여러 작업 중 어떤 작업을 우선으로 동작시킬 것인지 결저하는 세심한 컨트롤을 하기 위해 존재하는 것이 바로 이벤트 루프(event loop)
이다.
이벤트 루프는 브라우저 내부의 Call Stack
, Callback Queue
, Web APIs
등의 요소들을 모니터링하면서 비동기적으로 실행되는 작업들을 관리하고, 이를 순서대로 처리하여 프로그램의 실행 흐름을 제어한다.
간단히 표현하자면, 브라우저의 동작 타이밍을 제어하는 관리자라고 보면 된다.
이벤트 루프의 동작 과정을 간단히 살펴보면, 자바스클비트의 setTimeout
이나 fetch
와 같은 비동기 자바스크립트 코드를 브라우저 Web APIs에게 맡기고, 백그라운드 작업이 끝난 결과를 콜백 함수 형태로 큐(Callback Queue)에 넣고 처리 준비가 되면 호출 스택(Call Stack)에 넣어 마무리 작업을 진행한다.
이러한 이벤트 루프를 이용한 프로그램 방식을 이벤트 기반 프로그래밍
이라고 한다.
이벤트 기반 프로그래밍은 프로그램의 흐름이 이벤트에 의해 결정되는 방식이다.
예를 들어, 사용자의 클릭이나 키보드 입력과 같은 이벤트가 발생하면, 그에 맞는 콜백 함수가 실행된다.
대표적으로 자바스크립트의 addEventListner(이벤트 명, 콜백함수)
가 있다.
이벤트 기반 프로그래밍은 비동기 작업을 쉽게 처리할 수 있고, 멀티 스레드 언어에 비해 단순하고 직관적인 코드 작성을 가능하게 하며, 브라우저와 같은 환경에서도 안정적인 실행을 가능하게 하여 사용자와의 상호작용을 높일 수 있다.
따라서 이를 이해하고 적절한 방식으로 비동기 작업을 처리하는 것이 자바스크립트를 이용한 웹 애플리케이션 개발에 있어서 매우 중요하다.
자바스크립트는 왜 싱글 스레드인가?
자바스크립트는 1995년에 넷스케이프에서 웹 브라우저에서 동적인 웹 페이지를 만들기 위해 개발된 스크립트 언어이다.
당시에는 멀티 코어 프로세스가 보편화되지 않았고, 자바스크립트는 웹 브라우저에서 간단한 스크립트 동작을 수행하는 데 주로 사용되었기 때문에 복잡한 병렬 처리를 필요로 하지 않아 메모리 사용량이 적고, 동기화 문제를 피할 수 있는 싱글 스레드로 구현하였다.
그러나 싱글 스레드는 오래 걸리는 작업이 실행되면 다른 작업들이 대기해야 하므로 응답성이 떨어진다.
또한, CPU 코어를 여러 개 사용할 수 없으므로 성능이 제한된다.
이러한 문제들을 해결하기 위해 언어 자체의 설계를 바꾸는 것보단 브라우저의 멀티 스레드를 이용하는 자바스크립트의 비동기 프로그래밍을 지원하는 것이다.
그리고 이 비동기 프로그래밍의 핵심이이벤트 루프
인 것이다.
(다만,Web worker
최신 기술을 통해 자바스크립트도 멀티 스레드 구현이 가능해졌다.)
자바스크립트를 실행하는 소프트웨어로는 우리가 잘 알고 있는 웹브라우저와 런타임인 Node.js가 있다.
이벤트 루프 동작 원리를 배우기 앞서, 싱글 스레드인 자바스크립트 엔진이 어느 곳을 거쳐 비동기 작업을 수행하는지 이 둘의 내부 구성도를 눈에 익혀보자.
브라우저는 웹 사이트를 화면에 보여주기 위해 여러가지 역할을 하는 부품들로 이루어져 있다.
그중 자바스크립트 비동기 코드의 동작 과정에 관련된 구성 요소는 Web APIs
, Event Table
, Callback Queue
, Event Loop
등이 있다.
Call Stack
: 자바스크립트 엔진이 코드 실행을 위해 사용하는 메모리 구조Heap
: 동적으로 생성된 자바스크립트 객체가 저장되는 공간Web APIs
: 브라우저가 제공하는 API 모음으로, 비동기적으로 실행되는 작업들을 전담하여 처리한다. (AJAX 호출, 타이머 함수, DOM 조작 등)Callback Queue
: 비동기적 작업이 완료되면 실행되는 함수들이 대기하는 공간Event Loop
: 비동기 함수들을 적절한 시점에 실행시키는 관리자Event Table
: 특정 이벤트가 발생했을 때 어떤 callback 함수가 호출되어야 하는지를 알고 있는 자료구조Web APIs
는 타이머, 네트워크 요청, 파일 입출력, 이벤트 처리 등 브라우저에서 제공하는 다양한 API를 포괄하는 총칭이다.
Web API
는 브라우저에서 멀티 스레드로 구현되어 있다.
그래서 브라우저는 비동기 작업에 대해 메인 스레드를 차단하지 않고 다른 스레드를 사용하여 동시에 처리할 수 있는 것이다.
예를 들어, setTimeout
비동기 작업은 Web APIs의 한 종류인 Timer API
에서 타이머 스레드를 사용하여 타이머를 수행한다.
마찬가지로, XMLHttpRequest
, fetch
와 같은 네트워크 관련 API는 네트워크 스레드를 사용하여 네트워크 요청과 응답을 처리한다.
Web APIs의 대표적인 종류로는 다음과 같다.
<canvas>
요소를 통해 그래픽을 그리거나 애니메이션을 만들 수 있는 메소드들을 제공이때 오해하지 말아야 할 것이, 모든 Web API들이 비동기로 동작하는 것이 아니다.
Web API에는 동기적으로 처리되는 것과, 비동기적으로 처리되는 것이 모두 있다.
예를 들어, DOM API나 Console API는 동기적으로 처리되고,
XMLHttpRequest나 Timer API는 비동기적으로 처리된다.
Web APIs가 여러 API들을 묶어 말하듯이, Callback Queue도 여러가지 종류의 Queue를 묶어 총칭하는 개념이다.
Callback Queue에는 task queue
와 microtask queue
두가지 종류가 있다.
setTimeout
, setInterval
, fetch
, addEventListner
와 같이 비동기로 처리되는 함수들의 콜백 함수가 들어가는 큐promise.then
, process.nextTick
, MutationObserver
와 같이 우선적으로 비동기 처리되는 함수들의 콜백 함수가 들어가는 큐(처리 우선순위가 높다)Callback Queue의 종류에 따라 이벤트 루프가 콜 스택으로 옮기는 순서가 달라진다.
일반적으로 microtask Queue가 우선순위가 높아, 먼저 microtask queue를 처리하여 비우고, 그 다음 task queue의 콜백을 처리한다.
또한, 같은 queue 안에 적재되는 콜백이라도 어떠한 비동기 작업이냐에 따라 우선순위가 다른 태스크들이 있을 수 있다.
예를 들어, Microtask Queue에 적재되는 Promise
와 Mutation Observer
콜백 중 Mutation Observer가 먼저 처리되는 식이다.
브라우저 큐는 콜백 큐 뿐만 아니라 브라우저 애니메이션 작업에 대한 처리를 담당하는 AnimationFrame Queue
도 있다.
자바스크립트 애니메이션 동작을 제어하는 requestAnimationFrame
메소드를 통해 콜백을 등록하면, 이 큐에 적재되어 브라우저가 repaint 직전에 AnimationFrame Queue에 비동기로 처리하도록 구성하면 브라우저가 애니메이션의 타이밍을 관리하고, 적절한 프레임 속도를 유지하고, 다른 탭이나 창에 있을 때 애니메이션을 중지함으로써 브라우저의 애니메이션 동작의 성능과 품질을 향상시킬 수 있다.
NodeJS 환경에서도 브라우저와 거의 비슷한 구조를 볼 수 있는데, 차이점이 있다면 내장된 libuv
라이브러리를 사용하여 비동기 IO를 지원한다는 점이다.
또한, 브라우저에서는 Web API를 사용하여 DOM 조작, AJAX 호출, 타이머 및 애니메이션 등과 같은 다양한 작업을 처리하지만, NodeJS에서는 Web API가 아닌 Node.js API를 사ㅛㅇ하여 파일 시스템 엑세스, 네트워크 엑세스, 암호화, 압축 및 해제 등과 같은 다양한 작업을 처리한다.
예를 들어, Node.js에서 HTTP 요청을 수행하려면 http 모듈을 사용한다.
단, Node.js에서도 일부 Web API를 사용할 수 있는데, setTimeout
, setInterval
등이 그렇다.
이처럼 Node.js에서 자바스크립트 엔진은 비동기 작업을 위해서 Node.js의 API를 호출하며, 이때 넘겨진 콜백은 libuv의 이벤트 루프를 통해 스케쥴되고 실행된다.
Node.js의 내부 구성으로는 다음과 같다.
앞의 내용을 복습하자면, 싱글 스레드인 자바스크립트에서도 작업의 동시 처리를 지원할 수 있는 비결에는 이벤트 루프가 자바스크립트 엔진과 브라우저의 웹 API를 연결하여 비동기적인 일 처리를 가능케 하기 때문이다.
다만, 모든 자바스크립트 코드를 비동기로 처리할 수 있는 것은 아니다.
자바스크립트에는 비동기로 동작하는 비동기 전용 함수가 있는데, 대표적으로 setTimeout
이나 fetch
,addEventListenr
가 있다.
브라우저의 Web APIs는 위 그림과 같이 각각 전용 작업을 처리하는 API 스레드들로 구성된 집합을 말한다.
따라서 setTimeout
이 호출되면 Timer API라는 별도의 스레드에서 타이머 동작이 별도로 실행되는 것이며, fetch
가 호출되면 Ajax API 스레드에서 네트워크 통신이 이루어지는 것이다.
이벤트 루프는 이 비동기 함수 작업을 Web API에 옮기는 역할을 하고, 작업이 완료되면 콜백을 큐에 적재했다가 다시 자바스크립트 엔진에 적재해 수행시키는 일종의 '작업을 옮기는 역할'만을 한다.
작업을 처리하는 주체는 자바스크립트 엔진과 Web API이다.
그래서 이벤트 루프는 Call Stack에 현재 실행 중인 작업이 있는지, 그리고 Task Queue에 대기 중인 작업이 있는지 반복적으로 확인하는 일종의 무한 루프만을 돌고, 대기 작업이 있다면 작업을 옮겨주는 형태로 동작한다고 보면 된다.
예시로 가장 흔한 타이머 비동기 함수의 이벤트 루프 과정을 알아보자.
function bar() {
setTimeout(() => {
console.log('Second');
}, 500);
}
function foo() {
console.lof('First');
}
function baz() {
console.log('Third');
}
bar();
foo();
baz();
위의 자바스크립트 코드 실행 과정은 다음의 순서로 진행된다.
1. bar()
함수가 호출되고 그 안의 setTimeout
함수가 호출되어 스택에 쌓인다.
2. setTimeout
함수의 매개변수에 할당된 콜백 함수를 Timer Web API에 전달한다.
그리고 Timer Web API에서는 백그라운드로 500 밀리초를 셈한다.
3. 다음 foo()
함수가 호출되고 콘솔창에 'First'가 출력된다.
4. 이때 500 밀리초 대기 시간이 만료되면서, 이벤트 루프는 Timer Web API에서 가지고 있던 콜백 함수를 Task Queue로 옮긴다.
5. 그 다음 baz
함수가 호출되고 콘솔창에 'Thrid'가 출력된다.
6. 스택에 있는 모든 메인 자바스크립트 코드가 실행 완료되어 Call STack이 비워지게 된다.
7. 이벤트 루프는 Call Stack이 비어있는 경우를 탐지하여, Task Queue에 있는 콜백 함수를 Call Stack으로 옮긴다.
8. Call Stack에 콜백 함수 코드를 실행하게 되고 콘솔창에는 'Second'가 출력된다.
이 동작 원리의 핵심은 특정한 작업에 대해 비동기로 멀티 작업을 할 수 있다는 것이다.
비동기 동작 예시가 고작 타이머를 셈하는 setTimeout
이라 잘 와닿지 않을 수 있다.
하지만, 파일 입출력이나 키보드 타이핑을 하는 이벤트 동작일 경우라면 어떨까?
비동기가 없다면 파일을 다운 받거나 키보드를 타이핑 하는 동안에는 웹사이트는 멈추게 되어 아무것도 못하게 될 것이다.
바로 이벤트 루프는 이러한 작업들을 별도로 브라우저의 멀티 스레드에게 인가하여 비동기로 처리해주는 핸들러 역할을 하는 것이다.
웹 브라우저의 Web APIs와 Node.js의 Node.js APIs들은 구성은 비슷하지만 동작 측면에서 약간 차이가 있다.
웹 브라우저의 Web APIs는 비동기 작업이 끝나면 스스로 Callback queue에 적재하지만, Node.js API들은 이벤트 루프가 직접 옮겨준다.
예를 들어 Timer Web API에서 타이머가 모두 지나가면, 자바스크립트 환경이 웹 브라우저냐 Node.js냐에 따라 차이가 갈린다.