코어 자바스크립트 #2 실행 컨텍스트

신윤철·2022년 1월 16일
2

코어자바스크립트

목록 보기
2/8
post-thumbnail

※chapter1과 다르게 이번엔 var대신 let을 사용하면 문제가 되는 부분이 많습니다. let의 특성상 재선언이 불가능하여 Hoisting같은 개념을 설명하는데 어려움이 있어 우선 var을 사용하고 let과의 차이는 다른 포스팅(여기)에서 설명하겠습니다.※

실행 컨텍스트

  • 실행할 코드에 제공할 환경들을 모아놓은 객체
  • 실행 컨텍스트를 배우며 호이스팅(Hoisting),
    외부환경정보(VariableEnvironment, LexicalEnvironment),
    This 객체를 함게 배우시게 됩니다.
  • 클로저를 지원하는 대부분의 언어는 실행 컨텍스트와 유사하거나 동일한 개념을 갖고 있어, 확실히 이해하면 큰 도움이 될것입니다.

실행 컨텍스트란?

  • 앞서 실행 컨텍스트는 실행할 코드에 제공할 환경정보를 모아놓은 객체라 하였습니다.

    간단한 메커니즘을 살펴보면

    1. 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성한다.
    2. 이를 콜스택(call stack)에 쌓아올린다.
    3. 가장 위에 쌓여있는 컨텍스트와 관련있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장합니다.
      • 여기서 동일환 환경이란 하나의 실행 컨텍스트를 구성할 수 있는 방법으로 전역공간, eval()함수, 함수 등이 있습니다.
      • 주로 저희가 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것 입니다.

    예제 함수를 통해 컨텍스트가 콜스택에 쌓이는 구조를 살펴보겠습니다.

    • 컨텍스트는 함수를 생성할 때가 아닌 호출할 때 콜스택에 담깁니다.
    • 콜스택의 가장 위에 담긴 함수를 가장 우선으로 실행합니다. (코드 실행 중 새로 쌓이는 스택이 있다면 중지하고 새로 쌓인 스택을 실행)
    • 쌓인 컨텍스트는 해당 함수가 실행되면 제거됩니다.
      (전역 공간은 코드가 종료될때 제거)

이렇게 컨텍스트가 콜스택에 쌓이는 순서와 실행 순서를 간단히 알아봤습니다.

다음으로는 앞서 "컨텍스트를 실행할때엔 코드를 실행하기 위해 필요한 환경 정보들을 모은다" 했던 부분을 살펴보겠습니다.

환경 정보

  • VariableEnvironment : 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보.
    선언 시점의 LexicalEnvironment의 스냅샷으로, 변경 사항이 반영되지 않음.
  • LexicalEnvironment : 처음(선언시)에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영됨
  • ThisBinding : This 식별자가 바라봐야 할 대상 객체

VariableEnvironment

VariableEnvironment는 실행 컨텍스트를 생성할때 같이 생성됩니다.
내부는 environmentRecord(식별자 저장), outer-EnvironmentReference(외부환경정보)로 구성되어 있습니다.

VariableEnvironment의 매커니즘을 살펴보겠습니다.

  • 실행 컨텍스트를 생성할 때 VariableEnvironment에 정보(식별자, 외부환경)를 먼저 담습니다.
  • 이 정보를 그대로 복사해서 LexicalEnvironment를 만들고 이후에는 LexicalEnvironment를 사용합니다.
    ※ LexicalEnvironment는 변경사항이 실시간으로 반영되기 때문에

LexicalEnvironment

VariableEnvironment의 스냅샷을 통해 만들어지기 때문에
LexicalEnvironment도 environmentRecord, outer-EnvironmentReference로 구성됩니다.

즉, 컨텍스트를 구성하는 함수에 저장된 매개변수 식별자, 선언된 함수, 선언된 변수 등이 식별자정보(environmentRecord)에 해당합니다.
-> 자바스크립트 엔진은 컨텍스트 내부 전체를 처음부터 끝까지 탐색하며 순서대로 식별자를 수집합니다.

  • 자바스크립트 엔진이 컨텍스트를 탐색하며 식별자를 수집하는 것과 컨텍스트가 실행되는 것은 별개의 과정입니다.
  • 즉, 코드가 실행하기 전임에도 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 식별자를 모두 알고 있게 되는 것 입니다.
  • 즉, 자바스크립트 엔진은 "식별자들을 최상단으로 끌어올린다음 식별자를 수집하고 실제 코드를 실행한다" 생각할 수 있고, 여기서 호이스팅(Hoisting)이라는 개념이 등장합니다.

