실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다.
자바스크립트 엔진은 특정 실행 컨텍스트가 활성화되는 순간, 다음과 같은 중요한 작업들을 수행한다.
호이스팅(Hoisting): 해당 컨텍스트 내에 선언된 변수와 함수 선언을 코드의 최상단으로 끌어올린다. (실제로 이동하는 것은 아니지만, 엔진 내부적으로 먼저 처리한다.)
외부 환경 구성: 현재 컨텍스트가 속한 외부 스코프에 대한 정보를 설정한다. 이를 통해 스코프 체인을 따라 변수를 검색할 수 있게 된다.
this 값 설정: 해당 컨텍스트에서의 this 키워드가 어떤 객체를 참조해야 할지 결정한다.
스택은 출입구가 하나뿐인 깊은 우물같은 데이터 구조이고, 비어있는 스택에 순서대로 a,b,c,d를 저장했다면, 꺼낼 때는 반대로 d,c,b,a의 순서로 꺼낼 수 있다.
큐는 양쪽이 모두 열려있는 파이프 모양이다. 종류에 따라 양쪽 모두 입력과 출력이 가능한 큐도 있으나 보통은 한쪽은 출력만을 담당하는 구조를 말한다.
큐는 순서대로 데이터 a,b,c,d를 저장했다면 꺼낼 때도 a,b,c,d 순서로 꺼낼 수 있다.
콜 스택 예제
// ------------------------------ (1)
var a = 1;
function outer() {
function inner() {
console.log(a); // undefined
var a = 3;
}
inner(); // ------------------- (2)
console.log(a); // 1
}
outer(); // -------------------- (3)
console.log(a); // 1
위 코드에 대한 설명은 다음과 같다.
- (1) 자바스크립트 코드를 실행하는 순간 전역 컨텍스트가 콜 스택에 담긴다.
- (3) outer 함수를 호출하면 자바스크립트 엔진은 outer에 대한 환경 정보를 수집해서 outer 실행 컨텍스트를 생성한 후 콜 스택에 담는다. (콜 스택 맨 위에 outer 실행 컨텍스트가 놓인 상태가 됐으므로, 전역 컨텍스트와 관련된 코드의 실행을 일시 중단하고 대신 outer 실행 컨텍스트와 관련된 코드, 즉 outer 함수 내부의 코드들을 순차적으로 실행한다.)
- (2) inner 함수의 실행 컨텍스트가 콜 스택의 가장 위에 담기면 outer 컨텍스트와 관련된 . 코드의 실행을 중단하고 inner 함수 내부의 코드를 순서대로 진행한다.
- inner 함수 실행 종료되면 콜 스택에서 제거됨
- 아래에 있던 outer 컨텍스트가 콜 스택 맨 위에 존재하게 되므로, (2)의 다음줄부터 이어서 실행
- a 변수 출력하면 outer 실행 컨텍스트가 콜 스택에서 제거되고, 전역 컨텍스트만 남게됨
- 실행을 중단했던 (3) 다음줄 이어서 실행
- a 변수 값 출력하면 전역 공간에 실행할 코드가 없어 전역 컨텍스트도 제거. 콜 스택 비어진 채로 종료
실행 컨텍스트가 활성화 될 때 자바스크립트 엔진은 해당 컨텍스트에 관련된 코드들을 실행하는데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장함.
EnvironmentRecord는 현재 실행 컨텍스트와 관련된 코드의 모든 식별자 정보를 저장하는 장부이다. 자바스크립트 엔진은 컨텍스트가 생성될 때, 해당 컨텍스트의 코드를 처음부터 끝까지 쭉 훑으면서 변수와 함수 선언 정보를 차례대로 EnvironmentRecord에 기록한다. 마치 도서관 사서가 책 목록을 미리 정리해두는 것과 같다.
이러한 사전 작업 덕분에 우리는 호이스팅(Hoisting)을 겪게 된다. "자바스크립트 엔진이 식별자들을 최상단으로 끌어올려놓은 다음 코드를 실행한다"는 말은 절반만 맞는 이야기다. 실제로는 엔진이 코드를 실행하기 전에 이미 해당 스코프의 모든 변수명을 알고 있기 때문에, 우리가 마치 변수 선언 전에 변수를 사용하는 것처럼 보이는 것이다.
function a (x) {
console.log(x); //------------ (1) ➡️ 1
var x;
console.log(x); //------------ (2) ➡️ 1
var x = 2;
console.log(x); //------------ (3) ➡️ 2
}
a(1);
우리가 처음 예상했던 1, undefined, 2와 다른 결과가 나오는 이유는 바로 호이스팅 때문이다. a 함수가 실행되는 순간, a 함수만을 위한 실행 컨텍스트가 만들어지고, 변수 x에 대한 정보가 EnvironmentRecord에 먼저 기록된다.
호이스팅이 완료된 상태의 코드를 상상해보자.
function a (x) {
var x; // ➡️ 매개변수 x (이미 1로 초기화)
var x; // ➡️ 중복 선언 (무시)
var x; // ➡️ 중복 선언 (무시)
x = 1; // ➡️ 이미 초기화됨
console.log(x); //------------ (1) ➡️ 1
console.log(x); //------------ (2) ➡️ 1
x = 2; // ➡️ x 값 업데이트
console.log(x); //------------ (3) ➡️ 2
}
a(1);`]
실제 실행 순서는 다음과 같다.
호이스팅 시 변수는 선언만 끌어올려지지만, 함수 선언문은 함수 전체가 통째로 끌어올려진다. 반면 함수 표현식은 마치 일반 변수처럼 선언만 호이스팅된다.
1. 함수 선언문 (Function Declaration)
function sum(a, b) {
return a + b;
}
함수명만 존재하며, 별도의 변수에 할당하지 않는다.
호이스팅(끌어올림)이 일어나므로, 함수 선언 이전에 호출해도 정상 작동한다.
2. 함수 표현식 (Function Expression)
const multiply = function(a, b) {
return a * b;
};
함수를 변수에 할당하는 방식이다.
선언문과 달리, 함수 자체는 호이스팅되지 않기 때문에 정의 전에 호출하면 에러가 발생한다.
console.log(multiply(1, 2)); // ❌ TypeError: multiply is not a function
익명 함수 표현식
const divide = function(a, b) {
return a / b;
};
함수 이름 없이 변수에 바로 할당한다.
대부분의 함수 표현식이 이 형태로 사용된다.
기명 함수 표현식
const mod = function remainder(a, b) {
return a % b;
};
함수 안에 이름(remainder)을 붙이지만, 외부에서는 해당 이름으로 접근할 수 없다.
내부 재귀 호출 등에 사용할 수 있다.
mod(5, 2); // ✅ 실행 가능
remainder(5, 2); // ❌ ReferenceError
함수 선언문과 표현식의 차이점: 호이스팅
console.log(sum(1, 2)); // ✅ 3
console.log(multiply(1, 2)); // ❌ TypeError
function sum(a, b) {
return a + b;
}
const multiply = function(a, b) {
return a * b;
};
함수 선언문은 전체가 호이스팅되어 먼저 호출해도 문제가 없다.
함수 표현식은 변수 선언만 호이스팅되고, 할당된 함수는 나중에 평가되므로 오류가 발생
sum 함수는 선언 전에 호출해도 문제 없이 실행되기에 오류 가능성이 적은 함수 선언문이 더 나을 수도(?) 있다고 생각할 수 있겠지만 혼란을 야기할 수 도 있다.
전역 컨텍스트가 활성화 될 때, 전역 공간에 선언된 모든 함수들이 최상단으로 끌어올려지기에 동일한 변수명에 서로 다른 값을 할당한 경우, 나중에 할당한 값이 먼저 할당한 값을 덮어씌운다. 따라서 원활한 협업을 위해선 전역 공간 함수 선언 혹은 동명 함수 중복 선언을 피하는 것이 좋다. 혹은 동명 함수가 여럿 존재해도 모든 함수를 함수 표현식으로 작성하는 것이 좋다.