AI & 코딩

자바스크립트(JS) 파일은 어떻게 실행될까? 브라우저부터 V8 엔진, 이벤트 루프까지 완벽 가이드

디지털가드너 (Digital Gardener) 2026. 6. 6. 21:36

우리가 매일 사용하는 웹 사이트의 화려한 애니메이션, 실시간 데이터 업데이트, 복잡한 사용자 상호작용 뒤에는 항상 자바스크립트(JavaScript)가 있습니다. 하지만 단순히 텍스트로 작성된 .js 파일이 어떻게 컴퓨터의 메모리를 움직이고 화면을 변화시키는지 그 내부 동작을 이해하는 것은 개발의 핵심을 파악하는 것과 같습니다.

자바스크립트는 C 언어나 자바(Java)와 달리, 작성된 코드가 스스로 운영체제와 직접 소통하며 실행될 수 없습니다. 코드를 읽고, 해석하고, 컴퓨터가 이해할 수 있는 언어로 번역해 줄 '실행 환경(Runtime Environment)'이 반드시 필요합니다. 이 글에서는 자바스크립트가 어디서, 어떻게, 그리고 어떤 복잡하고 정교한 과정을 거쳐 실행되는지 그 모든 여정을 상세히 해부해 봅니다.

1. 자바스크립트의 두 가지 주요 실행 환경 (Runtime Environment)

자바스크립트 코드가 생명력을 얻기 위해서는 '런타임'이라는 무대가 필요합니다. 역사적으로 자바스크립트는 오직 웹 브라우저 안에서만 동작하도록 설계되었지만, 기술의 발전과 함께 그 한계를 벗어났습니다. 현재 자바스크립트의 실행 환경은 크게 두 가지로 나뉩니다.

1) 웹 브라우저 (Web Browser)

웹 브라우저는 자바스크립트의 고향이자 가장 대중적인 실행 환경입니다. 크롬(Chrome), 사파리(Safari), 엣지(Edge) 등 모든 현대적인 웹 브라우저 내부에는 자바스크립트 코드를 해석하는 '자바스크립트 엔진'이 내장되어 있습니다.

  • 실행 방식: 브라우저는 웹 페이지를 렌더링하기 위해 HTML 문서를 위에서부터 아래로 읽어 내려갑니다. 이 과정에서 <script> 태그를 만나면 HTML 파싱을 잠시 멈추고, 명시된 자바스크립트 파일을 네트워크를 통해 다운로드한 뒤 내장된 엔진을 통해 즉시 코드를 실행합니다.
  • 특징: 브라우저 환경은 웹 페이지의 요소들을 조작할 수 있는 DOM(Document Object Model) API와 브라우저 창 자체를 제어하는 BOM(Browser Object Model) API를 제공합니다. 이를 통해 버튼 클릭, 화면 스크롤, 애니메이션 효과 등의 시각적이고 인터랙티브한 작업이 가능해집니다.

2) Node.js (서버 및 PC 환경)

2009년, 자바스크립트를 브라우저라는 감옥에서 구출해 컴퓨터의 운영체제(OS) 위에서 직접 실행할 수 있게 만든 혁신적인 런타임이 바로 Node.js입니다. 크롬 브라우저의 핵심 엔진인 V8 엔진을 떼어내어 C++로 살을 붙여 만든 환경입니다.

  • 실행 방식: 컴퓨터에 Node.js를 설치한 후, 터미널이나 명령 프롬프트에서 node filename.js라는 명령어를 입력하면 Node.js 환경이 해당 파일을 읽어 들여 실행합니다.
  • 특징: Node.js는 브라우저와 달리 DOM이나 HTML을 조작하는 기능이 없습니다. 대신 컴퓨터의 파일 시스템에 접근하여 파일을 읽고 쓰거나, 네트워크 통신을 통해 웹 서버를 구축하고 데이터베이스와 연결하는 등 백엔드(Back-end) 시스템 개발에 필수적인 API를 제공합니다.
비교 항목 웹 브라우저 (Web Browser) Node.js
주요 목적 웹 페이지 UI/UX 제어, 클라이언트 사이드 스크립팅 서버 구축, 파일 시스템 제어, 백엔드 로직 처리
내장 엔진 V8 (크롬), WebKit (사파리), SpiderMonkey (파이어폭스) V8 (크롬의 V8 엔진 기반)
제공 API DOM, BOM, Web API (fetch, setTimeout 등) Node API (fs, http, path 등)
제한 사항 보안을 위해 로컬 파일 시스템 직접 접근 불가 화면 UI 조작(DOM 제어) 불가

2. 자바스크립트 엔진의 심장부: 텍스트가 기계어가 되기까지

