자바스크립트는 어떻게 실행되는가?

April 20, 2022 ˑ 17min read

thumbnail

개요

자바스크립트는 태생적으로 웹 페이지의 동적인 기능을 개발하기 위해 탄생하였습니다. 이러한 특징은 자바스크립트가 다른 언어와의 차이점을 갖도록 만들었고 자바스크립트만의 특징을 이해하는 것이 중요해지는 이유이기도 합니다.

자바스크립트는 인터프리터 언어로써 C++, Java 같은 컴파일러 언어처럼 코드를 컴파일하는 과정없이 실행할 수 있다는 특징을 갖습니다. 때문에 실행 속도 측면에서는 불리할 수 있지만 개발 과정을 빠르고 간편하게 만들었다는 장점을 갖습니다.

또한, 많은 언어들이 멀티 스레드를 이용하여 여러 작업을 처리하는 반면, 자바스크립트는 싱글 스레드와 이벤트 루프를 이용하여 효과적으로 작업을 처리합니다. 이와 같은 자바스크립트의 특징을 잘 파악한다면 더욱 효율적이고 안정적인 코드를 작성하는데 도움이 될 것입니다.

이 글에서는 자바스크립트가 실제로 어떻게 실행되는지 알아보기 위해 실행 환경과 내부적인 동작에 대해 알아보려고 합니다.

자바스크립의 런타임 환경

작성한 자바스크립트 코드가 실행되려면 코드가 실행될 수 있는 런타임 환경이 필요합니다. 가장 대표적인 런타임으로는 웹 브라우저를 들 수 있습니다. 그리고 브라우저 이외의 터미널 환경에서 자바스크립트를 실행할 수 있는 Node.js를 들 수 있습니다. 이후에 등장한 Deno와 Bun도 여기에 해당합니다.

그렇다면 런타임 환경은 어떻게 구성되어 있을까요?

javascript-runtime

런타임 환경은 단순히 코드를 실행하는 역할만 수행하는 것은 아닙니다. 런타임 환경은 자바스크립트 코드를 실행하는 엔진을 포함하여, 유용한 작업을 수행할 수 있는 API와 내부 모듈을 제공하기도 합니다. 그리고 싱글 스레드로 코드를 실행하는 이벤트 루프도 런타임 환경의 구성 요소 중 하나입니다.

자 그러면 자바스크립트 런타임의 구성 요소를 하나씩 살펴보겠습니다.

자바스크립트 엔진

자바스크립트 엔진은 실제로 코드를 실행하는 주체로써 런타임 환경의 핵심 요소로 볼 수 있습니다. 가장 대표적인 엔진으로는 Google에서 개발한 V8 엔진을 들 수 있습니다. V8 엔진은 크로미움 기반의 브라우저는 기본적으로 사용합니다. 그리고 Node.js가 바로 이 엔진을 브라우저 바깥에서도 사용할 수 있도록 하면서 주목을 받기 시작했습니다. V8 엔진 이외에도 Firefox의 SpiderMonkey, Safari의 JavaScriptCore 등 각 브라우저 환경마다 사용하는 고유의 자바스크립트 엔진이 있습니다.

Note

자바스크립트 엔진과 헷갈릴 수 있는 레이아웃 엔진

레이아웃 엔진은 HTML, CSS를 파싱하여 렌더링을 담당하는 엔진으로 DOM/CSSOM 트리 생성, 레이아웃, 페인드 같은 동작을 수행합니다. 대표적인 레이아웃 엔진으로는 Blink(Chrome, Webkit 기반), Webkit(Safari), Gecko(Firebox) 가 있습니다.

자바스크립트 엔진이 담당하는 주요 역할을 살펴보겠습니다.

코드 파싱 및 컴파일

작성한 자바스크립트 코드를 실행하려면 인터프리터 언어라 할지라도 컴퓨터가 이해할 수 있는 형태로 변환해야합니다. 자바스크립트 엔진은 내부적으로 인터프리터와 JIT(Just-In-Time) 컴파일러를 통해 이 과정을 수행합니다. 단계별로 간단히 살펴보겠습니다.

  • 파싱 — 자바스크립트 코드를 분석하여 AST(Abstract Syntax Tree)를 생성.
  • 인터프리터 실행 — AST를 순회하면서 코드를 한줄씩 해설하고 바이트 코드를 생성하거나, 경우에 따라 직접 실행.
  • 프로파일링 및 최적화
    • 코드가 인터프리터를 통해 실행되는 동안 자바스크립트 엔진은 프로파일러를 통해 어떤 코드가 자주 실행되는지(“핫 스팟” 이라고 부름) 파악하는 등 모니터링을 진행함.
    • 프로파일러를 통해 자주 사용되거나 최적화가 필요하다고 판단된 코드는 JIT 컴파일러를 통해 CPU가 매우 빠르게 실행 가능한 최적화된 기계어로 변환.
  • 최적화된 코드 실행 — 위 과정에서 변환된 코드를 실행

