자바스크립트의 큰 특징 중 하나는 단일 스레드 기반의 언어라는 점입니다. 스레드가 하나라는 말은 곧, 동시에 하나의 작업만을 처리할 수 있다라는 말입니다. 하지만 실제로 자바스크립트가 사용되는 환경을 생각해보면 맣은 작업이 동시에 처리되고 있는 걸 볼 수 있습니다.

예를 들면, 웹브라우저는 애니메이션 효과를 보여주면서 마우스 입력을 받아서 처리하고, Node.js 기반의 웹서버에서는 동시에 여러 개의 HTTP 요청을 처리하기도 합니다.

어떻게 스레드가 하나인데 이런 일이 가능할까? 질문을 바꿔보면 ‘자바스크립트는 어떻게 동시성(Concurrency)을 지원하는 걸까?

이때 등장하는 개념이 바로 Event Loop 입니다. Node.js를 소개할 때 “Event Loop 기반의 비동기 방식으로 Non-Blocking IO를지원하고…” 와 같은 문구를 본 적이 있을 것입니다. 즉, 자바스크립트는 Event Loop 를 이용해서 비동기 방식으로 동시성을 지원합니다. 동기 방식의 (Java 같은) 다른 언어를 사용하다가 Node.js 등을 통해 자바스크립트를 처음 접하게 되는 사람들은 이 Event Loop의 개념이 익숙하지 않아서 애를 먹습니다. 뿐만 아니라 자바스크립트를 오랫동안 사용해서 비동기 방식의 프로그래밍에 익숙한 사람들조차 Event Loop가 실제로 어떻게 동작하는지에 대해서는 자세히 모르는 경우가 많습니다.

ECMAScript에는 이벤트 루프가 없다.

웬만큼 두꺼운 자바스크립트 관련 서적들을 뒤져보아도 Event Loop에 대한 설명은 의외로 쉡게 찾아보기가 힘들다. 그 이유는 아마, 실제로 ECMAScript 스펙에 이벤트 루프에 대한 내용이 없기 때문일 것입니다. 좀더 구체적으로 표현하면 ECMAScript에는 동시성이나 비동기와 관련된 언급이 없다고 할 수 있다. (ES6 부터는 조금 달라졌다)

실제로 V8과 같은 자바스크립트 엔진은 단일 호출 스택(Call stack)을 사용하며, 요청이 들어올 때마다 해당 요청을 순차적으로 호출 스택에 담아 처리할 뿐입니다. 그렇다면 비동기 요청을 어떻게 이루어지며, 동시성에 대한 처리는 누가 하는 걸까? 바로 이 자바스크립트 엔진을 구동하는 환경, 즉 브라우저나 Node.js가 담당하게 됩니다. 먼저 브라우저 환경을 간단하게 그림으로 표현하면 다음과 같습니다.

브라우저 실행 환경

브라우저 실행 환경

위 그림에서 볼 수 있듯이 실제로 우리가 비동기 호출을 위해 사용하는 setTimeout 이나 XMLHttpRequest 와 같은 함수들은 자바스크립트 엔진이 아닌 Web API 영역에 따로 정의되어 있습니다. 또한 Event Loop와 Task Queue와 같은 장치도 자바스크립트 엔진 외부에 구현되어 있는 것을 볼 수 있습니다. 다음은 Node.js 환경입니다.

Node.js 실행 환경

Node.js 실행 환경

이 그림에서도 브라우저의 환경과 비슷한 구조를 볼 수 있습니다. 잘 알려진 대로 Node.js는 비동기 IO를 지원하기 위해 libuv 라이브러리를 사용하며, 이 libuv가 Event Loop를 제공합니다. 자바스크립트 엔진은 비동기 작업을 위해 Node.js의 API를 호출하며, 이때 넘겨진 콜백은 libuv의 Event Loop를 통해 스케쥴되고 실행됩니다.

이제 어느 정도 감이 잡힐 것입니다. 각각에 대해 좀더 자세히 알아보기 전에 한가지만 확실히 짚고 넘어가자면, “자바스크립트가 단일 스레드 기반의 언어”라는 말은 “자바스크립트 엔진이 단일 호출 스택을 사용한다.” 는 관점에서만 사실입니다. 실제 자바스크립트가 구동되는 환경(브라우저, Node.js 등) 에서는 주로 여러 개의 스레드가 사용되며, 이러한 구동 환경이 단일 호출 스택을 사용하는 자바스크립트 엔진과 상호 연동하기 위해 사용하는 장치가 바로 Event Loop 인 것입니다.