호이스팅(Hoisting)

  • 컨텍스트 내에서 변수의 선언부분과 할당부분 중 선언부분을 끌어올려 식별자를 먼저 판별하는 개념입니다.
  • '끌어올리다'라는 의미를 가진 호이스팅은 변수 정보를 수집하는 과정을 더욱 쉽게 이해하기위해 만들어진 가상의 개념입니다.
    • 자바스크립트 엔진이 실제로 변수들을 끌어올리지 않지만 편의상 끌어올린것으로 간주한것
    • 가상의 개념인 만큼 아래의 예제는 이해를 돕기 위한 예시일뿐 실제 자바스크립트 엔진의 동작 과정은 아닙니다.

호이스팅의 예제를 살펴보겠습니다.

// 호이스팅 예제(원본코드)
function hoist(x){	// environmentRecord 수집대상 1(매개변수)
  console.log(x);	// (1)
  var x;		// environmentRecord 수집대상 2(변수)
  console.log(x);	// (2)
  var x = "abc";		// environmentRecord 수집대상 3(변수)
  console.log(x);	// (3)
}
hoist(100);

만약 위의 코드를 실행하면 어떤 값이 나올까요?
논리적으로 생각해보면 (1) = 100, (2) = undefined, (3) = "abc" 가 호출될 것 같습니다.

실제론 어떤 결과가 나오는지 살펴보겠습니다.

// 매개변수를 변수 선언/할당과 같다 간주하고 변환 (호이스팅을 보기 좋게 하기 위한 작업)
function hoist(){
  var x = 100;		// environmentRecord 수집대상 1(매개변수를 변수 선언/할당으로 표현)
  console.log(x);	// (1)
  var x;		// environmentRecord 수집대상 2(변수)
  console.log(x);	// (2)
  var x = "abc";		// environmentRecord 수집대상 3(변수)
  console.log(x);	// (3)
}
hoist();

매개변수와 전달받은 인자를 함수내에서 변수의 선언과 할당으로 대체하여 표현하였습니다.
Hoisting을 좀더 명확하게 표현하기 위한 과정이기도 하지만 LexicalEvironment 관점에서도 이러한 표현은 문제가 되지 않습니다.

이제 호이스팅을 진행하고 결과를 살펴보겠습니다,

// 매개변수와 변수의 호이스팅을 마친 상태
function hoist(){
  var x;		// 수집 대상 1의 변수 선언 부분
  var x;		// 수집 대상 2의 변수 선언 부분
  var x;		// 수집 대상 3의 변수 선언 부분	
  			// Hoisting 과정에서 environmentRecord는 함수내의 모든 식별자를 파악할 수 있습니다.

  x = 100;		// 수집 대상 1의 할당 부분
  console.log(x);	// (1)
  console.log(x);	// (2)
  x = "abc";		// 수집 대상 3의 할당 부분
  console.log(x);	// (3)
}
hoist();
    1. 함수(같은 컨텍스트)내의 모든 선언부를 최상위로 끌어올립니다.
    1. 끌어 올린 식별자들을 저장할 공간을 메모리에 확보합니다.
    1. x에 100을 할당합니다. 그러면 (1)과 (2)에는 100이 출력됩니다.
    1. x에 할당된 값을 "abc"로 재할당 합니다. 그러면 (3)에는 "abc"가 출력됩니다.
  • 함수의 모든 코드가 실행 되어 실행 컨텍스트가 콜스택에서 제거되고 종료됩니다.

앞서 예상한 값은 (1) = 100, (2) = undefined, (3) = "abc" 였는데,
실제 실행된 값은 (1) = 100, (2) = 100, (3) = "abc" 가 됐습니다.
이는 호이스팅을 배우지 못했다면 이해하기 어려운 결과입니다.

하나의 예제로는 호이스팅을 완전히 이해하기 힘들 수 있으니 다른 예제를 살펴보겠습니다. (여러 종류의 식별자 사용)

//원본코드
function hoist(){
  console.log(x);	// 출력(1)
  var x = 100;		// 수집 대상(1) (식별자)
  console.log(x);	// 출력(2)
  function x() { };	// 수집 대상(2) (함수선언)
  console.log(x);	// 출력(3)
}
hoist();

만약 위의 코드를 실행하면 어떤 값이 나올까요?
순차적으로 생각하면 (1) = undefined, (2) = 100, (3) = 함수 x 가 출력될 것 같습니다.

그럼 실제론 어떤 값이 나올지 과정을 살펴보겠습니다.

//호이스팅을 마친 상태
function hoist(){
  var x;		// 수집 대상(1) (식별자)
  function x() {}; 	// 수집 대상(2) (함수선언)

  console.log(x);	// 출력(1)
  x = 100;		// 수집 대상 (1)의 할당 부분
  console.log(x);	// 출력(2)
  console.log(x);	// 출력(3)
}
hoist();

