자바스크립트는 실제로 어떻게 동작하는가

작성일:2026.10.25|수정일:2026.10.25|조회수:1

브라우저 이벤트 루프와 Node.js 이벤트 루프를 같은 코드로 나누어 쓰고 나니, 블로그에 흩어져 있던 다른 글들이 눈에 들어왔다. 실행 컨텍스트를 정리하다가 렉시컬 환경으로 내려간 글, 메서드를 변수에 담았다가 this를 잃어버린 글, class의 겉모양을 벗겨 prototype을 살펴본 글, resolve()를 호출하고도 아직 pending인 Promise를 붙잡은 글이 이미 있었다.

각 글을 쓸 때는 서로 다른 문제가 출발점이었다. 그런데 한곳에 놓고 보니 질문은 꽤 비슷했다. 문법을 어떻게 쓰는지가 아니라, 자바스크립트가 왜 그 코드를 그런 방식으로 해석하고 실행하는지 알고 싶어서 쓴 글들이었다.

이 시리즈는 그 질문을 따라간다.

문법을 외워도 설명되지 않는 순간

자바스크립트는 처음 배우기에는 꽤 관대한 언어다. 함수를 변수에 넣을 수 있고, 객체에 메서드를 붙일 수 있으며, async와 await를 사용하면 비동기 코드도 동기 코드처럼 읽힌다. 브라우저와 Node.js에서 같은 문법을 사용할 수도 있다. 대충 써도 제법 많은 것이 움직인다.

문제는 움직이던 코드가 예상과 다르게 행동할 때 시작된다. 메서드를 다른 함수에 넘겼더니 this가 달라지고, resolve()에 Promise를 전달했더니 곧바로 fulfilled가 되지 않으며, 똑같이 setTimeout()과 Promise를 사용한 코드가 실행 환경에 따라 다른 순서를 만들기도 한다. 이때 API 이름과 우선순위만 외우면 예제 하나는 풀 수 있어도 조금 다른 코드 앞에서 다시 흔들린다.

내가 이 시리즈에서 찾으려는 것은 새로운 암기표가 아니다. 지금 실행 중인 코드가 어떤 상태에 놓여 있는지, 객체의 프로퍼티를 어떤 규칙으로 찾는지, 비동기 작업을 다시 자바스크립트로 돌려보내는 책임이 어디에 있는지를 구분하는 기준에 가깝다.

코드가 실행되는 자리를 먼저 본다

첫 번째 축은 실행 컨텍스트다. 자바스크립트 엔진은 소스 코드를 읽고 결과만 내놓는 검은 상자가 아니다. 현재 어떤 코드가 실행 중인지, 식별자를 어느 환경에서 찾을지, this를 무엇으로 볼지 같은 상태를 관리한다.

TXT
소스 코드의 규칙→ 실행 컨텍스트→ 렉시컬 환경과 식별자 탐색→ 호출 방식에 따른 this 바인딩

렉시컬 스코프와 렉시컬 환경을 구분하는 이유도 여기에 있다. 전자는 소스 코드가 작성된 위치에 따라 정해지는 규칙이고, 후자는 코드를 실행하면서 실제 바인딩을 보관하는 구조다. 이름이 비슷하다는 이유로 둘을 하나로 뭉치면 클로저와 스코프 체인을 설명할 때 말이 조금씩 어긋난다.

this 역시 함수가 선언된 위치만 보고 정할 수 없다. 일반 함수와 화살표 함수의 규칙이 다르고, 메서드를 어떤 형태로 호출했는지에 따라 바인딩도 달라진다. 실행 결과만 외우기보다 호출 지점과 실행 컨텍스트를 함께 봐야 하는 이유다.

객체의 겉모양 아래에는 다른 규칙이 있다

두 번째 축은 객체 모델이다. class를 사용하는 코드는 다른 객체 지향 언어의 클래스와 비슷하게 보이지만, 자바스크립트는 prototype을 통해 동작을 공유한다. 인스턴스 필드와 프로토타입 메서드는 같은 중괄호 안에 적혀 있어도 저장되는 위치와 비용이 다르다.

프로퍼티도 단순한 키와 값의 쌍으로 끝나지 않는다. descriptor에는 값뿐 아니라 쓰기 가능 여부, 열거 가능 여부, 재정의 가능 여부가 들어간다. getter를 읽는 순간 함수가 실행되는 것도, Object.defineProperty()로 만든 값이 평범한 할당과 다르게 보이는 것도 이 메타데이터 때문이다.

TXT
객체├─ own property├─ [[Prototype]]을 통한 탐색└─ descriptor가 정하는 프로퍼티의 동작

문법은 이 구조를 꽤 잘 감춰준다. 평소에는 감춰져 있어도 괜찮다. 하지만 클래스 리팩터링 뒤에 예상하지 못한 getter가 실행되거나, 메서드 공유 방식과 인스턴스별 함수 생성 비용을 비교하는 순간에는 그 아래의 규칙을 꺼내 봐야 한다.

