Javascript의 주요 개념 중 실행 컨텍스트의 개념을 학습하면서 정리한 글이다.
모던 자바스크립트 딥 다이브 23장을 참고했으며 ES6 문법을 기준으로 작성했다.
들어가며
자바스크립트를 어느정도 공부하다보면 실행 컨텍스트라는 개념을 자주 접할 수 있다.
하지만 계속 겉핥기 식으로 이해하고 있다는 생각이 들어, 이번 기회에 다시 정리해보고자 한다.
실행 컨텍스트란
소스코드를 실행하는데 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리하는 영역이다.
이렇게 말하면 처음 접하는 사람들은 쉽게 이해하지 못할 것이다.
하지만 전체 글을 읽은 이후에 다시 돌아와 읽는다면 이보다 더 적합한 정의는 없을 것이라고 생각한다.
실행 컨텍스트를 알아야 하는 이유
우선 개념에 들어가기 앞서 실행 컨텍스트를 알아야 하는 이유를 알아보자.
이유에 대한 부가 설명은 후반부에서 하고, 지금은 실행 컨텍스트를 알면 어떠한 것들을 쉽게 이해할 수 있는지를 중점으로 기술한다.
1. 스코프 기반의 동작 방식의 이유를 설명할 수 있다.
우선 아래 예시를 보자.
function outer() {
let x = 10;
function inner() {
let y = 20;
console.log(x); // 여기서 출력되는 값은?
}
inner();
}
스코프라는 개념을 접한 사람이라면, 위 코드에서 console의 결과값으로 10이 출력됨을 알 수 있다.
하지만 왜 이렇게 출력되는지 물었을 때, 실행 컨텍스트 개념에 대한 이해가 없다면 단순히 스코프 체인의 동작 방식에 의해 변수 탐색을 수행하기 때문이라고 답할 것이다.
그러나 실행 컨텍스트를 이해하고 있다면 위 코드가 왜 그렇게 동작하는지 구체적으로 설명할 수 있다.
구체적으로는 실행 컨텍스트의 개념을 통해 자바스크립트의 스코프 체인과 변수 접근 매커니즘을 명확하게 이해할 수 있다.
2. 호이스팅이 발생하는 이유를 설명할 수 있다.
아래는 호이스팅이 발생하는 코드의 예시이다.
console.log(name); // undefined
var name = "lee";
많은 개발자들이 호이스팅 현상은 알고 있지만, 그럼 호이스팅은 왜 발생하는 걸까? 라는 질문에 대해서는 명확히 설명하지 못한다.
실행 컨텍스트를 이해하면 이러한 호이스팅이 어떠한 내부 매커니즘을 통해 발생하는지를 명확하게 설명할 수 있다.
정리하자면, 실행 컨텍스트를 이해하면 자바스크립트의 고급 문법의 동작 원리들을 명확한 이유로 설명할 수 있는 것이다.
또한 이러한 개념적인 이해 뿐만 아니라, 개념적인 이해를 기반으로 실제 개발 업무에서 문제 발생 시 디버깅 과정에서 코드의 동작 순서를 파악하며 효율적으로 문제 해결을 할 수 있기 때문에 Javscript를 사용하는 개발자라면 실행 컨텍스트를 이해하는 것은 매우 중요하다.
소스코드의 평가와 실행
실행 컨텍스트를 이해하기 이전에 모든 소스코드는 평가와 실행 2가지 과정으로 처리된다는 사실을 알아야 한다.
평가 과정에서는 실행 컨텍스트를 생성하고 변수, 함수 등의 선언문만 먼저 실행하여 실행 컨텍스트에 등록하고,
평가가 끝나면 소스코드가 실행되며, 이때 소스 코드의 실행 결과는 다시 실행 컨텍스트의 스코프에 등록된다.
아래 예시를 통해 살펴보자.
var x; // 1. 변수 선언문
x = 1; // 2. 변수 할당문
(1) 평가, (2) 실행 과정으로 진행될 때
(1) 평가 과정에서는 1번 변수 선언문이 실행되어 변수 식별자 x는 실행 컨텍스트가 관리하는 스코프에 등록되어 undefined로 초기화된다.
(2) 실행 과정에서는 2번 변수 할당문이 실행되어 x에 1이라는 값이 할당된다.
실행 컨텍스트는 왜 필요할까?
자바스크립트에서 실행 컨텍스트는 왜 필요한 개념인 걸까?
아래 예제가 실행되는 과정을 통해 그 이유를 파악해보자.
const x = 1;
const y = 2;
function foo(a) {
const x = 10;
const y = 20;
console.log(a + x + y);
}
foo(100);
console.log(x + y);
소스 코드가 실행되는 과정을 크게 4가지로 나눌 수 있다.
1. 전역 코드 평가
2. 전역 코드 실행
3. 함수 코드 평가 (foo 함수)
4. 함수 코드 실행
특히 4번 과정에서 foo 함수 내부에 있는 console.log를 실행할 때를 생각해보자.
console.log 내부에 전달된 표현식인 a + x + y가 평가될 것이고, a, x, y 식별자는 스코프 체인을 통해 검색해야 한다.
그렇게 a, x, y를 검색해서 값을 찾고, 나면 console.log 코드가 종료되고, foo 함수가 끝났으니 다시 foo 함수 호출문 아래부터 코드를 실행해야 한다.
그렇다면 이러한 동작들은 누가 관리하는 것일까?
1) 전체 소스코드 내에서 스코프를 구분하고
2) 식별자와 바인딩 된 값이 스코프에 따라 다르게 관리되고
3) 중첩 관계에 따라 스코프 체인을 형성해서 식별자를 검색할 수 있어야 하고
4) 전역 객체의 프로퍼티도 전역 변수처럼 검색할 수 있어야 하고
5) 함수 호출이 종료되면 함수 호출 이전으로 돌아가기 위해 현재 코드와 이전의 코드를 구분해야 한다.
즉, 정리해보면 코드가 원활하게 실행되려면 스코프, 식별자, 코드 실행 순서의 관리가 필요하다.
여기서 실행 컨텍스트의 필요성을 알 수 있다.
실행 컨텍스트란 소스코드를 실행하는데 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리하는 영역이다.
구체적으로, 식별자를 등록하고 관리하는 스코프와 코드 실행 순서를 관리하는 매커니즘으로, 모든 코드는 실행 컨텍스트를 통해 실행되고 관리된다.
식별자와 스코프는 실행 컨텍스트의 렉시컬 환경으로 관리하고, 코드 실행 순서는 실행 컨텍스트 스택으로 관리한다.
그렇다면 렉시컬 환경과 실행 컨텍스트 스택에 대해서도 알아야 실행 컨텍스트에 대한 이해를 할 수 있을 것이다.
실행 컨텍스트 스택이란
코드의 실행 순서를 관리하는 자료구조
아래 코드를 통해 실행 컨텍스트가 어떻게 관리되는지 알아보자.
const x = 1;
function outer() {
const y = 2;
function inner() {
const z = 3;
console.log(x + y + z);
}
inner();
}
outer();
위 코드에서 코드의 타입은 크게 전역 코드와 함수 코드가 있다.
자바스크립트 엔진이 소스 코드를 평가하면 실행 컨텍스트가 생성되며, 생성된 실행 컨텍스트는 스택에 쌓인다.
위 코드의 실행 컨텍스트를 시간의 흐름순으로 나타내면 아래와 같다.
(a) 전역 코드의 평가와 실행
- 전역 코드가 평가되면 전역 실행 컨텍스트가 생성되고 실행 컨텍스트 스택에 추가된다.
- 전역 코드의 평가가 끝나면 전역 코드가 실행되고, outer 함수가 호출된다.
(b) outer 함수의 평가와 실행
- outer 함수가 평가되면 outer 함수 실행 컨텍스트가 생성되고 실행 컨텍스트 스택에 추가된다.
- outer 함수에서 inner 함수를 호출한다.
(c) inner 함수의 평가와 실행
- inner 함수가 평가되면 inner 함수 실행 컨텍스트가 생성되고 실행 컨텍스트 스택에 추가된다.
- inner 함수가 실행되면 z에 값을 할당하고 console.log를 호출한 이후 inner 함수가 종료된다.
(d) inner 함수 종료 후 outer 함수로 복귀
- inner 함수가 종료되면 멈춰있던 outer 코드로 이동한다.
- 자바스크립트 엔진은 inner 함수의 실행 컨텍스트를 스택에서 제거한다.
- outer도 실행할 코드가 없기 때문에 종료된다.
(e) outer 함수 종료 후 전역 코드로 복귀
- outer 함수가 종료되면 멈춰있던 전역 코드로 이동한다.
- 자바스크립트 엔진은 outer 함수의 실행 컨텍스트를 스택에서 제거한다.
- 전역 코드까지 종료되면 남아있던 전역 컨텍스트도 스택에서 제거한다.
정리하자면 아래와 같다.
실행 컨텍스트가 평가될 때 => 실행 컨텍스트가 생성되고 스택에 추가
현재 코드가 종료됐을 때 => 실행 컨텍스트 스택에서 삭제
그리고 실행 컨텍스트 스택의 최상위에 존재하는 실행 컨텍스트는 언제나 현재 실행 중인 코드의 실행 컨텍스트이다.
렉시컬 환경이란
실행 컨텍스트를 구성하는 일부로 식별자와 식별자에 바인딩된 값, 상위 스코프에 대한 참조를 기록하는 자료구조
렉시컬 환경은 2개의 요소로 이루어져 있다.
1. 환경 레코드
- 스코프에 포함된 식별자를 등록하고 바인딩하는 저장소
- ES6 이후로 let, const 키워드가 등장했고, 이를 구분하기 위해 아래의 2개 공간으로 나눈다.
1) 객체 환경 레코드 : var으로 선언한 변수, 함수 선언문으로 정의한 전역 함수, 빌트인 전역 프로퍼티/함수, 표준 빌트인 객체를 관리
2) 선언적 환경 레코드 : let, const로 선언한 변수
2. 외부 렉시컬 환경 참조
- 상위 스코프(해당 실행 컨텍스트를 생성한 소스코드의 상위 코드의 렉시컬 환경)를 가리키는 값을 가진다.
- 외부 렉시컬 환경에 대한 참조를 통해 단방향 연결 리스트 형태로 스코프 체인을 구현한다.
- 즉, 렉시컬 환경의 외부 렉시컬 환경 참조에 저장된 상위 스코프의 렉시컬 환경 참조값으로 변수 탐색을 수행할 수 있는 것이다.
간단한 코드의 예시와 각 스코프에서 렉시컬 환경의 구성을 시각화 해보았다.
전역 코드와 func 함수 코드는 각각의 실행 컨텍스트와 렉시컬 환경을 가지고, 렉시컬 환경 내부에는 환경 레코드와 외부 렉시컬 환경 참조값을 가진다. 전역 렉시컬 환경의 외부 렉시컬 환경에 대한 참조는 없기 때문에 null 값을 가진다.
실행 컨텍스트의 동작 과정
지금까지 실행 컨텍스트를 알아야 하는 이유와 실행 컨텍스트를 이해하기 위해 필요한 개념들을 알아보았다.
이제는 실제 소스코드가 실행되는 동안 실행 컨텍스트가 어떻게 생성되고, 소스 코드가 관리되는지, 식별자는 어떻게 탐색하는지 알아보자.
아래 소스코드를 기준으로 한다.
var x = 'global x';
const y = 'global y';
function outer(oParam) {
var x = 'outer x';
const z = 'outer z';
function inner(iParam) {
var x = 'inner x';
console.log(x, y, z);
console.log(oParam, iParam);
}
inner('inner param');
console.log(x);
}
outer('outer param');
console.log(x);
동작 과정은 아래와 같이 나눌 수 있다.
1. 전역 객체 생성
2. 전역 코드 평가
3. 전역 코드 실행
4. outer 함수 코드 평가
5. outer 함수 코드 실행
6. inner 함수 코드 평가
7. inner 함수 코드 실행
8. inner 함수 코드 실행 종료
9. outer 함수 코드 실행 종료
10. 전역 코드 실행 종료
1. 전역 객체 생성
전역 객체란?
코드가 평가되기 이전에 자바스크립트 엔진으로 부터 자동으로 생성되는 특수한 객체
환경에 따라 다른 이름을 가지며, 브라우저에서는 전역 객체가 window이고, node.js에서는 global이다.
전역 객체는 아래 구조를 가진다.
1) 전역 코드가 평가되기 이전에 전역 객체가 생성된다.
2) 이때 전역 객체에는 빌트인 전역 프로퍼티, 빌트인 전역 함수, 표준 빌트인 객체가 추가된다. (+ 환경에 따라 Web API나 호스트 객체도 포함 가능)
2. 전역 코드 평가
소스 코드가 로드되면, 다음과 같은 순서로 코드를 평가한다.
1. 전역 실행 컨텍스트 생성
2. 전역 렉시컬 환경 생성
2-1. 전역 환경 레코드 생성
2-2. this 바인딩
2-3. 외부 렉시컬 환경에 대한 참조 결정
1. 전역 실행 컨텍스트 생성
비어있는 전역 실행 컨텍스트를 생성하고 실행 컨텍스트 스택에 추가한다.
2. 전역 렉시컬 환경 생성
전역 렉시컬 환경을 생성하고 전역 실행 컨텍스트에 바인딩(식별자와 값을 연결)한다.
2-1. 전역 환경 레코드 생성
전역 렉시컬 환경에는 객체 환경 레코드와 선언적 환경 레코드가 있다고 했다.
1) 먼저 객체 환경 레코드를 생성하고 전역 객체와 연결한다.
객체 환경 레코드는 BindingObject라고 부르는 객체와 연결되는데, 이 객체는 전역 객체이다.
이때 var 키워드로 선언된 전역 변수와 함수 선언문으로 정의된 전역 함수는 객체 환경 레코드와 연결된 전역 객체의 프로퍼티와 메서드가 된다.
변수 x는 var 키워드로 선언한 변수이다.
var로 선언한 변수는 선언과 초기화가 동시에 진행되기 때문에, 암묵적으로 undefined로 바인딩 된다.
이것이 var 키워드로 선언한 변수를 선언문 이전에도 참조가 가능한 이유이다.
정리) var 키워드로 선언한 변수를 선언문 이전에도 참조가 가능한 이유
아래와 같은 상황에서 선언문 이전에 첫번째 줄에서 a를 참조했을 때, 참조 에러가 발생하지 않고 undefined가 출력된다.
그 이유는 코드를 실행하기 전인 평가 단계에서 var로 선언한 변수 a를 전역 객체에 등록하면서 undefined라는 값을 넣어두었기 때문이다. 이렇게 선언문이 코드의 최상단으로 끌어올려진 것처럼 동작하는 현상을 호이스팅이라고 한다.
console.log(a); // undefined
var a = 3;
2) 선언적 환경 레코드를 생성한다.
let, const로 선언한 전역 변수는 선언적 환경 레코드에 등록되고 관리된다. (let, const 변수에 할당한 함수 표현식도 포함)
전역에서 선언한 y는 const로 선언한 변수이므로 전역 객체의 프로퍼티가 되지 않기에 window.y와 같이 전역 객체의 프로퍼티로 참조할 수 없다.
또한 선언과 초기화가 따로 진행되기 때문에 let이나 const로 선언한 변수는 런타임에 변수 선언문에 도달하기 전까지 일시적 사각지대(TDZ)에 빠지게 된다.
const a = 1; // 전역 변수
{
console.log(a); // 아직 a는 초기화되지 않은 상태라 참조 에러 발생
const a = 3; // 지역 변수
}
2-2. this 바인딩
전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에 this가 바인딩된다.
2-3. 외부 렉시컬 환경에 대한 참조 결정
외부 렉시컬 환경에 대한 참조는 상위 스코프를 가리킨다.
현재 평가중인 소스코드는 전역 코드이므로, 전역 렉시컬 환경의 외부 렉시컬 환경에 대한 참조값은 null이다.
3. 전역 코드 실행
지금까지 전역 코드를 평가했으니, 다음은 실행 단계이다.
전역 스코프에 해당하는 a 구간의 코드를 순차적으로 실행한다.
x와 y 할당문이 실행되면 전역 변수 x, y에 값이 할당되고 아래와 같이 상태가 변경된다.
이후 outer 함수를 호출한다.
4. outer 함수 코드 평가
함수가 호출되면 함수 내부로 코드 제어권이 이동한다. 함수 코드 평가의 과정은 아래와 같다.
1. 함수 실행 컨텍스트 생성
2. 함수 렉시컬 환경 생성
2-1. 함수 환경 레코드 생성
2-2. this 바인딩
2-3. 외부 렉시컬 환경에 대한 참조 결정
1. 함수 실행 컨텍스트 생성
함수 실행 컨텍스트가 생성되고 실행 컨텍스트 스택에 추가된다.
2. 함수 렉시컬 환경 생성
2-1. 함수 환경 레코드 생성
함수 환경 레코드의 환경 레코드는 매개변수, arguments 객체, 함수 내부에서 선언한 지역 변수와 중첩 함수를 등록하고 관리한다.
2-2. this 바인딩
함수 환경 레코드의 [[ThisValue]] 내부 슬롯에 this가 바인딩된다.
outer 함수는 일반 함수로 호출되었으므로 this는 전역 객체를 가리킨다.
2-3. 외부 렉시컬 환경에 대한 참조 결정
outer 함수는 전역 코드에 정의된 전역 함수이기에, outer 함수의 정의가 평가된 시점에 실행 중인 실행 컨텍스트의 렉시컬 환경인 전역 렉시컬 환경의 참조가 할당된다.
5. outer 함수 코드 실행
이제 outer 함수의 코드인 b구간의 코드가 순차적으로 실행된다.
x, z에 값을 할당한 이후의 상태는 아래와 같다.
그리고 inner 함수를 호출한다.
6. inner 함수 코드 평가
렉시컬 환경의 생성 과정은 outer 함수의 코드 평가와 동일하다.
7. inner 함수 코드 실행
c구간에 해당하는 코드들이 실행된다.
매개변수에 인수가 할당되고, 지역 변수 x에 값이 할당된다.
변수 할당문이 끝나면 console.log(x, y, z)가 실행된다.
console.log 실행 과정
실행 과정은 크게 다음과 같다.
1. console 식별자 검색
2. log 메서드 검색
3. 표현식 x, y, z 평가
4. console.log 메서드 호출
1. console 식별자 검색
현재 실행 중인 실행 컨텍스트는 inner 함수 실행 컨텍스트이므로, inner 함수 실행 컨텍스트의 렉시컬 환경에서 console 식별자를 검색하기 시작한다.
2. log 메서드 검색
console에서 log 메서드를 검색한다.
3. 표현식 평가
x, y, z라는 각각의 표현식을 평가하기 위해 각각의 식별자를 검색한다.
스코프 체인을 통해 x, y, z를 검색하고, inner 함수 환경 레코드를 기준으로 검색하여 존재하지 않으면 null을 만날 때까지 외부 렉시컬 환경 참조값으로 이동하여 식별자를 검색한다. 이것이 스코프 체인의 동작 원리이다. 이를 그림으로 나타내면 아래와 같다.
4. console.log 메서드 호출
표현식 x, y, z가 평가되어 값 'inner x', 'global y', 'outer z'으로 평가되고, 이를 console.log 메서드에 인수로 전달하여 호출한다.
8. inner 함수 코드 실행 종료
함수가 종료되면 실행 컨텍스트 스택에서 inner 함수의 실행 컨텍스트가 제거된다.
따라서 outer 함수의 실행 컨텍스트가 최상단에 오게 되므로 현재 실행중인 코드는 outer 함수가 된다.
9. outer 함수 코드 실행 종료
outer 함수가 종료되고 outer 함수의 실행 컨텍스트가 제거된다.
10. 전역 코드 실행 종료
outer 함수까지 종료되면 더이상 실행할 전역 코드가 없기 때문에 실행 컨텍스트 스택에는 아무것도 남아있지 않게 된다.
요약하기
1. 실행 컨텍스트란 소스코드를 실행하는데 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리하는 영역이다.
2. 실행 컨텍스트를 알아야 하는 이유는 첫 번째, 자바스크립트의 고급 문법(스코프 체인, 호이스팅 등)이나 깊은 동작원리를 이해하는 배경 지식이 되기 때문이고, 두 번째는 이러한 개념적인 이해를 바탕으로 효율적인 문제 해결을 할 수 있기 때문이다.
3. 실행 컨텍스트가 존재하는 이유는 코드를 원활하게 동작하게 하려면 스코프, 식별자, 코드 실행 순서의 관리가 필요하기 때문이다.
4. 콜 스택이라고도 불리는 실행 컨텍스트 스택이라는 자료구조에서 코드 실행 순서가 관리된다.
5. 스코프, 식별자의 관리는 렉시컬 환경에서 이루어지는데, 렉시컬 환경은 환경 레코드와 외부 렉시컬 환경의 참조값으로 구성되어 있다.
6. 스코프 체인의 동작은 렉시컬 환경의 외부 렉시컬 환경의 참조값을 통해 상위 스코프로 이동할 수 있기에 가능한 것이다.
7. 호이스팅은 코드의 평가가 먼저 이루어지기 때문에 발생하는 현상이다.
마무리하며
실제로 코드가 실행될 때 실행 컨텍스트가 어떤 방식으로 관리되는지 내부 동작들을 확인하면서, 실행 컨텍스트에 대한 충분한 이해가 되었다. 지금까지 호이스팅이 발생하는 이유나 코드 실행 과정에서 내부적으로 어떻게 관리되는지에 대한 막연한 궁금증은 있었는데, 오늘을 기점으로 깊은 동작 원리를 이해하는데 큰 도움이 되었다.
참고 자료
잘못된 내용이 있다면 이메일(tnghk9611@naver.com)또는 댓글 남겨주시면 감사하겠습니다.
'☘️ Front end > 🌱 Javascript' 카테고리의 다른 글
[Javascript] 클로저에 대해서 알아보자 (closure) (0) | 2025.01.28 |
---|---|
[Javascript] 콜백 함수 파헤치기 (callback function) (0) | 2025.01.24 |
[JavaScript] Javascript의 this에 대해 알아보자 (1) | 2025.01.22 |