호이스팅을 마치고 순차적으로 코드를 진행해보겠습니다.
1. 변수 x를 선언하고 메모리에 저장할 공간을 마련합니다.
2. x 함수가 선언되며 별도의 메모리에 함수가 저장되고, 함수가 저장된 메모리의 주소값이 변수 x에 담깁니다. (함수 선언문에서는 함수명이 곧 변수명입니다.)
3. 출력(1)에는 변수 x에 저장된 함수 x가 출력됩니다.
4. 변수 x에 다시 100을 재할당 합니다.
5. 출력(2),(3)에는 x의 값, 즉 100이 출력됩니다.

처음에 예상했던 (1) = undefined, (2) = 100, (3) = 함수 x와 완전 다른 결과인
(1) = 함수 x, (2) = 100, (3) = 100 이 출력되었습니다.
이처럼 호이스팅을 알지 못하면 코드에서 예상과 다른 값이 도출될 수 있으므로 개념을 잡는것이 중요합니다.

함수 선언문과 함수 표현식

호이스팅하는 과정에서 헷갈릴 수 있는 함수 선언문과 함수 표현식의 차이를 알아보겠습니다.
둘 모두 함수를 새롭게 정의할 때 쓰이는 방식입니다.

  • 함수 선언문 : 별도의 변수 없이 function의 정의부만 존재하고 할당 명령은 없습니다.
  • (익명) 함수 표현식 : 정의한 function을 별도의 변수에 할당하는 방법입니다.
  • 기명 함수 표현식 : 기명 함수 표현식은 function을 정의할 때 반드시 함수명을 명시해야하고, 별도의 변수에 할당하는 방법입니다.
    • 기본적으로 함수 표현식은 익명 함수 표현식을 의미합니다.
	function a () { ~~~ }		// 함수 선언문, 함수명 a가 곧 변수명
    	a();				// 함수 실행 
    	
      	var b = function () { ~~~ } 	// (익명) 함수 표현식, 변수명 b가 곧 함수명
        b(); 				// 함수 실행
                             
        var c = function d () { ~~~ } 	// 기명 함수 표현식, 변수명은 c, 함수명은 d
        c();				// 함수 실행
        d();				// 에러. 
                            		// (기명 함수 표현식은 외부에서 함수명으로 함수를 호출할 수 없음.)

기명 함수 표현식은 과거 익명 함수 표현식에서 함수명을 출력할 시 undefined, nonamed 등 함수명이 잘 출력되지 않아 사용했었습니다.
하지만 최근 브라우저들은 익명 함수 표현식의 변수명을 함수의 name 프로퍼티에 잘 할당하여, 굳이 변수명과 함수명을 다르게 사용하는 기명 함수 표현식은 이제 사용하지 않습니다.

이제 함수 표현식과 함수 선언문이 호이스팅 과정에서 어떤식으로 차이가 발생하는지 살펴보겠습니다.

// 함수 선언문과 함수 표현식의 Hoisting - 원본코드
console.log(sum(10, 20));	// 출력 (1)
console.log(mul(5, 4));		// 출력 (2)

function sum(a, b) {		// 함수 선언문
  return a + b;
}

var mul = function (a, b) {	// 함수 표현식
  return a * b;
}

원본 코드만 봐선 결과값이 잘 예측되지 않습니다.
그럼 이제 함수 선언문과 함수 표현식의 호이스팅 과정을 살펴보겠습니다.

// 함수 선언문과 함수 표현식의 Hoisting - Hoisting 이후 상태
function sum(a, b) {		// 함수 선언문 (함수 전체가 Hoisting 됨)
  return a + b;
}
var mul;			// 함수 표현식의 변수 선언부, 변수 선언부만 호이스팅됨

console.log(sum(10, 20)) 	// 출력 (1)
console.log(mul(5, 4))		// 출력 (2)

mul = function (a, b) {		// 함수 표현식의 할당 부분
  return a * b;
}
  • 함수 선언문은 함수 전체를 호이스팅한 반면에 함수 표현식은 변수의 선언부만 호이스팅 하였습니다.
  • 이는 함수 선언문과 함수 표현식의 가장 큰 차이이며,
    때문에 출력 (1) = 30, 출력 (2) = mul is not a function 이란 에러가 발생합니다.

위 예제 처럼 함수 선언문은 함수 전체가 Hoisting된다는 특징이 있습니다.

간단한 코드에서는 별 문제가 되지 않지만, 코드가 길어지면 함수 선언문의 특징은 결과에 큰 문제를 야기할 수 있습니다.

// 함수 선언문의 위험성 (같은 컨텍스트에서 코드 작성 시)
console.log(sum(10,20));	// 출력 (1)

function sum(a, b) {
  return a+b;
}

console.log(sum(30,40));	// 출력 (2)

function sum(a,b) {
  return "a + b = " + (a + b);
}

만약 출력 (1)에는 a+b의 값을, 출력 (2)에는 a + b = (a+b) 라는 형식의 값을 출력하고 싶어 위처럼 코드를 작성하면 결과가 어떻게 될까요?

