최근 개발자 도구 의 성능 탭을 분석하다가 Flame chart 를 보다가
도대체 '자바 스크립트 실행' 이라는 건 내부적으로 어떻게 돌아가는 걸까? 라는 생각이 들었다.
자바 스크립트를 활용하다 보면, 코드가 잘 돌아가는지 " 표면적인 결과 " 만 보게 되는 경우가 많은 것 같다.
하지만 성능을 분석하거나 예상치 못한 버그를 디버깅 하려고 하면 그 밑바닥에서 엔진이 어떻게 실행하는지 인지하고 있어야 디버깅 시간이 효율적으로 단축되지 않을까 하는 생각이 들었다.
바로 그 핵심이 실행 컨텍스트 와 스코프 인 것 같다.
Flame chart에서 노란색 Scripting 구간은 JS가
" 현재 어떤 실행 컨텍스트 위에서 동작 중인지" 를 시각화한 것이다.
즉, 실행 컨텍스트의 원리를 이해해야 Flame chart를 읽는 힘이 생기지 않을까!?
또는 예상치 못한 버그의 근본 원인이기도 하다.
변수 호이스팅, 클로저, this 바인딩 꼬임, 메모리 누수 등등 실행 컨텍스트 와 스코피 체인의 작동 방식과 연결되어 있기 때문에 JS가 어떤 환경 에서 실행중인지 모르면 답이 없다..
그래서 이번 글에서는 Performance 탭을 보며 실행 컨텍스트와 스코프의 개념을 정리해보려고 한다.
우선 실행 컨텍스트(Execution Context) 란 뭘까?
자바 스크립트 코드가 실행할 떄 필요한 환경 정보들을 모아놓은 객체.
쉽게 말해,
자바 스크립트가 어떤 코드를 "어떤 환경에서, 어떤 규칙으로" 실행해야 하는지를 담고 있는 하나의 객체 인것이다.
구성 요소로는 Variable Env, Lexical Env, This Binding 가 있다. ( 이는 뒤에서 자세히 공부해보자 )
그리고 뒤에서 계속 언급될 예정인 스코프 ( Scope ) 란 뭘까?
우선 간단하게 설명하자면 스코프는 변수들이 저장된 폴더 구조와 같다.
let, const로 선언된 변수는 자신이 선언된 블록 폴더 안에 들어 있고,
함수 안에서 선언된 변수는 그 함수 폴더 안에 들어 있다.
자바스크립트 엔진이 변수를 찾을 때는,
현재 폴더부터 하나씩 상위 폴더를 뒤져 올라간다.
이 폴더 트리 구조가 바로 스코프 체인이다.
자바 스크립트 엔진은 소스 코드를 한번에 처리하지 않고
"평가" -> "실행" 이라는 두 단계를 거쳐 순차적으로 코드를 해석한다.
평가 단계에서 엔진은
" 이 코드가 어떤 변수들을 보고, 어떤 상위 스코프를 따라가며, this를 무엇으로 가리킬지 " 를 정리해둔 실행 컨텍스트를 만들어 둔다.
엔진이 만드는 실행 컨텍스트의 종류는 3가지이다.
| 전역 컨텍스트(Global Context) | 자바스크립트 코드가 처음 실행될 때 생성 | 전역 스코프에 대한 환경 생성. window(브라우저) 또는 global(Node.js)을 this로 바인딩 |
| 함수 컨텍스트(Function Context) | 함수가 호출될 때마다 생성 | 호출된 함수마다 고유한 실행 컨텍스트가 생성되어 독립적인 환경을 가짐. |
| Eval 컨텍스트(Eval Context) | eval() 실행 시 생성 | eval에 전달된 코드가 별도의 실행 환경에서 평가될 때 생성됨. |
이 중에서 자동으로 한번 생성되는 전역 컨텍스트와 eval() 컨텍스트를 제외하면 실행 컨텍스트는 함수가 실행될때 생성된다.
그리고 코드 안에서 var, let, const, function 같은 “선언문”만 미리 훑어서, 실행 컨텍스트가 관리하는 스코프 에 등록한다.
실행컨텍스트의 동작과정을 잠깐 살펴보면 다음과 같다.
실행 컨텍스트는 Call Stack(콜 스택 = 후입 선출 )이라는 자료구조에서 관리된다.

