2023년 5월 11일
실행 컨텍스트란 (Execution Context)는 scope, hoisting, this, function, closure 등의 동작원리를 담고 있는 자바스크립트의 핵심원리를 담고있는 내용이라고 합니다.
즉 이것은 자바스크립트의 동적언어로서의 성격을 잘 파악할수 있는 개념이라고 할수있으며, 실행 컨텍스트를 이해하지 못한다고 하면 사실상 자바스크립트에 대한 이해도가 낮다고 할수 있습니다.
그래서 이 기회에 공부를 해보며, 완벽하진 않아도 ‘아 자바스크립트는 이렇게 실행되는구나!’ 라고 생각이 들정도로는 정리를 해보려 합니다.
스택과 큐는 알고리즘 자료구조입니다.
이는 자바스크립트의 개념이라기보다는 프로그래밍 개념이라고 할수 있겠습니다.
완벽하게 아는것은 아니지만 짧게 개념만 공부해보겠습니다.
스택은 바닥이 막힌 원통이라고 생각해보겠습니다.
해당 원통형에 물건을 차곡차곡 넣으면 꺼낼때는 위에 있는것부터 꺼내질것입니다. 이렇게 데이터의 입출력을 후입선출 방식입니다.
아마 가장 흔하게 만나볼수 있는 스택구조는 winodw.history와 같은 네비게이션 객체이지 않나 싶습니다. 페이지를 이동할때 스택구조에 페이지를 저장할 경우 뒤로가기를 클릭하면 그저 가장 최상단에 있는 페이지로 이동시키기만 하면 되기 때문입니다.
참고로 이런 스택은 제한이 있기 때문에 해당 스택을 넘어가는 횟수의 함수호출을 할 경우 에러를 발생시키게 됩니다.
큐는 양쪽이 뚫려있는 원통형과 같습니다. 먼저 들어온것이 먼저 나가는 선입선출의 구조로 되어있습니다. 이러한 큐는 입력된 순서대로 데이터를 처리하기 때문에 순서가 정해진 작업을 할 경우 사용할 수 있습니다.
자바스크립트는 제목에서처럼 알수 있듯이 스크립트 언어입니다. 그런데 공부하다보면 항상 마주치는것이 하나있습니다. 바로 ECMAscript, 그러다보니 마치 ES가 자바스크립트 자체라고 오해할수도 있는데, 사실은 ES는 모든 스크립트 언어가 호환될수 있도록 만든 협약같은 것이며, 최근에 자바스크립트는 이런 ES를 기반으로 만들어졌다고 할 수 있습니다.
이런 자바스크립트를 웹에서 실행시키기 위해 만들어진것이 자바스크립트 엔진입니다. 각 브라우저마다 엔진의 종류는 다르지만 기본적인 구조는 같습니다.
자바스크립트 엔진은 메모리힙과 콜스택구조를 가지고 있으며, 자바스크립트가 실행되면 실행컨텍스트가 콜스택이라는 엔진내에 스택구조에 쌓이게 되고, 역시 한번에 한가지 일을 수행하게 됩니다.
여담으로 콜스택구조에는 원시타입 데이터가, 메모리 힙에는 참조타입 데이터가 저장된다고 합니다.
하지만 우리는 코딩을하며 비동기적으로 무언가를 실행시켜본 경험이 있을것입니다. 만약 자바스크립트가 한번에 한개의 일밖에 못한다면 어떻게 비동기적으로 일을 처리할수가 있었을까요?
그 이유는 브라우저에는 자바스크립트 엔진 외에도 WebAPI, 콜백 큐, 이벤트 루프라는것이 있기 때문입니다.
해당 구조들이 어떤역할을 하는지 짧게 알아보겠습니다.
첫번째로 WebApi는 우리가 사용하는 여러가지 기능들입니다. setinterval이나 domevent같은 이벤트들이 webAPi라고 할수 있으며 자세한 정리는 다음번에 하기로 하고 다음으로 넘어갑니다.
두번째인 콜백 큐는 비동기 처리를 하는 함수를 관리하는 친구입니다. 이런 콜백 큐는 ECMA2015에 joq queue라는 개념이 추가되었는데, 이는 웹 API가 아니라 다른 비동기 처리인 프로미스등이 우선권을 가지는 개념입니다.
세번째인 이벤트 루프는 콜스택이 비어있을때 콜백큐에 있는 함수들을 콜스택으로 보내는 일을 합니다.
이런식으로 자바스크립트는 싱글스레드이지만 비동기적 처리를 할수 있게 되었습니다.
실행 컨텍스트는 실행할 코드에 제공할 환경정보들을 모아놓은 객체입니다. 동일한 환경에 있는 코드들을 실행할때 필요한 환경정버돌을 모아 컨텍스트를 구성하고, 이를 콜스택에 쌓아올렸다가, 가장 위에 컨텍스트와 관련있는 코드들을 실행하는 식으로 전체코드의 환경과 순서를 보장합니다.
실행 컨텍스트는 자동으로 생성되는 전역공간과 eval()함수를 제외하면, 일반적으로 우리가 실행컨텍스트를 구성하는 방법은 함수를 실행하는것 뿐입니다.
var a = 1 // 전역 컨텍스트
function outer() {
// outer 컨텍스트
function inner() {
// inner 컨텍스트
console.log(a) // undefined
var a = 3
console.log(a) // 3
}
inner()
console.log(a) // 1
}
outer()
console.log(a) // 1
위 코드가 실행된다면 다음과 같은 순서로 실행됩니다.
프로그램 실행: [전역컨텍스트]
-> 전역컨텍스트가 콜스택에 담깁니다. 자바스크립트가 열리는 순간 활성화됩니다.
(실행 컨텍스트와 전역 컨텍스트는 특별히 다를것은 없으나, 전역 컨텍스트의 경우 관리하는 공간이 함수가 아닌 전역공간이기 때문에 arguments가 없습니다.)
outer 실행: [전역컨텍스트, outer]
-> outer함수를 호출하면 관련 환경정보를 수집해 outer 실행컨텍스를 생성한 후 콜스택에 담습니다. 이때 콜스택 최상위에 outer실행컨텍스트가 놓였으므로, 전역 컨텍스트와 관련된 코드의 실행을 중단하고 outer내부함수를 순차적으로 진행합니다.
inner 실행: [전역컨텍스트, outer, inner]
-> inner를 호출하며 관련정보를 담은 실행 컨텍스트를 콜스택에 담습니다.
inner 종료: [전역컨텍스트, outer]
-> inner함수를 종료합니다.
outer 종료: [전역컨텍스트]
-> outer를 2부터 재실행하며, 종료됩니다. 더이상 실행할 코드가 없어 전역컨텍스트도 제거됩니다.
다음은 실행컨텍스트를 구성할때 생기는정보들입니다.
VariableEnvironment
a. 현재 컨텍스트 내의 식별자(변수)들에 대한 정보
b. 외부 환경 정보
c. 선언 시점의 LexicalEnvironment의 스냅샷(변경사항 반영 X)
LexicalEnvironment
a. 처음에는 VariableEnvironment와 같음
b. 변경 사항이 실시간으로 반영됨
ThisBinding
c. 식별자가 바라봐야 할 대상 객체
VariableEnvironment에 담기는 내용은 LexicalEnvironment와 같지만, 최초 실행 시의 스냅샷을 유지합니다. 실행 컨텍스트를 생서할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 복사해서 LexicalEnvironment를 만듭니다.
주로 활용하는 것은 LexicalEnvironment입니다. 보통 VariableEnviroment는 스냅샷 유지를 목적으로 사용합니다.
LexicalEnvironment의 내부는 environmentRecord와 outerEnvironmentReference로 구성되어 있습니다.
environmentRecord로 인하여 호이스팅이 발생하며, outerEnvironmentReference로 인하여 스코프와 스코프체인이 형성됩니다.
자바스크립트는 코드를 실행하기전에 식별자를 수집합니다.
현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됩니다.
즉, 코드가 실행 되기 전에 자바스크립트의 엔진은 이미 실행 컨텍스트에 속한 변수명들을 모두 알고 있게 됩니다.
이 때 호이스팅이란 개념이 이용됩니다.
엔진의 실제 동작 방식 대신에 자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음, 실제 코드를 실행한다 라고 생각하면 이해하기 좀뎌 편합니다.
중요한 점은, 자바스크립트 엔진이 실제로 변수를 끌어올리지는 않지만, 편의상 끌어올리는 것으로 간주하자는 것입니다.
function a(x) {
console.log(x)
var x
console.log(x)
var x = 2
console.log(x)
}
a(1)
위 코드의 인자를 함수 내부의 다른코드보다 먼저 선언 및 할당이 되었다고 해석해보겠습니다.
function a() {
var x = 1 // 매개변수 할당
console.log(x)
var x
console.log(x)
var x = 2
console.log(x)
}
a()
그리고 이 코드는 호이스팅 되어 다음과 같이 해석될 수 있습니다.
function a() {
var x
var x
var x
x = 1
console.log(x) // 1
console.log(x) // 1
x = 2
console.log(x) // 2
}
a()
위는 변수의 경우이고 함수의 호이스팅은 다음과 같습니다.
function a() {
console.log(b)
var b = 'bbb'
console.log(b)
function b() {}
console.log(b)
}
a()
위 코드에서 함수가 호이스팅이 일어날 경우 함수전체가 호이스팅됩니다.
function a() {
var b
function b() {}
console.log(b) // f b () {}
b = 'bbb'
console.log(b) // bbb
console.log(b) // bbb
}
a()
자바스크립트의 함수는 일급객체(혹은 일급시민)이기 때문에 함수 표현식이 가능하다.
일급객체(일급시민)
여기 x라는 것이 있다.
x를 변수에 담을 수 있다.
x를 매개변수에 넘길 수 있다.
x를 함수에서 반환할 수 있다.
x를 만족할 때, 이를 일급객체라고 한다.
즉, 자바스크립트의 함수는 일급객체이므로
함수를 변수에 담을 수 있다.
함수를 매개변수로 넘길 수 있다.
함수를 함수에서 반환할 수 있다.
위의 같은 조건을 만족한다.
앞의 함수를 표현식으로 변경해보면
function a() {
console.log(b)
var b = 'bbb'
console.log(b)
var b = function() {} // b에 익명함수를 할당했다.
console.log(b)
}
a()
그리고 다음과 같이 해석될수 있습니다.
function a() {
var b
var b
console.log(b) // undefined
b = 'bbb'
console.log(b) // bbb
b = function() {} // b에 익명함수를 할당했다.
console.log(b) // f () {}
}
a()
스코프란 식별자에 대한 유효범위 입니다. 스코프 A에서 선언한 변수는 A의 내,외부에서 모두 접근이 가능하지만, A에서 선언된 변수는 A내부에서만 접근할수 있습니다.
스코프의 개념은 대부분의 언어에 존재하지만, ES5까지의 Javascript는 특이하게도 오직 함수에 의해서만 스코프가 생성되었습니다.
outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다. [선언하다] 라는 행위가 실제로 일어날 수 있는 시점은 콜 스택 상에서 어떤 실행 컨텍스트가 활성화된 상태일 때뿐이다.모든 코드는 실행 컨텍스트가 활서화 상태일 때 실행되기 때문이다.
var a = 1 // 전역 컨텍스트
function outer() {
// outer 컨텍스트
function inner() {
// inner 컨텍스트
console.log(a)
var a = 3
console.log(a)
}
inner() // inner가 실행될 때 outer의 LexcicalEnvironemnt를 outerEnvironmentReference로 참조한다.
console.log(a)
}
outer() // outer가 실행될 때 전역 컨텍스트의 LexcicalEnvironemnt를 outerEnvironmentReference로 참조한다.
console.log(a)
위의 코드는 다음과 같은 scope chain을 형성합니다.
inner LexicalEnvironment {
식별자 a
outerEnvironmentReference = outer LexicalEnvironment {
식별자 a
outerEnvironmentReference = global LexicalEnvironment {
식별자 a
}
}
}
}
이러한 구조적 특성 덕분에 여러 스코프에 동일한 식별자를 선언할 경우, 무조건 scope chain 상에서 가장 먼저 발견된 식별자에만 접근 가능하게 됩니다.
inner LexicalEnvironment {
식별자 a # inner function에서 a에 접근할 때 여기에 가장 먼저 접근
outerEnvironmentReference = outer LexicalEnvironment {
식별자 a # outer function에서 a에 접근할 때 여기에 가장 먼저 접근
식별자 b # inner function에서 b에 접근할 때 여기에 가장 먼저 접근
outerEnvironmentReference = global LexicalEnvironment {
식별자 a # 전역에서 a에 접근할 때 여기에 가장 먼저 접근
식별자 b # 전역에서 b에 접근할 때 여기에 가장 먼저 접근
식별자 c # inner function에서 c에 접근할 때 여기에 가장 먼저 접근
}
}
}
}
실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장된다. this는 여기에 다루기에 복잡한 내용이 많기 때문에 다음 장에서 정확하게 설명하겠습니다.