카테고리 없음

[ JavaScript ] 실행 컨텍스트 그리고 호출 스택

람연 2021. 6. 18. 22:39

웹 페이지를 구성해 서비스를 올리면 웹 브라우저에 의해 HTML영역과 CSS영역이 각각 DOM Tree, CSSOM Tree로 컴파일 되며, 적절한 선택자를 통해 Render Tree를 구성해 브라우저에 표현한다. 마찬가지로 개발자가 작성한 JS 코드가 실제 동작하기까지의 매커니즘이 있다.


JS 엔진

JS 엔진은 종류에 따라 동작 방식과 매커니즘에 조금씩 차이가 있다. 그래서 웹 브라우저 중 점유율이 가장 높은 크롬에서 사용하는 V8엔진을 예로 본다.

웹 페이지가 컴파일 될 때, V8엔진은 JS 소스 코드를 파서(Parser)에게 전달해 추상 구문 트리(AST Abstract-Syntax-Tree)로 변환한다. V8엔진은 변환 된 추상 구문 트리를 바이트 코드로 변환한다. 개발자가 작성한 코드를 웹 브라우저에서 자원을 덜 소모하며 접근하기 위함이다.

따라서, 개발자가 작성한 코드는 바이트 코드로 변환되며 변환 된 바이트 코드를 통해 작동하는 것이다.

V8 엔진 로고

스코프(Scope)

JS 에서의 스코프(Scope)는 일종의 유효범위이며, 함수를 선언했을 때 생성된다. 스코프는 참조 대상 식별자( identifier, 변수, 함수, .. 등 )를 포함하며, 식별자의 접근성 및 생존 기간을 제어할 수 있다. JS는 일반적으로 함수 레벨 스코프(Function-Level-Scope)를 사용하며, ES6문법을 사용해 선택적으로 블록 레벨 스코프(Block-level scope)를 사용할 수 있다. JS 에서 스코프는 크게 전역 스코프와 지역 스코프 그리고 렉시컬 스코프로 나뉜다.

 

함수 레벨 스코프(Function-Level-Scope)

함수 레벨 스코프는 최 상단 전역 스코프와 함수 선언을 기준으로 스코프가 생성된다.

 

전역 스코프(Global Scope)

전역 스코프는 코드 어디에서든지 참조할 수 있다.

// 전역 스코프
var test = 'GO'

// 지역 스코프
function testFunction ( ) {
	console.log( '지역 스코프 test: ', test )
}
// 지역 스코프 종료

testFunction() // -> 지역 스코프 test: GO
console.log( '전역 스코프 test: ', test ) // -> 전역 스코프 test: GO
// 전역 스코프 종료

위 코드에서 변수 test를 전역 스코프에 선언했다. tesetFunction 함수 선언부에서 지역 스코프가 생성 되었고, 상위 스코프의 변수 test를 참조해 'GO' 를 출력했다.

 

지역 스코프(Local Scope)

지역 스코프는 지역 또는 하위 스코프에서 접근이 가능하다.

// 전역 스코프
var test = 'GO'

// 지역 스코프
function testFunction ( ) {
	var test = 'AO'
	console.log( '지역 스코프 test: ', test ) // -> 지역 스코프 test:  AO
}
// 지역 스코프 종료

testFunction() 
console.log( '전역 스코프 test: ', test ) // -> 전역 스코프 test:  GO
// 전역 스코프 종료

위 코드에서 변수 test를 전역 스코프에 선언했다. 그리고 tesetFunction 함수 선언부에서 지역 스코프가 생성 되었고, 변수 test를 재 선언 했다. 변수 test를 재 선언 했기 때문에, 실행 결과는 'AO', 'AO' 가 출력되야할 것 같은데 'AO', 'GO'가 출력됐다. 이러한 현상이 바로 스코프로 인해 참조 대상을 식별했기 때문이다.

 

렉시컬 스코프(Lexical Scope)

렉시컬 스코프란 함수가 선언되는 위치에 따라, 상위 스코프가 결정되는 스코프를 말한다. 

// 전역 스코프
var test = 'GO'

// 지역 스코프
function testFunction1 ( ) {
    var test = 'AO'
	testFunction2()
}
// 지역 스코프 종료
// 지역 스코프
function testFunction2 ( ) {
    console.log( '렉시컬 스코프 test: ', test )
}
// 지역 스코프 종료

testFunction1() // -> 렉시컬 스코프 test: GO
testFunction2() // -> 렉시컬 스코프 test: GO
// 전역 스코프 종료