동작 과정에서 확인할 수 있듯이
자바 스크립트 코드는 실행될 때 가장 먼저 전역 실행 컨텍스트가 생성되어 콜 스택에 쌓인다.
이후 코드가 실행되면서 함수가 호출되는 순간, 해당 함수의 실행 컨텍스트가 새로 생성되어 콜 스택의 맨 위에 추가(push) 된다.
함수의 코드가 모두 실행을 마치면, 해당 실행 컨텍스트는 스택에서 제거(pop) 되어 제어권이 바로 아래의 컨텍스트로 돌아간다.
실행 컨텍스트가 스택에서 제거되는 시점인 " 실행 " 단계를 가기 전에 실행 컨텍스트의 내부 구조 에 대해서 공부해보자!
실행 컨텍스트 내부는 Variable Environment, Lexical Environment, this binding 로 이뤄져있다.
Variable Environment
실행 컨텍스트가 처음 만들어질때, " 해당 스코프 안의 선언 정보 "를 담아두는 초기 스냅샷이다.
VariableEnvironment는 LexicalEnvironment와 구조는 같지만, 실행 컨텍스트가 최초 생성될 때의 상태를 그대로 유지하는 “초기 사본”이다.
즉 코드 실행 중에는 업데이트되지 않는다.
이 안에는 현재 컨텍스트 내의 식별자 정보(변수, 함수 선언 등) 와 외부 렉시컬 환경에 대한 참조(outer reference) 가 들어 있다.
실행 컨텍스트가 만들어질 때, 엔진은 먼저 VariableEnvironment를 초기화하고
→ 그 내용을 그대로 복사하여 LexicalEnvironment를 생성한다.
이후 실제 코드 실행에서는 LexicalEnvironment를 사용해 변수 참조, 값 변경 등을 처리하게 되는 것이다.
Lexical Environment
실행 중에 실시간으로 식별자와 스코프 정보를 관리하는 구조이다.
LexicalEnvironment는 변수와 함수의 식별자(identifier), 그에 바인딩된 값(value), 그리고 상위 스코프(outer) 정보를 저장하는 자료구조이다.
위에서 정리한 콜 스택이 코드 실행 순서를 관리한다면 LexicalEnvironment는 식별자와 스코프를 관리한다.
실행 컨텍스트가 처음 생성될 때는 VariableEnvironment의 내용을 그대로 복사하여 시작하지만,
이후 변수 값 변경이나 함수 호출 등의 변경 사항은 모두 LexicalEnvironment에 실시간 반영된다.
Lexical Environment의 구성
(1) 환경 레코드(Environment Record)
“현재 스코프 안의 변수/함수 식별자 정보를 담고 있는 저장소”
코드 실행 전에 엔진은 이미 해당 스코프에 어떤 식별자(변수, 함수 등)가 있는지 모두 알고 있다.
즉, 실행 전에 이 정보를 전부 수집해서 환경 레코드에 등록해둔다.
→ 이 과정에서 호이스팅이 발생한다.
전역 컨텍스트의 경우엔 따로 “변수 객체”를 생성하지 않고,
전역 객체(window, global 등) 를 환경 레코드로 활용한다.
예:
- 브라우저 환경 → window 객체
- Node.js → global 객체
(2) 외부 렉시컬 환경 참조 (Outer Lexical Environment Reference)
"현재 스코프의 바깥을 가르키는 링크"
외부 렉시컬 환경 참조는 상위 코드의 LexicalEnvironment 를 가리킨다.
즉, 현재 함수가 선언될 당시의 상위 스코프를 참조한다.
덕분에 스코프 체인(Scope Chain) 이 형성된다.
변수나 함수를 찾을 때 현재 환경에 없으면 → 상위 환경 → 다시 상위로 올라가며 탐색할 수 있다.
만약 상위 스코프에서도 해당 식별자를 찾을 수 없다면 참조 에러 를 발생시킨다.
This Binding
this 키워드가 가리켜야 할 대상 객체를 저장하는 공간이다.
this는 현재 실행 컨텍스트가 “어떤 객체를 기반으로 실행되고 있는가” 를 나타낸다.
this가 가리키는 대상은 함수의 호출 방식에 따라 달라진다.
실행 컨텍스트의 내부 구조를 살펴보았으니 이제 " 실행 " 단계에 대해서 공부해보자!
예제로 활용할 코드는 아래와 같다.
var x = 'xxx';
function foo () {
var y = 'yyy';
function bar () {
var z = 'zzz';
console.log(x + y + z);
}
bar();
}
foo();
위에 언급했던 실행 컨텍스트 동작 순서가 바로 위 예제 코드에 해당한다.