실행 환경이 브라우저이든 Node.js이든, 코드를 실제로 읽고 처리하는 핵심 부품은 '자바스크립트 엔진(JavaScript Engine)'입니다. 구글의 V8 엔진이 가장 대표적이며, 이 엔진 내부에서는 개발자가 작성한 영어 텍스트(코드)를 컴퓨터가 이해하는 0과 1의 기계어로 변환하기 위해 여러 단계의 복잡한 공정을 거칩니다.

1단계: 파싱 (Parsing)과 어휘 분석 (Lexical Analysis)

엔진이 코드를 처음 마주하면 가장 먼저 하는 일은 코드를 의미 있는 최소 단위인 '토큰(Token)'으로 잘게 쪼개는 것입니다. 예를 들어 let x = 10; 이라는 코드는 let, x, =, 10, ; 이라는 5개의 토큰으로 분해됩니다.

이후 엔진의 파서(Parser)는 이 토큰들을 조합하여 언어의 문법 규칙에 맞는지 검사하고, 코드를 트리 구조로 형상화한 추상 구문 트리(AST, Abstract Syntax Tree)를 생성합니다. 컴퓨터는 단순한 텍스트 줄글보다 이 트리 구조를 통해 코드의 논리적 흐름을 훨씬 더 빠르고 정확하게 파악할 수 있습니다.

2단계: JIT(Just-In-Time) 컴파일러의 마법

과거의 자바스크립트는 코드를 한 줄씩 읽어가며 즉시 실행하는 인터프리터(Interpreter) 방식을 사용했기 때문에 실행 속도가 매우 느렸습니다. 이를 극복하기 위해 현대의 엔진들은 인터프리터의 빠른 시작 속도와 컴파일러의 빠른 실행 속도를 결합한 JIT 컴파일 방식을 채택했습니다. V8 엔진을 기준으로 이 과정은 두 가지 주요 컴포넌트를 통해 이루어집니다.

  • 이그니션(Ignition - 인터프리터): 파싱 단계에서 만들어진 AST를 읽어, 컴퓨터가 빠르게 해석할 수 있는 중간 형태의 코드인 '바이트코드(Bytecode)'로 변환하고 즉시 실행을 시작합니다. 이 단계에서 코드는 지연 없이 바로 동작하기 시작합니다.
  • 터보팬(TurboFan - 최적화 컴파일러): 이그니션이 코드를 실행하는 동안 엔진은 코드를 프로파일링(감시)합니다. 반복해서 자주 실행되는 코드(Hot Code)를 발견하면, 터보팬이 개입하여 해당 코드를 CPU가 직접 읽을 수 있는 '최적화된 기계어(Optimized Machine Code)'로 완전히 번역해 버립니다. 이후 동일한 코드가 호출되면 번역 과정을 생략하고 기계어를 바로 실행하여 성능을 극대화합니다. 만약 데이터 타입이 바뀌는 등 최적화 조건이 깨지면 다시 바이트코드로 돌아가는 역최적화(Deoptimization)를 수행하기도 합니다.

3단계: 실행 (Execution)과 메모리 관리

코드가 번역되면 실제 연산이 일어납니다. 이 실행 단계에서 엔진은 데이터를 저장하고 코드의 실행 순서를 추적하기 위해 두 가지 주요 메모리 구조를 사용합니다.

  • 메모리 힙 (Memory Heap): 변수, 객체, 배열, 함수 등 크기가 동적으로 변할 수 있는 데이터들이 무작위로 저장되는 광활한 메모리 창고입니다. 자바스크립트는 '가비지 컬렉터(Garbage Collector)'를 내장하고 있어, 더 이상 사용되지 않는 데이터를 자동으로 추적하여 메모리 공간을 주기적으로 청소하고 확보합니다.
  • 콜 스택 (Call Stack): 코드가 실행되는 순서를 기억하는 작업 장부입니다. 자바스크립트는 기본적으로 단 하나의 콜 스택만을 가지는 '싱글 스레드(Single Thread)' 언어입니다. 함수가 호출되면 콜 스택의 가장 위에 쌓이고(Push), 함수의 실행이 완료되면 스택에서 제거(Pop)됩니다. 위에서부터 아래로 순차적으로 한 번에 하나의 작업만 처리할 수 있는 구조입니다.

3. 싱글 스레드의 한계를 극복하는 방법: 이벤트 루프 (Event Loop)

자바스크립트가 콜 스택이 하나뿐인 싱글 스레드 언어라면, 한 번에 하나의 작업만 할 수 있다는 뜻입니다. 만약 네트워크에서 100MB 크기의 이미지를 다운로드하는 함수가 실행되어 콜 스택을 차지하고 있다면, 그 작업이 끝날 때까지 화면은 멈춰 있어야 합니다. 이를 '블로킹(Blocking)' 현상이라고 합니다.

