본문 바로가기
  • soldonii's devlog
Javascript 공부/Advanced Javascript(-)

자바스크립트의 실행 컨텍스트와 호이스팅

by soldonii 2019. 8. 28.

*Udemy의 "Advanced Javascript Concepts" 강의에서 학습한 내용을 정리한 포스팅입니다.

*자바스크립트를 배우는 단계라 오류가 있을 수 있습니다. 틀린 내용은 댓글로 말씀해주시면 수정하겠습니다. 감사합니다. :)


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

function printName() {
  return 'Hyunsol'
}

function findName() {
  return printName()
}

function sayMyName() {
  return findName()
}

sayMyName()

 

자바스크립트 엔진은 (), 즉  함수 실행을 보면 "아 내가 무언갈 해야하는구나!"라고 인지하고, 동시에 실행 컨텍스트를 생성한다. 예를 들어, 위 코드에서 sayMyName() 함수를 보자마자 실행 컨텍스트를 생성한 후, 콜 스택의 가장 밑바닥에 삽입한다. 그리고 이 함수를 수행하는 과정에서 필요한 findName() 함수, printName() 함수가 차례차례 콜스택의 밑에서부터 쌓인다.

 

그런데 콜스택에 쌓이는 이 모든 실행 컨텍스트의 가장 베이스에서는 Global Execution Context가 존재한다. 즉 sayMyName() 실행 컨텍스트가 생성되기 이전에 global()이라는 글로벌 실행 컨텍스트가 생성되는 것이다. 그렇기 때문에 하나의 자바스크립트 파일을 로딩할 때, 이 파일 안에서 실행되는 모든 코드들은 사실상 global() 글로벌 실행 컨텍스트 내부에서 각각의 개별적 실행 컨텍스트를 생성하면서 실행되는 것이다.

 

실행 컨텍스트

 

글로벌 실행 컨텍스트 내부에는 global 객체와 this가 포함되어 있다. 하나의 자바스크립트 파일 내에서 선언된 변수, 함수 등은 글로벌 실행 컨텍스트 내부에 존재하는 global 객체의 property가 된다. 이렇게 글로벌 실행 컨텍스트 내부에서 값을 메모리에 할당하는 과정이 코드 실행의 첫번째 phase이다. 두번째 phase는 실제로 코드를 구동시키는 과정이다.

 

2. 렉시컬 환경(Lexical Environment)

렉시컬 환경은 특정 코드가 작성, 선언된 환경을 의미한다. 잘 와닿지 않으니 사례로 보자. 위에서 작성된 코드는 총 4개의 실행 컨텍스트(1개의 글로벌 실행 컨텍스트와 3개의 개별적 실행 컨텍스트)가 생성된다.(아래 그림 참조) 

 

각각의 실행 컨텍스트는 하나의 개별적인 소우주이다. sayMyName() 소우주와 findName() 소우주, printName() 소우주는 가장 큰 global() 우주 안에 속해 있는 3개의 소우주이다.(바꿔말하면, sayMyName 함수, fineName 함수, printName 함수의 렉시컬 환경은 global이다. 해당 함수들은 글로벌 실행 컨텍스트 환경 내에서 작성되었기 때문이다.)

 

만약 findName()이라는 함수 내에 let yourName = 'blahblah'라고 선언되어 있다면, 변수 yourName의 렉시컬 환경은 findName이다.

 

렉시컬 환경

 

그런데 렉시컬 환경을 왜 알아야 하는 것일까?

 

왜냐하면 내가 사용하고자 하는 변수, 함수 등이 어떤 렉시컬 환경에 속해있는지에 따라서 이용 가능한 변수가 달라지기 때문이다. 더 쉽게 말하자면, 어떤 변수나 함수의 값은 이를 '어디에서 호출했는지'가 아니라, '어디에서 선언했는지', 즉 렉시컬 환경이 어디인지에 따라서 결정된다는 의미이다.

 

In javascript, our lexical scope(available data + variables where the function was defined) determines our available variables.
Not where the function is called(dynamic scope)

아래 코드를 다시 보자. findName 함수가 sayMyName 함수 내에서 호출되었는데, sayMyName 함수 내에서 findName 함수가 선언되지 않았기 때문에 해당 함수를 실행할 수 없는 것일까? 그렇지 않다.

 

sayMyName 함수 내에서 findName 함수가 호출되기는 했으나 '어디에서 호출했는지'가 아니라 '어디에서 선언했는지'가 중요하다고 했다. findName 함수는 글로벌 실행 컨텍스트, 즉 global 함수 내에서 선언이 되어있다.(바꿔 말하면, findName 함수의 렉시컬 환경은 글로벌 실행 컨텍스트이며, 글로벌 실행 컨텍스트 내에 존재하는 global 이라는 객체 내의 property로 지정되어 있다. findName 뿐 아니라 printName, sayMyName 함수 모두 global 객체의 property 중 하나이다.)

 

