오늘은 JS의 이벤트 루프와 콜백 큐, 그리고 그와 관련된 것들에 대해 알아본다.
이벤트 루프를 알아보기에 앞서 JS가 단일 스레드 언어라는 것을 알고 있어야 한다.
이는 간단히 설명하자면 JS는 한 번에 두 개 이상의 코드를 처리하지 못 한다.
왜냐하면 JS 엔진은 콜 스택이 하나밖에 없기 때문이다.
이전에 실행 콘텍스트에 대해 설명할 때의 실행 스택이 콜 스택이다. 위키피디아에 따르면, 콜 스택을 부르는 방법은 여러가지라고 한다.
콜 스택(call stack) 이란 컴퓨터 프로그램에서 현재 실행 중인 서브루틴에 관한 정보를 저장하는 스택 자료구조이다. 또한 실행 스택(execution stack), 제어 스택 (control stack), 런 타임 스택 (run-time) 스택 혹은 기계 스택 (machine stack) 이라고도 하며, 그냥 줄여서 스택 (the stack) 이라고도 한다.
JS 엔진은 코드를 읽으며 호출된 함수를 콜 스택에 추가하고, 실행이 완료된 함수는 콜 스택에서 제거한다. 그리고 이 콜 스택은 하나밖에 없으니 동시에 코드를 두 개 이상의 코드를 읽지는 못 하는 것이다.
이벤트 루프를 알아보기에 앞서, JS 엔진에 대해 알아보자. 필요해서 그런다.
우리가 JS 코드를 작성하고 코드를 실행하면, JS 엔진이 코드를 해석(인터프리팅)한다. 그리고 실행 환경에 따라 사용되는 JS 엔진도 달라진다. 크롬이나 Node.js는 V8 엔진을 사용한다. 크롬이나 Node.js에서 JS 코드를 실행하면 V8 엔진이 코드를 인터프리팅 한다는 뜻이다.
JS 엔진은 V8 말고도 그 종류가 꽤 다양하다. 자세한 것은 위키피디아의 JS 엔진 문서에서 확인할 수 있다.
또한 JS 코드가 실행되는 환경을 런타임이라고 일컫는다.
이 글에서는 크롬을 런타임으로 두고 설명한다.
비동기성이란 무엇인가? 프로그래밍에서 동기 / 비동기의 개념은 이렇다.
동기(Syncronous) : 요청을 보내고 해당 요청에 대한 응답을 받기 전 까지 다음 동작을 실행하지 않는다.
비동기(Ansyncoronous) : 요청을 보내고 해당 요청에 대한 응답 여부에 상관없이 다음 동작을 실행한다.
JS는 동기적인 프로그래밍 언어이다. 다음의 코드를 살펴보자.
let a = new XMLHttpRequest();
a.open('GET', 'http://www.example.org/some.file', false);
a.send(null);
console.log(a.response);
console.log("it's async");
위의 코드는 XMLHttpRequest를 통해 해당 url에 GET요청을 보내는 코드이다. open메서드의 세 번째 인자로 false를 주어서 이 코드는 동기적으로 실행된다.
이것이 의미하는 바는 요청을 보내고나서 응답을 받기 전까지 다음 코드가 실행되지 않는다는 것이다. 이를 두고 코드가 블로킹(Blocking) 되었다고 표현한다. 이것은 좋지 않다.
만일 서버에 데이터를 요청하고, 그 데이터를 웹에 나타내는 코드를 실행했다고 생각해보자. 그럼 서버가 요청에 응답해서 데이터를 반환할 때 까지 웹 상의 모든 이벤트는 멈추게 될 것이다. 이 지경이 되면 브라우저는 페이지가 응답이 없으니 기다릴 것인지, 페이지를 나갈 것인지를 묻는 창을 화면에 띄운다. 이것이 형편 없는 사용자 경험이라는 것은 당연하다.
그래서 크롬은 JS 코드를 비동기적으로 실행할 수 있는 여러 web API들을 지원한다.
아래의 코드를 보자.
function first() {
setTimeout(() => {
console.log("first")
}, 1000);
};
function second() {
setTimeout(() => {
console.log("second")
}, 500);
};
first();
second();
first 함수를 호출하고 나서 second 함수를 호출했다.
그런데 실행 결과는 second 함수가 first 함수보다 먼저 실행됐음을 나타내고 있다. 즉, JS코드가 비동기적으로 작동했다. JS코드가 이렇게 비동기적인 작동이 가능한 이유가 바로 이벤트 루프이다.
크롬의 JS 런타임을 그림으로 나타내면 이렇다.
드디어 이 글의 주제인 이벤트 루프가 보인다.
이벤트 루프에 대해 설명하려면 JS 코드가 비동기적으로 작동하는 원리에 대한 설명부터 필요하다.
setTimeout(() => {
console.log("first")
}, 0);
console.log("second");
setTimeout은 첫 번째 인자로 받은 함수를 두 번째 인자로 받은 숫자만큼 ms가 지난 이후에 실행시키는 함수이다. 자세한 건 MDN을 참고하자.
그럼 위의 코드는 console.log("first")를 0ms뒤에 실행시켜달라는 의미가 되겠다. 그럼 first가 출력 된 뒤 second가 출력 될 것이라고 예상하는 것도 이상하지 않다.
그러나 second가 출력 된 뒤 first가 출력 된다. 런타임에서는 무슨 일이 일어났는지 알아보자.
- 실행스택에서 setTimeout 함수가 호출되면 setTimeout의 콜백 함수는 백그라운드라는 공간으로 옮겨집니다. 그리고 setTimeout 함수는 실행 스택에서 사라집니다.
- setTimeout 함수의 동작이 끝났으니 console.log("second")가 실행 스택에 올라오고, second를 console에 출력시키고 종료됩니다.
- 백그라운드의 console.log("first")는 0ms가 지난 뒤 콜백 큐로 옮겨집니다.
- 이벤트 루프는 실행 컨텍스트와 콜백 큐를 감시하고 있다가 콜백 큐에 실행 시킬 함수가 존재하고, 실행 컨텍스트가 완전히 비워지면 콜백 큐에 가장 먼저 들어온 함수부터 차례대로 실행 스택로 옮깁니다.
이상이 위의 코드의 실행결과에 대한 설명이다. 이제 이벤트 루프가 무엇인지, 콜백 큐가 무엇인지 대략 알게 되었다.
사실 콜백 큐의 내부에는 여러 가지 큐(Queue)들이 모여있다. 그리고 각각의 큐는 실행 스택으로 옮겨지는 우선순위가 정해져 있다.
이에 대해서는 sculove님의 블로그 포스트에 잘 정리되어 있다.
요약만 하겠다.
- 콜백 큐는 크게 Microtask Queue, Animation Frames, Task Queue로 구성되어 있다.(실제로는 더 많다.)
- web API의 종류에 따라 다른 Queue로 이동된다.
- 콜백 큐에서 실행 스택으로 옮겨지는 우선 순위는 1.Microtask Queue 2.Animation Frames 3.Task Queue이다.
오늘은 JS 런타임, V8 엔진, 그리고 이벤트 루프와 콜 스택등을 알아 보았다.
다음은 V8 엔진의 다른 한 부분인 메모리 힙에 대해 알아 볼 예정이다.
sculove님의 자바스크립트 비동기 처리 과정
thms200님의 이벤트 루프
beomy님의 자바스크립트 런타임