실행 컨텍스트는 함수가 '선언'될 때 생성된다.
왜 실행이 아닌 선언될 때 생성될까?
이에 대해 알기 위해서는 우리가 작성한 코드가 어떤 원리로 실행되는지 알아야 한다.
모든 코드는 실행되기 전 준비단계가 필요한데, 이 단계를 '평가' 단계라고 한다.
코드를 평가하는 단계에서는 변수, 함수 등의 선언문을 먼저 실행한다. 그 다음 실행 컨텍스트를 생성하여 선언된 변수나 함수 식별자를 여기에 등록한다.
(정확히는 실행컨텍스트가 관리하는 스코프(렉시컬 환경의 환경 레코드)에 등록한다.)
예시를 보자.
let x;
x = 1;
이 코드를 실행할 때 평가단계에서 let x
변수 선언문을 먼저 실행한다. 생성된 변수 x를 실행 컨텍스트에 등록하는데, 이 때 x의 값은 초기화된 undefined 이다.
준비가 끝나고 본격적인 실행 단계에서는,
선언된 변수와 함수가 선언되었는지 확인 후 값을 할당한다.
위의 예시를 이어 살펴보면,
x의 값에 1이 할당되어 x=1이 된다.
그렇다면 실행 컨텍스트에서는 어떻게 코드를 등록하고 관리할까?
실행 컨텍스트는 식별자를 등록하고 관리하는 스코프와 코드 실행순서 관리를 구현한 내부 메커니즘으로,
소스코드를 실행하는 데 필요한 환경을 제공하고 코드의 실행결과를 실제로 관리한다.
Global Context (전역 컨텍스트)
- 자바스크립트 엔진이 코드를 실행할 때 처음으로 생성되는 첫 실행 컨텍스트이다.
Functional Context (함수 컨텍스트)
- 선언된 함수가 호출될 때를 기점으로 생성이 되고, 함수의 모든 동작이 종료되면 소멸된다. (js 엔진의 콜 스택에서 제거된다.)
실행컨텍스트가 하는 일은 더 자세하게 다음과 같다.
이것을 실행컨텍스트의 구성으로 구분지어서 보면,
실행 컨텍스트 스택이 어떻게 코드의 실행순서를 관리하는지부터 정리해보자.
(실행 컨텍스트 스택을 콜 스택이라고 부르기도 한다.)
실행 컨텍스트는 스택의 형태로 코드의 실행순서를 관리한다.
소스코드(실행가능한 코드)의 타입에 따라 실행 컨텍스트를 생성하는 과정과 관리 내용이 다르기 때문에, 이 소스코드의 종류부터 알아보자.
전역에 존재하는 소스코드
전역에 정의된 함수, 클래스 등의 내부 코드는 포함되지 않는다.
함수 내부에 존재하는 소스코드
함수 내부에 중첩된 함수, 클래스 등의 내부 코드는 포함되지 않는다.
빌트인 전역 함수인 eval함수에 인수로 전달되어 실행되는 소스코드
모듈 내부에 존재하는 소스코드
모듈 내부의 함수, 클래스 등의 내부 코드는 포함되지 않는다.
이 코드의 종류들에 따라 스택에 넣는 과정을 이미지로 알아보자.
--이미지--
렉시컬 환경(Lexical Environment)은 식별자와 식별자에 바인딩된 값, 그리고 상위 스코프에 대한 참조를 기록하는 자료구조로 실행 컨텍스트를 구성하는 컴포넌트다.
렉시컬 환경은 다시 아래 2 개의 컴포넌트로 구성된다.
환경 레코드
스코프에 포함된 식별자를 등록하고 등록된 식별자에 바인딩된 값을 관리하는 저장소
외부 렉시컬 환경에 대한 참조
외부 렉시컬 환경에 대한 참조
를 통해 상위 렉시컬 환경과 연결되어 스코프 체인을 형성한다.무슨 말이지?
외부 렉시컬 환경에 대한 참조
에 상위 렉시컬 환경에 대한 참조값을 저장한다. 그리고 이 상위 렉시컬 환경에 대한 참조값이 상위 스코프이다.외부 렉시컬 환경에 대한 참조
컴포넌트에 상위 스코프에 대한 참조값을 넣는데, 이 상위 스코프는 함수가 정의된 위치에 따라 결정한다. 이것을 렉시컬 스코프라고 한다.자바스크립트 엔진은 함수를 정의한 위치에 따라 상위 스코프를 결정하는데, 이를 렉시컬 스코프라고 한다.
그렇다면, 어떻게 호출되는 위치와 상관없이 함수가 정의된 위치인 상위 스코프를 기억할까?
함수 객체에는 [[Environment]]라는 내부 슬롯이 있다. 이곳에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다.
왜?
렉시컬 스코프가 가능하려면 함수가 호출된 위치와 상관없이 정의된 위치인 상위 스코프를 기억해야 하기 때문이다.
외부 렉시컬 환경에 대한 참조
에 저장된다.이 부분이 헷갈릴 수 있다. 내부슬롯과 외부렉시컬 환경에 대한 참조에 각 상위스코프 참조값이 저장되는 시점이 다소 헷갈린다면, 아래 클로저의 예를 보면서 과정을 짚어보자.
클로저는 일종의 '현상'과 같은 개념으로 생각한다.
MDN의 정의는 함수와 그 함수가 선언된 렉시컬 환경과의 조합
이라고 한다.
클로저 : 외부함수보다 중첩함수가 더 오래 유지되는 경우
중첩함수는 이미 생명 주기가 종료한 외부함수의 변수를 참조할 수 있다.
즉, 클로저의 일반적인 조건은 다음과 같다.
어떻게 클로저가 가능할까?
위에서 알아본 렉시컬스코프와 관련해 클로저의 한 예를 함께 살펴보자.
아래 예시 코드가 실행되는 과정과 클로저가 형성되는 원리를 뜯어보자.
const x = 1;
function outer() {
const x = 10;
const inner = function () {console.log(x)};
return inner;
}
const innerFunc = outer();
innerFunc(); //10
아 코드에서 outer가 inner를 반환하고 종료되었음에도 inner함수에서 변수 x를 참조하여 10이라는 숫자를 출력한다.
외부 렉시컬 환경에 대한 참조
에 할당outer함수는 inner함수를 반환하면서 종료하기 떄문에 실행컨텍스트가 실행 컨텍스트 스택에서 제거된다. 그러나 inner함수에 저장된 outer함수의 렉시컬 환경은 유지된다.
가비지 컬렉션의 대상이 되지 않는다.
Garbage Collection이란, 자바스크립트 엔진이 내부적으로 사용하는 메모리 관리 시스템이다. 사용하지 않는 값들에 대한 메모리 영역을 주기적으로 정리하고 불필요한 메모리 사용이 없도록 관리한다.
가비지 컬렉터는 누군가가 참조하고 있는 메모리공간을 해제하지 않는다.
때문에 inner함수의 내부에서 상위 스코프를 참조할 수 있고 식별자의 값을 변경할 수도 있다.
func()를 실행했을 때, say()의 외부에서 어떻게 a,b 변수에 접근할 수 있었을까?
function say() {
const a = 1;
const b = 2;
function log() {
console.log(a + b);
}
return log;
}
const func = say();
func(); //3
모든 자바스크립트 함수는 선언될 당시에 클로저가 형성되어 주변환경을 기억할 수 있게 된다.
따라서 say 함수가 선언될 때 클로저가 생성되었다.
func() 호출로 인해 say 함수가 실행될 때 함수 실행 컨텍스트가 생성되어
변수와 내부의 함수를 선언하게 되고, 이 때 클로저가 생성되어 주변환경을 기억하게 된다.
따라서 변수 a, b, log함수 정보에 컴퓨터의 메모리에 저장되어, 함수 선언 당시의 환경에 대한 접근이 가능하게 된다.
function carrot() {
const food = "jjajang";
function potato() {
console.log(food);
}
mushroom(potato);
}
function mushroom(fn) {
fn();
}
carrot();
carrot 함수가 실행되면 실행컨텍스트에 변수와 함수 정보가 저장된다.
carrot 함수내부의 food 변수와 potato 함수가 선언되어 클로저가 형성되었다.
이후 mushroom함수로 인해 potato함수가 실행되는데, potato함수는 mushroom함수 내부에서 실행되었지만 carrot함수 내부의 food 변수에 접근하여 콘솔에 출력할 수 있다.
function carrot() {
let potatoCount = 0;
function potato() {
potatoCount++;
console.log(potatoCount);
}
return potato;
}
const veggie = carrot();
veggie(); // 1
veggie(); // 2
veggie(); // 3
carrot함수를 실행시키면 potatoCount와 potato함수 정보를 기억하는 클로저가 형성되었다.
veggie함수로 인해 carrot함수가 3번 실행되는데, 지속적으로 변화를 추적하여 potatoCount++된다.
function addCurry(x) {
return function add(y) {
return x + y;
};
}
const addFive = addCurry(5);
const result = addFive(5);
console.log(result);
addCurry함수를 실행하면 add함수가 생성되어 클로저가 형성된다. (매개변수 x=5라는 정보를 기억한다.)
addCurry(5)는
function add(y) {
return 5 + y;
}
를 리턴하여 addFive에 할당된다.
addFive(5)를 실행하면 5+5를 연산하여 10을 리턴하고 result에 할당된다.
function addCurry(x) {
return function add(y) {
return x + y;
};
}
const addFive = addCurry(5);
const addTen = addCurry(10);
const result1 = addTen(20);
const result2 = addFive(5);
const result3 = addTen(10);
console.log(result1); //30
console.log(result2); //10
console.log(result3); //20
addCurry함수는 한번 선언되었지만 2번 각기 다른 매개변수와 함께 호출되었다.
즉, 실제로 코드가 실행되며 생성된 add함수는 2개이다.
이에 따라 각각 2개의 add함수가 각각의 x매개변수의 값을 기억하고 있다.
언뜻 보면 모호하면서도 단순한 개념 같다.
클로저는 왜 필요한걸까? 어디에 활용되는 걸까?
상태(state)를 안전하게 변경하고 유지하기 위해 클로저를 사용한다.
의도치 않게 상태가 변경되지 않도록 방지하고, 특정 함수에서만 가능하도록 만들 때 활용한다.
캡슐화 (encapsulation)는 프로퍼티와 메서드를 하나로 묶는 것을 말한다.
정보 은닉은 이 캡슐화로 특정 프로퍼티/메서드를 감추는 것이다.
클로저 활용 사례에 대해서는 별도로 다시 정리할 예정이다.