위 코드에서 지역 스코프 testFunction1에 변수 test를 선언했다. 하지만 실행 결과는 모두 'GO'를 출력했다. 스코프는 함수 선언 시 생성된다. 따라서, 함수 testFunction2가 생성될때 지역 스코프 testFunction2에 test 변수가 없어 전역 스코프의 변수 test를 참조한 것 이다.

 

블록 레벨 스코프(Block-Level-Scope)

블록 레벨 스코프는 ES6문법 중 let, const를 이용해 사용할 수 있다. 호이스팅(hoisting)에 의해 let, const 변수의 유효 범위가 변경되어 블록 레벨 스코프를 사용할 수 있는것.

// 전역 스코프

// 지역 스코프
function testFunction1 ( ) {
	// 블록 레벨 스코프
	for( let i=0; i<4; i++ ) {
		console.log( 'i: ', i ) // i: 0, 1, 2, 3
	}
	// 블록 레벨 스코프 종료
	console.log( 'can i? ', i ) // Error: i is not defined
}
// 지역 스코프 종료

// 지역 스코프
function testFunction2 ( ) {	
	for( var i=0; i<4; i++ ) {
		console.log( 'i: ', i ) // i: 0, 1, 2, 3
	}
	console.log( 'can i? ', i ) // can i? 4
}
// 지역 스코프 종료

testFunction1()
testFunction2()
// 전역 스코프 종료

실행 컨텍스트(Execution Context)

실행 컨텍스트(Execution Context)란 실제 코드 실행에 필요한 정보를 갖는 객체이다. 코드가 실행될 때 JS 엔진에 의해 생성된다. ECMAScript에서는 실행 컨텍스트를 '실행 가능한 코드를 형상화하고 구분하는 추상적인 개념' 으로 정의하고 있으며, JS엔진은 정보의 형상화 및 구분을 목적으로 컨텍스트를 물리적 객체의 형태로 관리한다.

실행 컨텍스트가 갖는 정보는 다음과 같다.

실행 컨텍스트 객체 구조

 

VO(Variable Object)

실행 컨텍스트가 생성되면 JS 엔진은 실행에 필요한 정보를 담을 객체를 생성한다. 이를 VO(Value Object) 라고 한다.

VO는 아래의 정보를 갖으며, 코드가 실행될 때 JS 엔진에 의해 참조된다. 따라서 코드에서의 접근이 불가하다.

  • 변수
  • 매개변수, 인수정보
  • 함수 선언(함수 표현식은 제외)

VO는 실행 컨텍스트의 스코프에 따라 바라보는 대상이 다르며, 크게 전역 스코프 (GO Global-Object), 지역 스코프(AO Active-Object)로 나뉜다.

// Global object
var test = 'GO'

// Active object
function firstFunction ( ) {
	var test = 'First AO'
	console.log( 'Active object test: ', test )
}
// Active object 종료

// Active object
function secondFunction ( ) {
	var test = 'Second AO'
	console.log( 'Active object test: ', test )
}
// Active object 종료

firstFunction()
secondFunction()

console.log( 'Global object test: ', test )
// Global object 종료

 

전역 컨텍스트(Global Object)

전역 컨텍스트는 실행 컨텍스트의 최 상위에 위치하며 전역 객체 GO(Global Object)를 가리킨다. 전역 객체는 전역에 선언된 변수 및 함수를 속성으로 갖고있다. 전역 컨텍스트의 경우 함수 컨텍스트와 달리 매개변수 및 인수 정보가 없다.

전역 컨텍스트(Global Context)

 

함수 컨텍스트(Active Object)

함수 컨텍스트는 현재 활성객체 AO(Active Object)를 가리킨다. 활성 컨텍스트는 전역 컨텍스트와 달리 매개변수와 인수들의 정보가 필요하다. 따라서 매개변수와 인수들의 정보를 담는 arguments object가 추가된다.

함수 컨텍스트(Activive Object)

 

SC(Scope Chain)

스코프 체인(Scope Chain)은 속해있는 컨텍스트가 참조할 수 있는 래퍼런스를 담고 있는 리스트 이다. 현재 속해있는 실행 스코프의 AO를 시작으로, 상위 컨텍스트 및 전역 컨텍스트를 가리키고있다.

스코프 체인

스코프 체인은 Varable Lookup에 사용되며, 어떤 변수를 만났을때 스코프 체인에 담겨져 있는 순서대로 검색한다.

즉, AO(firstFunction)에서 변수 test를 찾아보고 AO에 없다면 스코프 체인을 통해 상위 컨텍스트에 접근해 변수 test를 찾게된다.

 

this

this는 함수 호출 방식에 의해 어떤 객체가 바인딩 될지 동적으로 결정된다.

함수를 호출하는 방식은 크게 아래 경우를 볼 수 있다.

 

함수 호출