// 함수 선언문의 위험성 (같은 컨텍스트에서 코드 작성 시) (호이스팅을 마친 상태)
function sum(a, b) {
  return a+b;
}
function sum(a,b) {
  return "a + b = " + (a + b);
}

console.log(sum(10,20));	// 출력 (1)
console.log(sum(30,40));	// 출력 (2)
  • 의도와는 다르게 출력 (1)과 (2) 모두 a + b = (a+b) 형식의 값을 출력하게 됩니다.

이는 컨텍스트가 활성화 될때 해당 컨텍스트의 함수를 모두 끌어올리게 되면서 sum 함수가 중복 선언되고, 마지막 sum함수의 값이 override되었기 때문입니다.

정리하자면 함수 선언문으로 정의된 동일한 함수명에 서로 다른 값을 할당할 경우 마지막에 선언된 함수의 값으로 할당되게 됩니다.(같은 컨텍스트 내에서)

※ 코드가 길어지면 함수 선언문으로 정의된 함수는 이러한 문제를 야기할 수 있고, 에러도 나타나지 않아 고치기 매우 어렵습니다.
때문에 함수 표현식을 사용하길 권장합니다.

스코프

스코프란 식별자에 대한 유효범위(접근 가능한 범위)입니다.

어떤 경계 A(함수, 블록)의 외부에서 선언한 변수는 A의 외부뿐만 아니라 A의 내부에서도 접근 가능하지만, A의 내부에서 선언된 변수는 오직 A의 내부에서만 접근 가능합니다.

var a = 10;
function b(){
  var c = 20;
  console.log(a);		// 10 출력
  console.log(c);		// 20 출력
}
console.log(a);			// 10 출력
console.log(c); 		// undefined 에러
  • 위 예제에선 어떤 경계(함수 b)의 외부에서 선언한 변수 a는 함수 b의 외부에서도 접근가능하고, 내부에서도 접근 가능합니다.
  • 하지만 함수b 내부에서 선언된 변수 c는 함수 b에서만 접근 가능하고, 경계 외부에서는 접근할 수 없습니다.

ES5까지는 스코프가 오직 함수에 의해서 생성되었습니다.
하지만 ES6부터 경계를 블록과 함수 두가지로 구분하여 블록 스코프, 함수 스코프로 나뉘었습니다.

  • var은 블록 스코프에는 접근하지 못하고, ES6부터 등장한 let, const, class 등이 접근 가능합니다.

스코프체인

스코프로 인해 분리된 '식별자의 유효범위'(접근 가능한 범위)를 내부에서부터 외부로 차례대로 검색해 나가는 것을 스코프 체인이라 합니다.

  • 스코프 체인은 LexicalEvironment의 두번째 수집 자료인 outerEnvironmentReference를 사용하여 내부에서 외부로 식별자를 검색할 수 있습니다.
  • outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEvironment를 참조합니다.

이렇게 설명하면 어려울 수 있어 예제를 통해 살펴보겠습니다.

앞서 "outerEnvironmentReference는 현재 호출된 함수가 선언될 당시"라는 말을 풀어 써보면 함수가 실행될 당시라 할 수 있습니다.
(선언이 된다는 것은 콜스택에 쌓인 컨텍스트가 실행될 때이고, 함수는 호출될 때 실행되기 떄문에)

따라서 위 코드를 보면 10번째 줄에서 outer함수를 실행할 때 전역 공간의 lexicalEvironment를 참조하게 되고,
7번째 줄에서 inner함수를 실행할 때 outer함수의 lexicalEvironment를 참조하게 됩니다.

즉 전역 컨텍스트의 environmentRecord인 식별자 a, outer은 outer컨텍스트의 outerEnvironmentReference에 참조되고 때문에 outer 함수에 변수 a가 없어도 8번째 줄에서 a = 1이 출력 될 수 있었습니다.

그럼 outer의 LexicalEnvironment를 참조한 inner 컨텍스트에서도 외부에서 선언된 a를 사용할 수 있지 않을까요?

결론적으로 말하자면 외부에서 선언되었기 때문에 사용가능합니다.
하지만 예제에서 undefined값이 나온 이유는 무엇일까요?

스코프 체인은 외부에서 선언된 변수를 사용할 수 있지만, 우선적으로 내부에서 선언된 변수를 사용하기 때문입니다.

  1. inner 함수 내에 5번째 줄에 선언된 var a = 3;이 먼저 호이스팅되어 Inner environmentRecord에 저장됐기 때문에
  2. 외부 공간에서 참조한 a가 아닌 inner에 선언된 a를 사용하게 됩니다.
  3. 그런데 a에 할당된 값이 없어 undefined이 출력된 것 입니다.
profile
기본을 탄탄하게🌳

0개의 댓글