따라서 선언된 장소, 즉 렉시컬 환경이 글로벌 실행 컨텍스트이기 때문에 글로벌 실행 컨텍스트 내에서(=global 객체 내에서) findName 함수를 찾아서 실행하게 된다.(그러면 마찬가지로 printName 함수를 실행해야 하는데, 이 함수 또한 글로벌 실행 컨텍스트에서 선언되어 있기 때문에 해당 함수에 접근할 수 있으며, 따라서 최종적으로 'Hyunsol'을 리턴하게 된다.)

 

function printName() {
  return 'Hyunsol'
}

function findName() {
  return printName()
}

function sayMyName() {
  return findName()
}

sayMyName()

 

3. 호이스팅(Hoisting)

호이스팅

 

모든 실행 컨텍스트들은 2개의 단계로 작업이 이루어진다. 첫번째 단계는 생성 단계(creation phase), 두번째 단계는 실행 단계(execution phase)이다. 위 사진에서 노란색 점선을 기준으로 위쪽이 creation phase, 아래쪽이 execution phase이다.

그리고 생성 단계 내에서 자바스크립트 엔진은 Hoisting이라는 개념을 이용하게 된다.

 

# 호이스팅(Hoisting)이란?

console.log('1--------'); // 1------
console.log(teddy); // undefined
console.log(sing()); // ohh la la la 

var teddy = 'bear';
function sing() {
  console.log('ohh la la la');
}

 

위 코드를 보면, 변수 teddy와 함수 sing을 선언하기 전에 console.log로 호출하였는데, 변수의 경우 undefined, 함수의 경우는 정상적으로 원하는 결과를 출력했다. 이 같은 결과는 자바스크립트 엔진이 변수에 메모리를 할당하는 방식의 차이 때문에 발생한다. 결론적으로는 변수와 함수 모두 호이스팅이 일어난 상황이지만, 다른 방식으로 호이스팅이 일어난 경우이다.

 

// var teddy = undefined;
// function sing() {
//	 console.log('ohh la la la');
// }
console.log('1--------');
console.log(teddy);
console.log(sing());

var teddy = 'bear';

 

자바스크립트 엔진은 var와 function 키워드를 만나면 해당 키워드 뒤에 있는 변수명(또는 함수명)을 스코프의 최상단으로 끌어올려서 선언을 한다. 위 코드에서 주석처리된 부분을 보면, 변수 teddy와 함수 sing이 실행 컨텍스트 내부에서 최상단으로 끌어올려진 것을 볼 수 있다. 이렇게 끌어올리는 행위 자체가 호이스팅이다.

 

그런데 왜 변수는 undefined를 출력하고 함수는 원하는 결과를 출력했을까?

변수는 부분적으로만 호이스팅이 일어나고, 함수는 전부 호이스팅이 되기 때문이다. 변수의 경우 위로 끌어올리면서 변수 값을 undefined로 초기화를 한다. 그리고 실행 컨텍스트의 1단계인 생성 단계가 끝나고 2단계 실행 단계가 진행될 때 undefined로 초기화 된 값을 실제로 선언한 값인 'bear'로 바꾸게 되는 것이다.

 

반면 함수는 아래에서 선언한 함수 내부의 모든 코드들을 전부다 위로 끌어올린다. 전부 호이스팅하는거다. 그렇기 때문에 원하는 값을 그대로 출력할 수 있는 것이다.

 

참고로 아까 위에서 각각의 실행 컨텍스트들은 개별적인 소우주라고 말했다. 여태까지 말한 것들은 각각의 실행 컨텍스트 내부에서 발생하는 일이다. 실행 컨텍스트가 4개라고 하면, 각각의 컨텍스트 내부에서 각각 1번 생성단계, 2번 실행 단계가 진행되는 것이다.


console.log('1--------'); // 1------
console.log(teddy); // ReferenceError
console.log(sing()); // ReferenceError

const teddy = 'bear'; // 또는 let 키워드
(function sing() {
  console.log('ohh la la la');
})

 

만약 var 키워드 대신 let, const를 사용해서 변수를 선언하면 어떻게 될까? 이를 이해하기 위해서는 변수가 메모리에 저장되는 단계를 살펴볼 필요가 있다. (아래 내용 중 일부는 Medium에서 'var, let, const 특징 및 호이스팅' 글의 일부를 참고했다.)

 

