자바스크립트 코드를 작성하면 어떤 일이 벌어질까요? 우리가 단순히 console.log(“Hello, world!”)라고 입력하면, 브라우저는 어떻게 이를 해석하고 실행할까요? 사실 우리가 보는 코드와 컴퓨터가 이해하는 코드는 전혀 다릅니다. 그렇다면 컴퓨터는 자바스크립트 코드를 어떻게 실행하는 것인지 살펴봅시다. 컴퓨터는 우리가 사용하는 자연어를 그대로 이해하지 못합니다. 따라서 우리가 작성한 코드를 해석하고, 실행 가능한 형태로 변환한 후, 실행 결과를 출력하는 과정을 거칩니다. 이 과정에서 자바스크립트 엔진이 중요한 역할을 하며, 코드가 파싱(Parsing), 컴파일(Compilation), 실행(Execution) 과정을 거쳐 실행됩니다. 자바스크립트의 실행 과정을 이해하면 코드 최적화, 호이스팅, 스코프 등의 개념을 명확하게 파악할 수 있습니다. 특히 함수 호출이 어떤 방식으로 처리되는지, 실행 컨텍스트가 어떻게 관리되는지를 알면, 예상치 못한 버그를 줄이고 보다 효율적인 코드를 작성할 수 있습니다. 이번 글에서는 자바스크립트 코드가 실행되는 전체 과정과 실행 컨텍스트의 내부 구조를 차근차근 살펴보겠습니다. 자바스크립트 코드 실행 과정자바스크립트 엔진은 코드를 해석하고 실행하기 위해 여러 단계를 거칩니다. 크게 파싱, 컴파일, 실행 세 단계로 나눌 수 있습니다. 1) 파싱(Parsing)코드를 실행하기 전에, 자바스크립트 엔진은 먼저 파싱 과정을 거칩니다. 이 과정에서 엔진은 우리가 작성한 코드를 문법적으로 분석하고, 실행할 준비를 합니다. <출처: Napkin AI, 작가 편집> 먼저 토큰화(Tokenization) 단계에서 코드 문자열을 의미 있는 최소 단위(토큰)로 분해합니다. 예를 들어, let x = 10; 이라는 코드는 let, x, =, 10, ;와 같은 개별 토큰으로 나누어집니다. 이후 추상 구문 트리(AST, Abstract Syntax Tree) 생성 단계에서, 파싱된 토큰을 기반으로 코드의 구조를 트리 형태로 변환합니다. AST는 말 그대로 소스 코드의 구조를 트리 형태로 표현한 자료구조로, 코드의 구성 요소를 분석하고 최적화하며, 문법 오류를 더욱 편리하게 검출할 수 있도록 도와줍니다. 이 트리는 이후 단계에서 컴파일러가 바이트 코드를 생성하거나 최적화 과정에서 코드 조작을 수행하는 데 활용됩니다. 이 트리는 프로그램의 구조를 분석하는 데 사용되며, 이후 단계에서 컴파일러가 바이트 코드를 생성하거나 최적화 과정에서 코드 조작을 수행하는 데 활용됩니다. 즉, AST는 컴퓨터가 코드를 보다 쉽게 분석하고 처리할 수 있도록 변환하는 중요한 역할을 합니다. 2) 컴파일(Compilation)AST가 생성된 후, 자바스크립트 엔진은 이를 바이트 코드(Bytecode)로 변환하는 컴파일(Compilation) 과정을 거칩니다. ‘바이트 코드로 컴파일한다.’라는 말이 어렵게 느껴진다면 전부 다 잊고, 컴파일 과정을 나타내는 그림을 먼저 살펴봅시다. <출처: Napkin AI, 작가 편집> 일반적으로 우리가 코드를 입력하는 과정은 사람이 자바스크립트와 같은 프로그래밍 언어를 컴퓨터에 전달하는 것이라고 볼 수 있습니다. 우리가 작성한 코드를 컴퓨터가 이해할 수 있도록 변환해 주어야 하는데요. 여기서 컴퓨터가 이해할 수 있는 중간 형태의 코드가 바로 바이트 코드(Bytecode)입니다. 그리고 이 변환 과정을 컴파일이라 부릅니다. 바이트 코드는 고수준의 언어(자바스크립트)와 기계어(컴퓨터가 이해할 수 있는 코드)의 중간 단계에 위치하는 코드로, 실행 속도를 높이면서도 유연성을 유지할 수 있도록 합니다. 최신 자바스크립트 엔진에서는 JIT(Just-In-Time) 컴파일을 활용하여 실행 중에 바이트 코드가 네이티브 코드(기계어)로 변환됩니다. 이를 통해 반복적으로 실행되는 코드를 최적화하고, 실행 속도를 극대화할 수 있습니다. 3) 실행(Execution)컴파일이 완료된 바이트 코드는 실행 단계에서 처리됩니다. 실행이 시작되면 실행 컨텍스트(Execution Context)가 생성되며, 변수 할당, 함수 호출, 연산 수행 등이 이루어집니다. 자바스크립트는 콜 스택(Call Stack)을 활용하여 실행 컨텍스트를 관리합니다. 함수가 호출되면 새로운 실행 컨텍스트가 스택에 추가되고, 실행이 완료되면 제거되는 방식입니다. 이렇게 하면 현재 실행 중인 함수와 실행이 끝난 함수를 구분할 수 있습니다. function sayHello() { console.log("Hello, world!"); } sayHello(); 위 코드가 실행되면 sayHello 함수가 호출되고, 실행 컨텍스트가 콜 스택에 추가됩니다. 함수 실행이 끝나면 해당 컨텍스트가 제거되고, 프로그램이 종료됩니다. 실행 단계에서는 변수와 함수의 스코프(Scope)가 결정되며, 클로저(Closure)와 같은 개념이 적용됩니다. 또한 실행 컨텍스트 내부에서 호이스팅(Hoisting)이 발생하여, 선언된 함수나 변수가 코드 실행 전에 메모리에 미리 할당됩니다. 실행 컨텍스트란?이제 코드가 실행되는 환경을 보다 깊이 이해하기 위해 ‘실행 컨텍스트(Execution Context)’에 대해 살펴보겠습니다. 실행 컨텍스트는 코드가 실행될 때 생성되며, 자바스크립트 엔진이 변수, 함수, 객체 등의 실행 정보를 저장하고 제어하는 역할을 합니다. 실행 컨텍스트를 이해하면 함수 호출의 흐름을 명확하게 알 수 있으며, 호이스팅, 스코프 체인, 클로저와 같은 개념도 자연스럽게 파악할 수 있습니다. 이러한 개념들은 자바스크립트의 동작 방식에서 핵심적인 부분이므로, 실행 컨텍스트를 잘 이해하는 것이 매우 중요합니다. 1) 콜스택자바스크립트 엔진은 콜 스택(Call Stack)을 이용해 실행 컨텍스트를 관리합니다. 콜 스택은 함수 실행 순서를 추적하는 LIFO(Last In, First Out) 구조로 동작합니다. 즉, 가장 마지막에 호출된 함수가 먼저 실행되며, 실행이 완료되면 콜 스택에서 제거됩니다. 예를 들어, 다음과 같은 코드가 있다고 가정해 볼게요. function first() { second(); console.log("함수1"); } function second() { third(); console.log("함수2"); } function third() { console.log("함수3"); } first(); 위 코드가 실행되면, 다음과 같이 함수 호출 순서에 따라 실행 컨텍스트가 콜 스택에 쌓였다가 제거됩니다. <출처: 작가> first() 함수가 호출되면 실행 컨텍스트가 콜 스택에 추가됩니다.first() 함수 내부에서 second() 함수가 호출되므로 새로운 실행 컨텍스트가 추가됩니다.second() 함수 내부에서 third() 함수가 호출되므로 또 다른 실행 컨텍스트가 추가됩니다.third() 함수가 실행되면서 "함수3"이 출력된 후, third() 함수의 실행이 종료되면서 해당 실행 컨텍스트가 콜스택에서 제거됩니다.second() 함수로 돌아와 "함수2"가 출력된 후, second() 함수의 실행이 종료되면서 실행 컨텍스트가 제거됩니다.마지막으로 first() 함수의 "함수1"이 출력된 후, first() 함수의 실행이 종료되면서 실행 컨텍스트가 콜 스택에서 제거됩니다. 이처럼 콜 스택은 함수 실행 순서를 관리하는 중요한 역할을 합니다. 2) 실행 컨텍스트의 종류자바스크립트에서 실행 컨텍스트는 실행되는 코드에 따라 다르게 생성됩니다. 크게 두 가지 실행 컨텍스트가 존재하는데요, 하나씩 살펴보도록 하겠습니다. 첫 번째는 전역 실행 컨텍스트(Global Execution Context)입니다. 코드가 처음 실행될 때 생성되며, window 또는 global 객체가 포함됩니다. 전역 실행 컨텍스트는 프로그램이 종료될 때까지 콜 스택에 유지됩니다. 두 번째는 함수 실행 컨텍스트(Function Execution Context)입니다. 함수가 호출될 때마다 생성되며, 함수 내부의 변수와 실행 정보를 포함합니다. 각 함수는 실행될 때마다 새로운 실행 컨텍스트를 생성하며, 실행이 끝나면 해당 컨텍스트는 콜스택에서 제거됩니다. 예를 들어, 전역 실행 컨텍스트와 함수 실행 컨텍스트가 어떻게 생성되고 관리되는지 살펴보겠습니다. let globalVar = "전역 변수"; function greet() { let message = "안녕하세요"; console.log(message); } greet(); 이 코드가 실행되면, 실행 컨텍스트는 다음과 같은 흐름으로 진행됩니다. <출처: 작가> 코드가 실행되면 전역 실행 컨텍스트가 생성됩니다. 이때 globalVar 변수가 전역 실행 컨텍스트에 포함됩니다.greet() 함수가 호출되면 새로운 함수 실행 컨텍스트가 생성되어 콜 스택에 추가됩니다. 이 실행 컨텍스트에는 message 변수가 포함됩니다.console.log(message)가 실행되면서 "안녕하세요"가 출력됩니다.greet() 실행이 종료되면 해당 실행 컨텍스트가 콜 스택에서 제거됩니다.코드가 종료되면 전역 실행 컨텍스트가 콜 스택에서 제거됩니다. 이처럼 실행 컨텍스트는 코드가 실행될 때마다 생성되며, 실행이 끝나면 사라지는 구조를 가집니다. 그리고 위의 그림에서 볼 수 있듯이, 코드가 시작될 때 생성되고 전역 공간의 정보가 저장되는 컨텍스트를 ‘전역 실행 컨텍스트’, 함수가 호출될 때 생성되는 컨텍스트를 ‘함수 실행 컨텍스트’라고 부릅니다. 그렇다면 이러한 실행 컨텍스트는 어떤 과정으로 생성되고 종료될까요? 3) 실행 컨텍스트의 생성과 종료실행 컨텍스트는 코드가 실행될 때 자동으로 생성되며, 크게 세 가지 단계를 거칩니다. 먼저 생성 단계에서 실행 컨텍스트가 생성되고, 자바스크립트 엔진은 실행할 코드의 환경을 설정합니다. 이때 this 바인딩이 결정되며, 실행할 코드가 전역 코드인지 함수 코드인지 판단됩니다. 또한 변수와 함수 선언을 위한 공간이 미리 확보되고, 변수가 아직 초기화되지 않고 존재만 등록됩니다. 이후 초기화 단계에서는 생성 단계에서 등록된 변수와 함수가 초기화됩니다. var로 선언된 변수는 undefined로 초기화되고, 함수 선언문은 메모리에 저장됩니다. 이 단계에서 변수와 함수가 메모리에 로드되지만, 실제 값이 할당되지는 않습니다. 마지막으로 실행 단계에서는 코드가 실제로 실행되면서 변수에 값이 할당되고, 연산이 수행되며, 함수가 실행됩니다. 실행이 완료되면 실행 컨텍스트는 콜스택에서 제거되며, 프로그램은 다음 실행 컨텍스트로 진행합니다. 예를 들어, 다음 코드가 실행될 때 실행 컨텍스트가 어떻게 생성되고 종료되는지 살펴보겠습니다. function sayHello() { let text = "Hello, world!"; console.log(text); } sayHello(); 위 코드를 실행하면 먼저 sayHello() 함수가 호출되면서 새로운 실행 컨텍스트가 생성됩니다. 생성 단계에서 text 변수가 메모리에 등록되지만, 아직 값이 할당되지 않습니다. 그다음 초기화 단계에서 text 변수는 undefined로 초기화됩니다. 실행 단계에서 text 변수에 “Hello, world!”가 할당되고, console.log(text);가 실행됩니다. 마지막으로는 sayHello() 함수 실행이 종료되면서 실행 컨텍스트가 콜 스택에서 제거됩니다. 이처럼 실행 컨텍스트는 생성, 초기화, 실행 단계를 거치면서 코드의 흐름을 관리합니다. 이를 이해하면 변수의 스코프, 호이스팅, this 바인딩 등의 개념도 더욱 쉽게 파악할 수 있습니다. 실행 컨텍스트의 내부 구조실행 컨텍스트가 생성되면, 내부적으로 변수 환경(Variable Environment)과 렉시컬 환경(Lexical Environment)이 함께 생성됩니다. 이 두 가지 개념은 변수와 함수 선언을 관리하며, 실행 중인 코드의 스코프를 결정하는 중요한 요소입니다. 이를 이해하면 자바스크립트에서 변수가 어떻게 저장되고 검색되는지, 그리고 클로저(Closure)가 어떻게 동작하는지 명확하게 이해할 수 있습니다. <출처: Napkin AI, 작가 편집> 1) 변수 환경(Variable Environment)변수 환경(Variable Environment)은 실행 컨텍스트에서 선언된 변수, 함수 선언, this 바인딩 정보 등을 포함하는 공간입니다. 사실 변수 환경은 렉시컬 환경(Lexical Environment)과 구조가 동일하지만, 단순히 변수의 값이 변경되는 부분만을 관리하는 역할을 합니다. 변수 환경에서 가장 중요한 개념 중 하나는 호이스팅(Hoisting) 입니다. 자바스크립트에서는 변수 선언이 코드의 실행 전에 메모리에 미리 저장되는데요, 이 과정이 바로 호이스팅입니다. 예를 들어, 아래 코드를 보면 실행 전에 변수 선언이 어떻게 처리되는지 알 수 있습니다. console.log(name); // undefined var name = "JavaScript"; console.log(name); // "JavaScript" 위 코드에서는 name 변수를 선언하기 전에 console.log(name);을 실행했는데도 오류가 발생하지 않고 undefined가 출력되는데요, 이 이유는 var로 선언된 변수가 실행 전에 메모리에 등록되지만, 초기화되지 않고 undefined로 설정되기 때문입니다. 반면, let과 const로 선언된 변수는 실행 전에 초기화되지 않으므로 선언 전에 접근하면 ReferenceError가 발생합니다. 이러한 현상을 TDZ(Temporal Dead Zone, 일시적 사각지대) 라고 합니다. 또한 변수 환경에는 this 바인딩 정보도 포함됩니다. 실행 컨텍스트가 생성될 때, this가 어떤 값을 가리킬지가 결정되는데요. 이는 실행되는 환경(전역 또는 함수)에 따라 달라집니다. 전역 실행 컨텍스트에서는 this가 브라우저 환경에서는 window 객체를, Node.js 환경에서는 global 객체를 가리킵니다. 하지만 함수 실행 컨텍스트에서는 this가 함수 호출 방식에 따라 다르게 결정됩니다. function showThis() { console.log(this); } const obj = { method: showThis }; showThis(); // window obj.method(); // obj 위 코드에서 showThis 함수는 일반 함수 호출일 경우, window(브라우저)를 가리키지만, obj.method()처럼 객체의 메서드로 호출될 경우 obj를 가리키게 됩니다. 2) 렉시컬 환경렉시컬 환경(Lexical ENvironment)은 변수 환경과 유사하지만, 실행 컨텍스트의 스코프(Scope)와 스코프 체인(Scope Chain)을 관리하는 역할을 합니다. 자바스크립트는 렉시컬 스코프(Lexical Scope)를 기반으로 동작하기 때문에 렉시컬 환경을 이해하는 것이 중요합니다. 렉시컬 환경은 현재 실행 컨텍스트에서 선언된 변수, 함수, 매개변수 등을 저장하는 객체인 환경 레코드(Environment Record)와 현재 실행 컨텍스트 바깥의 렉시컬 환경을 참조하여 변수를 찾는 역할을 하는 외부 렉시컬 환경(Outer Lexical Environment Reference)으로 구성됩니다. <출처: Napkin AI, 작가 편집> 코드를 통해 조금 더 자세하게 배워보겠습니다. function outer() { let outerVar = "바깥 변수"; function inner() { let innerVar = "안쪽 변수"; console.log(innerVar); // "안쪽 변수" console.log(outerVar); // "바깥 변수" } inner(); } outer(); 이 코드가 실행되면 먼저 outer 함수가 호출되면서 새로운 실행 컨텍스트가 생성됩니다. 이 실행 컨텍스트에는 outerVar 변수가 포함되며, 이는 outer 함수의 렉시컬 환경에 저장됩니다. 그런 다음 inner 함수가 실행되면서 또 다른 실행 컨텍스트가 생성되는데, 이 실행 컨텍스트에서는 innerVar 변수가 선언되고 저장됩니다. <출처: 작가> 즉, inner 함수는 실행될 때 자신의 렉시컬 환경을 가지지만, 동시에 outer 함수의 렉시컬 환경도 참조할 수 있어 outerVar 변수에 접근할 수 있게 됩니다. 이처럼 내부 함수가 외부 함수의 변수를 참조할 수 있는 이유는, 외부 렉시컬 환경을 따라 스코프 체인을 검색하기 때문입니다. 이러한 매커니즘을 활용하면 클로저(Closure)를 만들 수 있습니다. 그럼 클로저에 대해서도 간단하게 살펴볼까요? 클로저는 내부 함수가 외부 함수의 변수를 계속해서 기억하고 접근할 수 있는 개념입니다. 아래의 예제를 통해 쉽게 이해해 보겠습니다. function makeCounter() { let count = 0; return function () { count++; console.log(count); }; } const counter = makeCounter(); counter(); // 1 counter(); // 2 위 코드에서 makeCounter 함수가 실행되면, count 변수를 포함하는 실행 컨텍스트가 생성됩니다. 그리고 makeCounter 함수가 실행이 끝나면서 실행 컨텍스트는 제거되지만, 반환된 내부 함수는 여전히 count 변수를 참조할 수 있습니다. 왜냐하면, 내부 함수의 렉시컬 환경이 외부 렉시컬 환경을 참조하고 있기 때문입니다. 즉, 클로저는 실행 컨텍스트가 사라진 후에도 변수를 유지할 수 있도록 해주는 강력한 개념으로, 자바스크립트에서 상태를 기억하는 함수나 비동기 처리에서 자주 활용됩니다. 마치며자바스크립트는 코드를 실행하기 위해 파싱, 컴파일, 실행 단계를 거쳐 컴퓨터에게 전달됩니다. 이 과정에서 실행 컨텍스트는 코드가 실행되는 환경을 정의하는 핵심 개념으로 작용하며, 실행 흐름을 제어하고 변수를 관리하는 역할을 합니다. 실행 컨텍스트의 구조를 이해하면 스코프, 호이스팅, 클로저 같은 중요한 개념을 보다 깊이 있게 다룰 수 있습니다. 특히 코드의 실행 순서를 정확히 파악할 수 있어 디버깅과 최적화에도 큰 도움이 됩니다. 따라서 자바스크립트의 실행 컨텍스트를 깊이 이해하는 것은 보다 안정적이고, 효율적인 코드를 작성하는 데 필수적인 요소라고 할 수 있습니다. 이번 글을 통해 개발자로서 자바스크립트에 대해 더 잘 이해하는 계기가 되었길 바랍니다. ©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.