하지만 우리가 웹 사이트를 사용할 때 무언가를 다운로드하면서도 동시에 화면을 스크롤하고 버튼을 클릭할 수 있습니다. 싱글 스레드인 자바스크립트가 어떻게 이런 동시성(Concurrency)을 가질 수 있을까요? 그 비밀은 자바스크립트 엔진 단독으로 일하는 것이 아니라, 브라우저나 Node.js 환경이 제공하는 웹 API(Web APIs)와 이벤트 루프(Event Loop) 시스템이 협력하기 때문입니다.

비동기 처리의 핵심 3요소

자바스크립트의 비동기 작업은 엔진 외부의 시스템과의 완벽한 릴레이 경주를 통해 이루어집니다.

  1. Web APIs (또는 Node APIs): 타이머 기능(setTimeout), 네트워크 요청(fetch), DOM 이벤트 등은 자바스크립트 엔진이 아닌 환경(브라우저 등)이 제공하는 기능입니다. 자바스크립트 코드가 타이머를 설정하면, 엔진은 콜 스택에서 그 작업을 즉시 브라우저의 Web API로 넘겨버리고 다음 코드를 실행합니다. 타이머 카운트는 콜 스택이 아닌 Web API의 백그라운드 환경에서 별도로 진행됩니다.
  2. 콜백 큐 (Callback Queue / Task Queue): Web API에서 백그라운드 작업(예: 3초 타이머 대기)이 끝나면, 함께 실행하기로 약속했던 함수(콜백 함수)를 이 대기열(Queue)로 보냅니다. 이곳은 콜 스택으로 들어가기 위해 순서를 기다리는 대기실과 같습니다.
  3. 이벤트 루프 (Event Loop): 이벤트 루프는 쉴 새 없이 콜 스택과 콜백 큐를 주시하는 관찰자입니다. 규칙은 아주 단순합니다. "콜 스택이 완전히 비어 있을 때만, 콜백 큐에 대기 중인 함수를 하나씩 콜 스택으로 밀어 올려 실행시킨다." 이 단순한 규칙 덕분에 자바스크립트는 무거운 작업을 백그라운드로 미루고, 화면이 멈추는 현상 없이 끊임없이 사용자와 상호작용할 수 있습니다.

※ 마이크로태스크 큐 (Microtask Queue)

콜백 큐 내부에는 사실 VIP 대기실 격인 '마이크로태스크 큐'가 따로 존재합니다. Promise, async/await와 같이 우선순위가 매우 높은 최신 비동기 작업들은 일반적인 타이머 콜백(setTimeout)보다 먼저 처리되어야 합니다. 이벤트 루프는 콜백 큐를 확인하기 전에 항상 마이크로태스크 큐를 먼저 확인하여, 그곳에 대기 중인 작업이 하나도 남지 않을 때까지 전부 처리한 후에야 일반 큐의 작업을 처리합니다.

4. 코드의 환경을 구성하는 실행 컨텍스트 (Execution Context)

자바스크립트 코드가 평가되고 실행되는 '환경'을 추상적으로 나타낸 개념이 바로 실행 컨텍스트입니다. 엔진이 코드를 실행할 때 머릿속에 그리는 지도로 생각할 수 있습니다. 모든 코드는 반드시 특정한 실행 컨텍스트 안에서 실행됩니다.

  • 글로벌 실행 컨텍스트 (Global Execution Context): 파일이 처음 실행될 때 단 하나만 생성되는 기본 컨텍스트입니다. 전역 객체(브라우저의 경우 window, Node.js의 경우 global)를 생성하고 this를 바인딩합니다. 파일이 닫히거나 브라우저 탭이 종료될 때까지 유지됩니다.
  • 함수 실행 컨텍스트 (Function Execution Context): 함수가 호출될 때마다 해당 함수만을 위한 새로운 컨텍스트가 생성됩니다. 함수가 호출될 때마다 콜 스택에 새로운 컨텍스트가 쌓이고, 실행이 끝나면 소멸합니다.

호이스팅(Hoisting)이 발생하는 이유

실행 컨텍스트는 생성될 때 코드를 무작정 실행하는 것이 아니라, '생성 단계(Creation Phase)'와 '실행 단계(Execution Phase)' 두 번에 걸쳐 코드를 훑어봅니다.

생성 단계에서 엔진은 코드 전체를 스캔하며 변수 선언문(var, let, const)과 함수 선언문(function() {})을 찾아내 메모리 공간을 미리 확보합니다. 코드가 실행되기도 전에 메모리 윗부분으로 끌어올려진 것처럼 식별자가 준비되는 현상을 '호이스팅'이라고 부릅니다. 이 과정이 끝나야 비로소 코드를 한 줄씩 읽어가며 변수에 실제 값을 할당하고 함수를 연산하는 실행 단계로 넘어갑니다.

