본문 바로가기

Web/javascript

javascript - 실행컨텍스트와 Hoisting(호이스팅) 그리고 ScopeChaning(스코프 체이닝)

호이스팅과 실행컨텍스트

이번 포스팅은 Javascript의 Execution Context(실행컨텍스트)`Hoisting(호이스팅) 그리고 Scope Chaining(스코프체이닝)에 대해 알아보도록 하겠습니다. 실행 컨텍스를 이해해야지만, 이 포스팅에 담기는 내용들인 Scope Chaininghoisting에 대한 이해가 수월합니다.

 

이번 포스팅역시 코어자바스크립트라는 책을 기반으로 작성되었습니다.

 

 

 

1. 실행 컨텍스트(Execution Context)란?

javascript의 Execution Context(이하 실행컨텍스트)는 code 실행시 필요한 정보들(필요한 환경들)을 모아놓은 객체 입니다.

 

javascript는 코드를 실행하며, 필요한 환경정보들(ex. 변수, 함수 등..)들을 모아 이를 이용해 실행컨텍스트를 만들고, 이를 콜스택에 쌓습니다. 그후, 코드를 실행하며 필요한 실행컨텍스트를 사용하고, 이를 스택에서 제거하는 식으로 코드의 환경과 순서를 보장합니다. 아직 잘 이해가 안가셔도 괜찮습니다. 아래에서 조금더 자세히 살펴보도록 하겠습니다.

 

 

 

1.1 실행컨텍스트가 생성되는 경우

실행 컨텍스트는 크게 3가지 상황에서 생성이 됩니다.

  1. javascript 실행 시작시 (전역컨텍스트)
  2. eval() 함수
  3. 함수 실행시

 

전역컨텍스트는 자동으로 생성이 되고, eval()은 여러 관점이 존재하지만, 많은 곳에서 악마로 취급을 받습니다. 때문에 우리가 실행컨텍스트를 구성하는 방법은 함수를 실행하는 것이 유일합니다. 다시 한번 강조드리자면, 함수가 선언되는 시점이 아닌, 함수가 실행(호출)되는 시점입니다.

 

 

 

 

1.2 실행컨텍스트와 코드의 실행과정

코드가 실행되며 실행 컨텍스트가 어떤식으로 적용되는지는 예제를 살펴보며 알아보도록 하겠습니다. 실행컨텍스트에 어떤 정보가 들어가는지 등은, 잠시뒤 설명드릴 예정입니다. 지금은 실행컨텍스트가 언제 생성되고 콜스택에 쌓이고 제거되는지만을 집중해서 봐주세요.

 

  1. (1)에서 전역컨텍스트가 생성되고, 이를 콜스택에 쌓습니다. (전역컨텍스트는 일반 컨텍스트와 별다를 바가 없습니다. 단지 코드 최상단의 전역적인 코드의 환경이기 때문에 붙혀진 이름입니다.)
  2. 다시 코드를 실행하며 (3)에서 outer()가 호출되고, outer() 함수 실행을 위한 실행컨텍스트가 생성되고, 콜스택에 쌓입니다.
  3. outer() 함수가 실행되고 코드가 실행되다가, (2)에서 inner() 함수가 호출되어, inner() 함수 실행을 위한 실행컨텍스트가 생성되고, 콜스택에 쌓입니다.
  4. inner() 함수안에서 a를 콘솔에 출력하고 나면 실행할 코드가 더이상 없기 때문에, inner()가 콜스택에서 제거됩니다.
  5. 그 다음 콜스택의 최상단에 위치한 outer() 함수가 다시 실행되면서, 9번째 줄을 실행하고 a를 콘솔에 출력하고, outer() 함수도 콜스택에서 제거됩니다.
  6. 콜스택 최상단에 위치한 전역컨텍스트가 다시 실행되며, 12번째줄에서 a를 다시 콘솔에 출력하고 종료됩니다.

 

 

 

1.3 실행컨텍스트의 정보

실행컨텍스트에 담기는 내용은 위 그림과 같습니다. 아래에서 조금더 자세히 설명드리겠습니다.

 

1.3.1 VariableEnvironment

최초 컨텍스트 생성시 VariableEnvironment에 모든 정보가 담기고 이를, LexicalEnvironment에서 복사하여 사용합니다. 즉, LexicalEnvironment의 스냅샷으로, 실행중에도 변경사항이 반영되지 않습니다. 지금은 중요하지 않으므로, LexicalEnvironment를 살펴보겠습니다.

 

 

1.3.2 LexicalEnvironment

LexicalEnvironment에는 다시 크게 environmentRecordouterEnvironmentReference가 있습니다. LexicalEnvironment에 담기는 내용은 아래  2.에서 더 자세히 다루도록 하겠습니다.

 

 

1.3.3 ThisBinding

ThisBinding에는 실행컨텍스트가 생성될 당시 this로 지정된 객체가 저장이 됩니다. 함수와 같이 this에 아무런 객체가 저장되지 않는 경우에는, 전역객체가 저장이 됩니다.

 

 

 

 

 

2. LexicalEnvironment와 Hoisting 그리고 ScopeChaning

LexicalEnvironment의 구성은 environmentRecord, outerEnvironmentReference로 이루어져 있다고 앞서 말씀드렸습니다. 이 각각의 데이터가 어떻게 수집되고 이용되는지를 파악한다면, HoistingScopeChaining에 대한 개념 역시 수월하게 파악하실 수 있습니다.

 

 

2.1 environmentRecord와 Hoisting(호이스팅)

environmentRecord에는 현재 실행컨텍스트와 관련된 코드의 식별자 정보, 함수정보들이 저장됩니다. javascript 엔진은 컨텍스트 내부의 모든 식별자를 environmentRecord에 저장한뒤 코드를 실행하게 됩니다. 아래에는 저장되는 식별자 정보들의 예입니다.

  1. 함수의 매개변수 식별자
  2. 선언된 함수자체
  3. let, const등으로 선언된 변수의 식별자

 

 

Hoisting(호이스팅)

environmentRecord에 저장될 정보들은 코드 실행전, 컨텍스트 내부를 쭉 훑으며 순서대로 수집을 합니다. 각각의 식별자 정보들을 모두 수집한 뒤에도, 코드들은 아직 실행전이기 때문에, javascript엔진은 실행할 컨텍스트의 내부에 있는 모든 식별자들을 코드의 최상단으로 끌어올려 놓은 다음 코드를 실행하는것과 마찬가지로 간주해도 코드를 해석하는데 잘못되는 부분이 발생하지 않습니다.

 

실제로는 변수들을 최상단으로 끌어올리는 등의 코드변화를 일으키지는 않지만 그렇게 간주를하자는 개념이 바로 Hoisting입니다. javascript 엔진이 변수 정보를 수집하고 코드를 실행하는 과정을 더욱 이해하기 쉽게 하기위해 발생된 개념이죠.

 

조금 더 이해를 도와드리기 위해 Hoisting 예제를 살펴보겠습니다.

 

hoisting이 되지 않은 경우

hoisting이 되지 않은 경우의 위 예제의 출력을 예상해본다면 어떨까요? (1)에서는 1이, (2)에서는 undefined, (3)에서는 2가 출력된다고 예상이 됩니다. 하지만 실제 javascript 실행시 이 예상과는 전혀다른 결과값이 나타나게 됩니다.

 

hoisting이 되는 경우

hoisting이 되는 경우에는 8번째 줄에서 함수 a(1)이 호출되면서, 콜스택에 a() 함수의 실행컨텍스트가 쌓이게 됩니다. 실행컨텍스트는 코드 실행전 모든 식별자를 스캔하여 저장한다고 말씀드렸습니다. 때문에 매개변수 x(1), (2), (3)에서 선언된 변수 x는 모두 같은 이름의 식별자이기 때문에, 스캔 후 실행컨텍스트에는 단 하나의 x 식별자 정보가 담겨있게 됩니다. 한줄씩 자세히 살펴보겠습니다.

  1. 매개변수 x에 1을 담아 호출을 했기 때문에, 2번째 줄코드가 실행되기전의 environmentRecord에는 var x = 1이라는 정보가 담겨있습니다. 따라서 (1)에서는 1이 출력됩니다.
  2. environmentRecord에 이미 var x가 존재하기 때문에, 3번째 줄의 var x는 무시됩니다.
  3. (2)에서는 environmentRecord에 존재하는 x식별자의 값인 1이 출력됩니다.
  4. 5번째 줄에서 environmentRecord의 x값을 2로 변경합니다.
  5. (3)에서는 2가 출력됩니다.

 

위 예제코드는 앞선 예제코드가 hoisting이 진행되고난 후의 가상의 코드상황입니다.

 

 

 

2.2 outerEnvironmentReference와 scopeChaining

 

scopeChaning (스코프 체이닝)

scope는 어떤 식별자에 대한 유효범위를 의미합니다. 위와 같은 예제에서 javascript는 함수 c()내부에 a와 b라는 변수가 존재하지 않더라도, 상위 scope (콜스택의 하위에 존재하는 실행컨텍스트들)에 존재하는 변수인 b, a에 접근하여 출력이 가능합니다.

 

어떻게 이러한 접근이 가능할까요? 이는 바로 실행컨텍스트 내부의 LexicalEnvironment의 두번째 수집 자료인 outerEnvironmentReference덕분입니다. 이 outerEnvironmentReference가 어떠한 데이터를 담고있는지를 파악한다면, scopeChaining의 과정을 쉽게 이해하실 수 있습니다.

 

outerEnvironmentReference는 현재 함수가 호출될 당시의 LexicalEnvironment를 참조합니다. 다시말해 콜스택에서 현재 실행컨텍스트 바로 하단에 위치한 LexicalEnvironment를 참조한다는 것 입니다. outerEnvironmentReferenece연결리스트의 형태를 띄고 있습니다. 천천히 예제와 그림을보며 설명드리겠습니다.

 

  1. 16번째 줄에서 a()가 호출되며 function a에 대한 실행컨텍스트가 생성되어 콜스택에 쌓이며, 이 LexicalEnvrionmentouterEnvironmentReference는 호출될 당시의 활성 컨텍스트인 전역컨텍스트의 LexicalEnvironment를 참조합니다.
  1. a() 함수가 실행되다가, 14번째 줄에서 b()가 호출되며 function b에 대한 실행컨텍스트가 생성되어 콜스택에 쌓이고, 이 LexicalEnvrionmentouterEnvironmentReference는 호출될 당시의 활성 컨텍스트인 function a의 LexicalEnvironment를 참조합니다.
  2. b() 함수가 실행되다가 12번째 줄에서 c()가 호출되며 같은 방식으로 c의 실행컨텍스트의 outerEnvironmentReference는 c()가 호출될 당시의 활성컨텍스트인 b의 LexicalEnvironment를 참조하게 됩니다.
  3. c() 함수가 실행되다가 8번째 줄에서 c를 콘솔에 출력하라는 명령을 받고, javascript엔진은 현재 활성화 실행컨텍스트(c)의 environmentRecord에서 식별자 c가 존재하는지를 찾습니다. 존재 하기 때문에, 3을 출력합니다.
  4. 이어 9번째 줄에서는 b를 콘솔에 출력하라는 명령입니다. javascript엔진은 현재 활성화 실행컨텍스트의 environmentRecord에서 식별자 b가 존재하는지를 찾지만 존재하지 않습니다. 때문에 outerEnvironmentReference가 참조하는 상위 컨텍스트(b)environmentRecord에서 식별자 b가 존재하는지를 찾습니다. b를 찾았으니 2를 출력합니다.
  5. 10번째 줄에서는 a를 콘솔에 출력하기 위해 javascript엔진은 environmentRecord에서 식별자 a를 찾고 존재하지 않다면 outerEnvironmentReference를 참조하는 식으로 반복하며 a를 찾아 나갑니다. a 실행컨텍스트에서 식별자 a를 찾아 1을 출력하고 종료됩니다.

 

이처럼 현재 scope에서 식별자를 찾고 존재하지 않는다면 계속해서 상위 scope로 범위를 넓히며 식별자를 찾는 과정을 scopeChaning이라고 합니다.

 

 

scopeChaning의 특징

scopeChaning은 상위로 점차 scope를 넓히며 식별자를 찾아 나아간다고 말씀드렸습니다. 이 때문에 코드 실행시 필요한 식별자가 현재 실행컨텍스트에 존재한다면 이를 반환하고 더이상 상위 scope로 검색을 진행하지 않습니다. 따라서 위의 예제에서 8번째 줄의 결과는 평생을 찍어보아도 3밖에 출력되지 않습니다.

 

즉 여러 scope에서 동일한 식별자가 선언되어 있는 경우, 무조건적으로 현재 scope에서 가장 먼저 발견되는 식별자에만 접근이 가능합니다.