Promise와 이벤트 루프는 같은 문제가 아니다

세 번째 축은 비동기 실행이다. 여기서는 먼저 Promise 자체의 의미론과 Promise 반응을 언제 실행할지 결정하는 호스트의 스케줄링을 분리할 필요가 있다.

Promise Resolution Procedure는 resolve()가 단순히 값을 상자에 넣는 함수가 아니라는 사실을 보여준다. 다른 Promise나 thenable을 전달하면 그 상태를 따라가고, resolved 상태이면서 아직 pending일 수도 있다. 이것은 Promise가 어떻게 정착하는지를 설명하는 ECMAScript의 규칙이다.

반면 이벤트 루프는 브라우저나 Node.js 같은 호스트 환경의 책임이다. 브라우저는 task queue, microtask checkpoint, 렌더링 갱신을 다룬다. Node.js는 libuv의 phase와 next tick queue, V8 microtask queue를 함께 조율한다. 두 환경에서 같은 로그가 나온다고 같은 이벤트 루프를 사용하는 것은 아니며, 같은 문법을 썼다고 실행 순서까지 같으리라는 보장도 없다.

TXT
ECMAScript└─ Promise와 microtask가 될 작업의 의미브라우저└─ task, microtask checkpoint, 렌더링 갱신Node.js└─ libuv phase, next tick queue, V8 microtask queue

이 경계를 구분하고 나면 Promise는 setTimeout보다 빠르다 같은 암기에서 조금 벗어날 수 있다. 현재 코드는 어느 호스트에서 실행되는지, 어느 콜백 안에서 다음 작업을 등록했는지, 지금 실행이 끝난 뒤 어떤 queue와 phase를 확인하는지를 차례로 보면 된다.

시리즈 구성

이 시리즈는 넓은 지도에서 시작해 한 가지 경계를 좁혀 보는 순서로 구성한다.

  1. 실행 컨텍스트와 브라우저 이벤트 루프는 실행 컨텍스트, 환경 레코드, 스코프, this, 비동기 실행의 전체 지도를 그린다.
  2. 렉시컬 스코프 v 렉시컬 환경은 소스 코드의 규칙과 실행 중 만들어지는 바인딩 구조를 구분한다.
  3. this는 호출 방식에 따라 바인딩이 달라지는 과정을 실제 오류에서 출발해 추적한다.
  4. 생성자 함수와 prototype으로 이해하는 JS class는 class 문법 아래의 생성자 함수와 프로토타입 체인을 살펴본다.
  5. descriptor는 프로퍼티가 가진 값과 메타데이터를 분리하고, 읽기와 쓰기의 동작이 어디에서 결정되는지 확인한다.
  6. Promise Resolution Procedure는 resolve, fulfill, thenable 흡수와 Promise 체인의 평탄화를 따라간다.
  7. 같은 JavaScript인데 환경에 따라 실행 순서가 달라지는 이유는 ECMAScript와 호스트 환경의 책임을 나누고 브라우저와 Node.js를 비교한다.
  8. 브라우저 이벤트 루프에서 task 하나가 끝난 뒤 일어나는 일은 task 하나를 선택한 뒤 microtask checkpoint와 렌더링 갱신까지 이어지는 흐름을 추적한다.
  9. Node.js 이벤트 루프에서 콜백 하나가 끝난 뒤 일어나는 일은 I/O 콜백이 끝난 뒤 next tick, microtask, check와 timers가 만나는 순서를 추적한다.

여기에 모든 JavaScript 글을 넣지는 않았다. 브라우저 API 사용법, 함수형 프로그래밍, 메타프로그래밍을 이용한 DSL 설계도 자바스크립트를 다루지만 이 시리즈의 질문과는 조금 다르다. 카테고리가 주제를 모으는 서랍이라면, 시리즈는 하나의 질문을 따라가는 읽기 순서여야 한다고 생각한다.

동작을 안다는 것

자바스크립트의 모든 명세를 외우는 일은 가능하지도 않고, 아마 그다지 즐겁지도 않을 것이다. 적어도 나는 자신이 없다. 대신 예상과 다른 결과를 만났을 때 어느 층을 확인해야 하는지는 배울 수 있다.

식별자를 찾는 문제라면 렉시컬 환경을 보고, 메서드 호출이 달라졌다면 this 바인딩을 보고, 프로퍼티의 읽기와 쓰기가 이상하다면 descriptor와 prototype을 본다. Promise의 상태와 실행 시점이 섞였다면 ECMAScript의 resolution 규칙과 호스트 이벤트 루프를 분리한다.

결국 자바스크립트가 어떻게 동작하는지 이해한다는 것은 모든 결과를 미리 외우는 일이 아니다. 언어가 정한 규칙과 실행 환경이 맡은 책임 사이에서, 지금 보고 있는 이상한 결과가 어느 경계에서 만들어졌는지 찾아가는 일에 가깝다.

댓글

댓글을 불러오는 중...