5. 실제 코드의 실행 흐름 추적해 보기

지금까지 설명한 이론들이 실제 코드에서 어떻게 동작하는지 하나의 예시를 통해 종합해 보겠습니다.

JavaScript
 
console.log("1. 시작");

setTimeout(() => {
  console.log("2. 타이머 끝");
}, 0);

Promise.resolve().then(() => {
  console.log("3. 프라미스 끝");
});

console.log("4. 끝");

위 코드의 출력 순서는 어떻게 될까요? 코드를 위에서 아래로 읽는다면 1 -> 2 -> 3 -> 4일 것 같지만, 자바스크립트 엔진과 이벤트 루프의 동작 방식을 적용하면 결과는 완전히 다릅니다.

  1. 동기 코드 실행: 코드가 파싱되어 글로벌 실행 컨텍스트가 생성되고 콜 스택에 진입합니다. 첫 번째 줄 console.log("1. 시작")이 콜 스택에 들어가 바로 실행되고 화면에 출력된 후 스택에서 빠집니다.
  2. 비동기 함수 (Web API로 위임): setTimeout이 콜 스택에 들어옵니다. 엔진은 이것이 비동기 타이머 작업임을 인식하고, 실행을 Web API 환경으로 넘긴 뒤 콜 스택에서 제거합니다. 대기 시간이 0초이므로 타이머는 즉시 종료되고, 안에 있던 콜백 함수 console.log("2. 타이머 끝")은 일반 콜백 큐(매크로태스크 큐)로 이동하여 대기합니다.
  3. 마이크로태스크 큐 진입: Promise 코드가 콜 스택에 들어옵니다. 프라미스의 .then() 절에 있는 콜백 함수는 우선순위가 높은 VIP 작업이므로 대기실 중에서도 마이크로태스크 큐로 이동하여 대기합니다.
  4. 동기 코드 실행: 마지막 줄 console.log("4. 끝")이 콜 스택에 들어가 즉시 실행되어 화면에 출력되고 스택에서 빠집니다.
  5. 이벤트 루프의 개입: 이제 콜 스택이 완전히 비었습니다. 이벤트 루프가 작동을 시작합니다.
  6. 마이크로태스크 큐 우선 처리: 이벤트 루프는 마이크로태스크 큐를 먼저 들여다봅니다. 대기 중이던 프라미스 콜백을 콜 스택으로 끌어올려 실행합니다. 화면에 "3. 프라미스 끝"이 출력됩니다.
  7. 일반 큐 처리: 마이크로태스크 큐가 텅 빈 것을 확인한 후, 이벤트 루프는 비로소 일반 콜백 큐에 있던 타이머 콜백을 콜 스택으로 끌어올립니다. 마지막으로 "2. 타이머 끝"이 출력됩니다.

최종 출력 결과: 1. 시작 -> 4. 끝 -> 3. 프라미스 끝 -> 2. 타이머 끝

이 짧은 네 줄의 코드 안에도 자바스크립트의 실행 환경, 콜 스택의 동기적 처리, Web API의 백그라운드 작업, 그리고 마이크로태스크 큐를 우선시하는 이벤트 루프의 철저한 규칙이 모두 숨어 있습니다.

결론: 자바스크립트는 끊임없이 진화하는 오케스트라

자바스크립트 파일 하나가 실행되는 과정은 결코 단순하지 않습니다. 텍스트로 된 코드가 AST로 파싱되고, JIT 컴파일러를 통해 순식간에 기계어로 번역되어 메모리 힙과 콜 스택을 넘나듭니다. 동시에 단일 스레드의 한계를 깨기 위해 브라우저의 Web API와 이벤트 루프가 환상적인 호흡을 맞추며 복잡한 비동기 작업들을 매끄럽게 처리해 냅니다.

단순히 브라우저의 알림창을 띄우는 용도로 시작했던 작은 스크립트 언어가, 오늘날 수십억 명의 사용자를 처리하는 글로벌 서버 시스템(Node.js)과 데스크톱 애플리케이션(Electron), 모바일 앱(React Native)까지 구동하는 세계에서 가장 강력하고 널리 쓰이는 생태계로 자리 잡을 수 있었던 이유는 바로 이토록 치밀하고 고도로 최적화된 실행 과정 덕분입니다. 코드를 작성할 때 이 거대한 엔진의 톱니바퀴들이 어떻게 맞물려 돌아가는지를 머릿속에 그릴 수 있다면, 더 빠르고 안정적이며 효율적인 소프트웨어를 설계하는 데 큰 도움이 될 것입니다.