메모리 관리

코드 실행에 필요한 메모리 공간(콜 스택, 힙)을 관리하고, 더 이상 사용하지 않는 메모리를 가비지 콜렉션을 통해 정리하여 메모리 누수를 방지하는 역할을 합니다.

여기까지 자바스크립트 엔진을 통해 코드가 어떤 과정을 거쳐 실행되는지 알아보았습니다.

API와 내부 모듈

자바스크립트라는 언어만으로는 웹 페이지의 버튼의 색상을 바꾼다거나, 파일을 읽는 등의 작업은 수행할 수 없습니다. 이러한 동작을 구성하기 위해서는 자바스크립트 런타임에서 제공하는 API와 내부 모듈이 필요합니다.

브라우저 환경

브라우저에서 실행되는 자바스크립트 코드인 만큼 브라우저와 도큐먼트에 접근할 수 있는 Web API를 제공합니다. 대표적인 API들을 살펴보겠습니다.

  • DOM(Document Object Model) API — Document 내 HTML 요소에 접근하여 조작할 수 있는 API.
  • BOM(Browser Object Model) APIwindow 객체를 통해 제공되며, navigator, location, history, screen 등 하위 API에 접근할 수 있음.
  • XMLHttpRequest API — 서버에 비동기적으로 데이터를 요청할 때 사용.
  • Storage APIlocalStorage, sessionStorage 등 브라우저 저장소에 접근하여 데이터를 저장.
  • 등등..
Caution

모던 브라우저들은 UI 스레드를 블록하여 프레임 드롭을 유발하는 복잡한 로직을 효율적으로 처리할 수 있도록 Web Workers API를 제공합니다. 이러한 환경은 웹 페이지의 자바스크립트가 실행되는 환경과 별도로 존재하기 때문에 DOM API를 사용할 수 없는 등 주의가 필요합니다.

터미널 환경

Node.js와 같은 터미널 환경 런타임은 컴퓨터 시스템에 직접 접근할 수 있는 내장 모듈을 제공합니다. 내장 모듈은 모듈 방식에 따라 require() — CommonJS, import 모듈 from '모듈명' — ES Modules 으로 불러올 수 있습니다. 그럼 주요 모듈을 살펴보겠습니다.

  • fs — 컴퓨터의 파일 시스템에 직접 접근하여 파일을 읽고 쓸 수 있는 기능을 제공.
  • http/https — 웹 서버를 만들거나, 다른 서버에 요청할 때 사용.
  • os — 컴퓨터의 운영체제에 대한 정보를 얻기 위해 사용.
  • path — 파일, 디렉토리의 경로를 다룰 때 사용.
  • process — 실행 중인 Node.js 프로세스에 대한 정보를 얻어나 작업을 수행할 때 사용.

제공되는 API, 모듈이 환경별로 다르다보니 작성하는 코드가 어떤 환경에서 실행되는지를 명확히 구분해야합니다.

Tip

런타임 환경에서 제공하지 않는 API나 모듈을 사용하는 것을 방지하기 위해 TypeScript에서 제공하는 lib 설정과 @types/node를 통해 타입 선언을 등록할 수 있습니다.

브라우저 환경과 터미널 환경에서 제공하는 API와 내장 모듈은 주로 비동기 방식으로 동작하는데요. 그 이유는 자바스크립트가 싱글 스레드로 실행되는 것과 관련이 있습니다.

자바스크립트가 싱글 스레드만으로 어떻게 효율적인 처리를 하는지 알아보겠습니다.

싱글 스레드와 이벤트 루프

자바스크립트 런타임은 주로 웹 페이지나 서버 어플리케이션에서 사용자 이벤트나 네트워크 요청이 발생할 때마다 함수를 실행합니다. 만약 함수 실행이 오래 걸린다면 싱글 스레드로 실행되는 자바스크립트는 이어 발생한 이벤트나 요청을 처리할 수 없는 블로킹 상태가 되어버립니다. 블로킹 상태가 되면 브라우저는 어떤 UI 업데이트도 진행할 수 없게되고, 서버의 경우 어떠한 응답도 할 수 없게됩니다.