변수는 메모리에 저장될 때 크게 3단계를 거친다.

 선언 단계 : 변수를 실행 컨텍스트의 변수 객체에 등록한다.

 초기화 단계 : 실행 컨텍스트에 등록된 변수 객체에 대한 메모리를 할당한다. 이 단계에서 변수는 undefined로 초기화된다.

 할당 단계 : undefined로 초기화 된 변수에 값을 할당한다.

 

var의 경우 ①, 단계가 동시에 진행되기 때문에 스코프의 최상단으로 호이스팅 되면서 동시에 undefined가 값으로 할당된다.

반면 let, const는 단계와 단계가 분리되어 진행된다. ②단계인 초기화 단계는 실제 코드가 실행되는 단계에서 진행된다. 따라서 초기 값이 undefined로 할당된 것이 아니므로 undefined를 출력하는 대신 ReferenceError가 발생한다.

 

const는 let과 유사하지만, const의 경우 반드시 변수를 선언할 때 값도 함께 초기화를 하지 않으면 에러가 발생하는 점이 다르다.

 

참고로 위 코드에서 sing 함수의 경우도 ()로 함수 전체를 묶어 놓았는데, 이 경우 자바스크립트 엔진이 function 키워드를 만나지 못하기 때문에 sing이라는 함수가 메모리로 할당되지 못한 상태이므로 ReferenceError가 발생된다.


console.log(sing2); // undefined
console.log(sing2()); // sing2 is not a function
console.log(sing()); // ohhh la la la

// 1. function expression
var sing2 = function() {
  console.log('uhh la la la');
}

// 2. function declaration
function sing() {
  console.log('ohhh la la la')
}

 

이 코드의 경우 함수 선언을 두가지 방식으로 달리 했다. 첫번째는 함수 표현식, 두번째는 함수 선언식으로 선언했는데, 함수 표현식의 경우 sing2라는 변수에 함수의 값을 담은 경우이다. 따라서 sing2는 변수로 취급되어 자바스크립트 엔진은 이를 호이스팅하면서 동시에 변수 sing2에 undefined를 초기값으로 부여하여 메모리에 저장한다. 따라서 위에서 console.log(sing2)의 경우 undefined가 출력되며, console.log(sing2()) 또한 현재는 함수가 아니라 undefined인 값에 함수 호출을 시키기 때문에 당연히 함수가 실행되지 않고 대신 sing2 is not a function이라는 오류를 내게 된다.

 

두번째 방식인 함수 선언식으로 선언한 경우, function 키워드를 만나면서 함수 전체가 호이스팅 되기 때문에 원하는 결과를 출력한다.


var favouriteFood = 'grapes';

var foodThoughts = function () {
  console.log("Original favourite food: " + favouriteFood); // undefined
  
  var favouriteFood = 'sushi';
  console.log("New favourite food: " + favouriteFood); // sushi
};

foodThoughts();

 

위 사례는 실행 컨텍스트를 잘 알아야 이해할 수 있다. 앞서 위에서 각각의 실행 컨텍스트들은 하나의 소우주라고 얘기했다. 이 코드에서 실행 컨텍스트는 총 2개인데, 글로벌 실행 컨텍스트(global())와 그 안에 존재하는 foodThoughts()가 실행 컨텍스트이다.

 

호이스팅은 각각의 실행 컨텍스트 내부에서 발생하는데, 따라서 foodThoughts()라는 실행 컨텍스트 내부에서 favouriteFood라는 변수가 var 키워드로 할당되어 있기 때문에, 이 컨텍스트 내부에서 favouriteFood 변수가 호이스팅되면서 동시에 값이 undefined로 초기화된다. 그렇기 때문에 첫 console.log는 undefined를 출력하게 되고, 그 이후에 실행 단계에서 실제로 값이 undefined에서 sushi로 변경되기 때문에 그 이후에는 sushi를 출력한다.

 

// global 실행 컨텍스트
var favouriteFood = undefined; // hoisting
var foodThoughts = undefined; // hoisting

favouriteFood = 'grapes';

      // foodThought 실행 컨텍스트
      foodThoughts = function () {
        var favouriteFood = undefined; // hoisting
        console.log("Original favourite food: " + favouriteFood);

        favouriteFood = 'sushi';
        console.log("New favourite food: " + favouriteFood);
      };
      // foodThought 실행 컨텍스트
// global 실행 컨텍스트

foodThoughts();

 

글로벌 실행 컨텍스트와 foodThoughts 실행 컨텍스트 둘 다 생성 단계에서 호이스팅이 끝난 상황에서 코드는 위와 같다. 

댓글