우선 컨텍스트가 생성되는 프로세스에 대해서 알아보자
생성 단계에서는 스코프와 환경을 만들고 식별자를 메모리에 등록하게 된다.
초기값은 var -> undefined, 함수 선언 -> 함수객체로 등록된다.
(1) 코드 로드 직후
엔진이 자바스크립트 코드를 만나면 가장 먼저 전역 실행 컨텍스트(Global EC)를 생성하고
이를 콜 스택에 push하게 된다.
| 코드 로드 직후 | Global EC | 전역 렉시컬 환경 (1) 환경 레코드 → { x: undefined, foo: <function> } (2) 외부 참조 스코프 → null |
전역 컨텍스트 생성과 동시에 변수와 함수 선언이 메모리에 등록 된다.
var x는 undefined, function foo는 함수 객체로 초기화 된다.
이 후, 전역 코드가 실행되며 x = 'xxx'가 대입된다.
그리고 foo() 호출문을 만나면 새로운 함수 실행 컨텍스트가 만들어진다.
(2) foo() 호출
foo()를 호출하는 순간, 새로운 실행 컨텍스트(foo EC)가 생성되고 콜 스택에 push 된다.
| foo() 호출 시 | foo EC Global EC |
foo 렉시컬 환경 (1) 환경 레코드 → { y: undefined, bar: <function> } (2) 외부 참조 스코프 → 전역 렉시컬 환경 |
foo의 환경 레코드에 y와 bar가 등록된다.
Outer 참조는 전역 환경을 가리킨다.
y = 'yyy' 실행 후, bar() 호출문을 만나면 또 다른 함수 컨텍스트가 생성된다.
(3) bar() 호출
bar() 호출로 인해 세 번째 실행 컨텍스트가 스택의 맨 위에 쌓인다.
| bar() 호출 시 | bar EC foo EC Global EC |
bar 렉시컬 환경 (1) 환경 레코드 → { z: undefined } (2) 외부 참조 스코프 → foo 렉시컬 환경 |
z = 'zzz' 대입이 실행되고,
console.log(x + y + z) 실행 시, 렉시컬 환경 체인(Outer 참조)을 따라 변수 탐색이 일어난다.
이 시점의 탐색 과정에서 JS 엔진은 스코프 체인을 타고 올라가면서 식별자를 찾는다.
1. 현재(bar) 환경 레코드에서 z 탐색 -> zzz 발견
2. Outer(=foo) 환경 레코드에서 y 탐색 -> yyy 발견
3. Outer(=Global) 환경 레코드에서 x 탐색 -> xxx 발견
따라서 최종적으로 스코프 탐색이 끝나고 계산이 완료된 값은 아래와 같다.
→ "xxx" + "yyy" + "zzz"
→ 최종 문자열 "xxxyyyzzz"
이렇게 bar 함수 실행이 종료되면 bar EC 가 콜스택에서 제거 (pop ) 되고
차례로 foo 함수 -> 전역 코드까지 모두 실행되면 Global GC까지 스택에서 제거되게 된다.
이렇게 자바스립트의 실행 과정은 콜 스택을 통해 함수 실행 순서를 관리하고 렉시컬 환경을 통해 변수 탐색 경로를 관리한다 라고 정리할 수 있을 것 같다.
이렇게 자바스크립트의 엔진은 어떤 흐름으로 코드를 실행시키는가 를 이해하기 위해 실행 컨텍스트와 스코프에 대한 개념을 정리해보았다. 다음 게시글에서는 "클로저(Closure)" 에 대해 다뤄보겠다!
참고
[JS] 📚 자바스크립트 실행 컨텍스트 원리
실행 컨텍스트 실행 컨텍스트(Execution Context)는 scope, hoisting, this, function, closure 등의 동작원리를 담고 있는 자바스크립트의 핵심원리이다. 실행 컨텍스트를 바로 이해하지 못하면 코드 독해가
inpa.tistory.com
https://www.datoybi.com/execution-context/
실행 컨텍스트 (Execution Context) 톺아보기
실행 컨텍스트에 대해 스터디한 내용을 기록합니다.
www.datoybi.com
https://product.kyobobook.co.kr/detail/S000001766397
코어 자바스크립트 | 정재남 - 교보문고
코어 자바스크립트 | 자바스크립트의 근간을 이루는 핵심 이론들을 정확하게 이해하는 것을 목표로 합니다!최근 웹 개발 진영은 빠르게 발전하고 있으며, 그 중심에는 자바스크립트가 있다고
product.kyobobook.co.kr
'공부 > Javascript' 카테고리의 다른 글
| [JS] 얕은 복사와 깊은 복사 (1) | 2025.02.04 |
|---|