일반 함수 실행 방식으로 함수를 실행 시, this는 호출한 대상의 상위를 가리킨다.

function firstFunction ( ) {
	console.log( 'this: ', this ) // test: window object
}

firstFunction() 

 

메소드 호출

메소드로 정의된 함수를 호출한다. 이 경우 this는 메소드의 상위를 가리킨다.

var obj = {	
	firstFunction: function ( ) {
		console.log( 'this: ', this ) // this: {firstFunction: f}
	}
}

obj.firstFunction()

 

화살표 함수 호출

화살표 함수를 통해 호출한 경우 this는 자신의 상위가 가리키는 this를 가리킨다.

var obj = {	
	firstFunction: ( ) => {
		console.log( 'this: ', this ) // test: window object
	}
}

obj.firstFunction()

 

생성자 함수 호출

함수를 호출하는 방법 중 new 키워드를 통해 생성자 함수로 만들어 사용 할 수 있는데, 이런 경우 this는 빈 객체가 된다.

이는 생성자 함수의 특징 중 하나이다.

var test = 'GO test'

function firstFunction ( ) {
	console.log( 'this: ', this.test ) // this: undefined
	this.test = 'new test'
	console.log( 'this: ', this.test ) // this: new test
}

new firstFunction() 

위 코드에서 firstFunction이 생성되었을 때, this는 빈 객체를 바라보고 this에 test 속성을 추가해 'new test'를 출력하는것.

 

apply, call, bind 호출

appll, call, bind 호출은 this객체를 명확하게 지정할 수 있다. 호출 시, 첫 번째 인자로 this 객체를 전달할 수 있기 때문.

var test = 'GO test'

function firstFunction ( ) {
	console.log( 'this: ', this.test )
}

var testScope = {
	test: 'block this'
}

firstFunction.call( testScope ) // this: block this

호출스택(Call stack)

호출 스택은 여러 함수를 호출하는 JS에서 순서를 추적하는 매커니즘이다. 현재 어떤 함수가 동작하고, 그 함수 내에서 실행되는 중첩 함수가 있는지, 다음 실행 함수는 무엇인지 등을 제어한다. JS는 싱글 스레드 언어이기 때문에 하나의 호출스택을 갖으며, 스택 자료구조를 사용한다.

function thirdFunction ( ) {
	console.log( 'thirdFunction!' )
}

function secondFunction ( ) {
	thirdFunction()
	console.log( 'secondFunction!' )
}

function firstFunction ( ) {                
	secondFunction()
	console.log( 'firstFunction!' )
}

// thirdFunction!
// secondFunction!
// firstFunction!
firstFunction()

호출스택

위 코드의 호출스택은 이와 같다. 먼저 실행된 firstFunction 컨텍스트가 스택에 쌓이고 뒤이어 호출된 컨텍스트가 차례로 스택에 쌓인다. 더 이상 호출할 컨텍스트가 없다면, 마지막에 들어온 컨텍스트부터 처리하게 된다. 하지만, 싱글 스레드 언어 기반으로 하나의 호출 스택을 사용하다 보니 문제가 있었다. thirdFunction를 처리하는데 많은 시간이 소요된다면 다른 함수 실행에 지장이 있기 때문이다.

 

이벤트 루프(Event Loop)

싱글 스레드 기반의 단점을 극복하고자 JS는 이벤트 루프라는 매커니즘과 비동기 콜백을 사용한다. 이는 싱글 스레드를 비동기 방식을 통해 효율적으로 사용할 수 있게끔 해준다.

function test ( ) {
	let param = {}

	// post request
	http.post( 'url', param ).then( res => {
		// response
		// 조회 완료 후 액션
	} )
}

test()

위 코드에서 test 함수는 url으로 request를 보냈다. 무언가 처리를 하고 response가 오기까지 10초가 걸렸다면, 10초 동안 다른 동작을 하지 못했을 것이다. 이와 같은 상황을 극복하고자 JS는 이벤트 루프라는 매커니즘과 비동기 콜백을 사용한다.

1. 전역 컨텍스트, test 함수에 대한 지역 컨텍스트가 생성된다.

2. test 함수가 호출되어 호출 스택에 test 함수가 쌓인다.

3. 더 이상 호출할 컨텍스트가 없어, test 함수가 실행된다.

4. 실행 후, 백그라운드 영역으로 이동한다.

5. http.post request에 대한 response를 받아 콜백 함수가 태스크 큐에 전달된다.

6. 이벤트 루프는 호출 스택이 비어있을때 까지 대기하다가 호출 스택이 비어있게되면 태스크 큐를 호출 스택에 쌓는다.

7. 호출 스택에 쌓인 test 함수의 콜백 함수를 실행한다.