그렇기 때문에 Web API와 내장 모듈은 논블로킹 비동기 방식으로 설계되었습니다. 핵심 포인트는 이렇게 실행된 함수들은 메인 스레드가 직접 실행하지 않고 런타임에게 작업을 위임해 버린다는 것입니다. 메인 스레드는 이러한 작업을 기다리지 않고 즉시 다음 작업을 수행합니다.

해당 동작에 대해 그림으로 살펴보겠습니다.

async-function

  1. 메인 스레드는 setTimeout을 통해 1초 뒤에 callback 함수를 실행하도록 요청하고 곧바로 함수의 나머지 부분과 콜 스택의 다음 함수를 실행
  2. 1초가 지나면 자바스크립트 런타임은 callback 함수를 태스크 큐에 등록
  3. 콜 스택이 비게되면 이벤트 루프를 통해 순서대로 콜 스택에 적재되어 실행
Note

setTimeout API는 등록한 시간이 되면 태스크 큐에 콜백 함수를 등록하기 때문에 콜백 함수가 실행되는 정확한 시간을 보장하지 못합니다.

3번 동작에서 주의 깊게 보아야할 점으로 이벤트 루프는 콜 스택이 비어있을 때 태스크 큐에 있는 함수를 실행한다는 점 입니다.

이벤트 루프는 태스크큐에 등록된 콜백 함수를 어떻게 실행할까요? 좀 더 자세히 알아보았습니다.

태스크 큐, 마이크로 태스크 큐

태스크 큐는 매크로 태스크 큐와, 마이크로 태크스 큐로 나뉘며 일반적으로 부르는 태스크 큐는 매크로 태스크 큐를 의미합니다. 태스크 큐에는 타이머, UI 이벤트, 네트워킹과 같은 대부분의 Web API가 작업을 완료한 뒤 실행할 콜백 함수가 등록됩니다.

마이크로 테스크 큐는 일반적으로 Promise가 완료된 후 실행되는 콜백 함수가 등록됩니다. 주의할 점은, Promise 객체를 생성할 때 인자로 등록하는 콜백은 태스크 큐에 등록되고 resolve, reject 이후 실행되는 콜백 함수가 여기에 해당된다는 점입니다. 그리고 Promise를 가독성 있게 처리할 수 있도록 제공되는 async/await 문에서도 await 이후에 작성되는 코드는 then 함수에 등록하는 영역에 해당하므로 마이크로 태스크 큐에 등록되어 실행됩니다.

태스크 큐와 마이크로 태스크 큐는 다음 규칙에 따라 콜 스택에 적재되어 실행됩니다.

  1. 이벤트 루프는 콜 스택이 비어 있는 경우 우선 마이크로 태스크에 실행할 작업이 있는지 검사합니다.
  2. 마이크로 태스크 큐에 작업이 있는 경우 모든 작업을 태스크 큐에 적재하고 실행합니다.
  3. 마이크로 태스크 큐에 작업이 없는 경우 태스크 큐에서 하나의 작업을 콜 스택에 적재하고 실행합니다.

2번 동작에서 마이크로 태스크 큐에 있는 모든 작업을 콜 스택에서 실행하던 중, 마이크로 태스크 큐에 또 다른 작업이 추가된다면 어떻게 될까요?

이벤트 루프는 콜 스택이 비어있을 때만 태스크큐, 마이크로 태스크 큐에 등록된 함수를 적재하고, 마이크로 태스크 큐를 우선적으로 모두 처리한다는 특징 때문에 최악의 경우 실행되지 못하는 경우가 생길 수 있습니다.

requestAnimationFrame

이외에도, Web API는 모니터의 주사율에 따라 부드러운 애니메이션을 그릴 수 있도록 requestAnimationFrame 함수를 제공합니다. WHATWG의 Web API 표준에 따르면 해당 함수를 통해 등록된 콜백은 테스크 큐도 마이크로 테스크 큐도 아닌 독자적인 Map을 통해 관리되며 이벤트 루프의 렌더링 단계에서 이전 프레임에서 등록된 모든 콜백 함수를 실행한다고 합니다.

마치며

이번 글에서는 자바스크립트가 실행되는 환경에는 어떤 구성 요소가 있는지 알아보고, 싱글 스레드로 동작하기 위해 어떤 방식으로 동작하는지 알아보았습니다. 매일 같이 실행하는 자바스크립트 코드가 어떻게 동작하는지 조사하면서 몰랐던 내용들도 알게되어 뿌듯한 기분이 들었습니다. 이 글을 접하신 분들도 자바스크립트의 동작에 대해 알아보고 계셨다면 조금이나마 도움이 되었으면 좋겠습니다.

Related Articles

Github

Linkedin

Instagram