호이스팅, 실행 컨텍스트 그리고 클로저


호이스팅 (Hoisting)

변수를 선언하고 초기화했을 때 선언 부분이 최상단으로 끌어올려지는 현상.

호이스팅의 대상

  • var 변수 선언.
    할당은 끌어올려 지지 않는다.( let, const 변수 선언은 호이스팅이 일어나지 않는다. )
    아래 예시 코드를 보자.
console.log(a); // (2) 출력 - undefined
var a = 10; // (1) var a; 변수 선언, (3) a = 10; 변수 할당
console.log(a); // (4) 출력 - 10

주석의 괄호 안 숫자가 바로 코드가 실행되는 순서이다.
호이스팅이 발생해 var변수 선언이 최상단에 위치하게 되고,
이로 인해 첫번째 콘솔에는 undefined가 출력된다.
즉, 실제로 자바스크립트가 해석한 코드는 다음과 같다.

var a;
console.log(a);
a = 10;
console.log(a);

하지만 let과 const는 호이스팅이 발생하지 않으므로 에러가 발생한다.

console.log(a); // 에러 발생
let a = 10; 
console.log(a);
  • 함수 선언문 (함수 표현식은 호이스팅이 일어나지 않는다.)
    마찬가지로 할당은 끌어올려 지지 않는다.
fun1(); // (3) 호출
fun2(); // (5) 호출 - TypeError: fun2 is not a function
function fun1() { // (1) 함수 선언
    console.log("I'm fun1()"); // (4) 출력
}
var fun2 = function() { // (2) var fun2; 변수 선언
    console.log("I'm fun2 ");
}

여기서 잠깐~!

과연 let, const 는 호이스팅이 일어나지 않을까?
엄밀히 말하자면 let, const도 자신이 속한 스코프의 최상단으로 호이스팅은 일어난다.
하지만 선언되어 초기화 되기 전까지 TDZ (Temporal Dead Zone) 영역에 속해 메모리가 할당되지 않아 참조에러(ReferenceError) 가 발생한다.

여기서 바로 var와 let,const 의 차이가 발생한다.
변수의 선언은 3단계로 구분이 되는데,

  • 선언 (declaration phase) : 변수를 실행 컨텍스트의 변수 객체에 등록하는 단계. 이 변수 객체는 스코프가 참조하는 대상이 된다.
  • 초기화 (Initialization phase) : 실행 컨텍스트에 존재하는 변수 객체에 선언 단계의 변수를 위한 메모리를 만드는 단계. 이 단계에서 할당된 메모리는 undefined로 초기화된다.
  • 할당 (Assignment phase) : 사용자가 undefined로 초기화된 메모리에 다른 값을 할당하는 단계.

var 으로 선언된 변수는 선언과 초기화가 한번에 이루어지는 반면,
let 으로 선언된 변수는 선언과 초기화 단계가 분리되어 이루어진다.

이러한 이유로 var와 let,const 의 차이가 발생하는 것이다.


실행 컨택스트 (Execute Context)

context문맥 이라는 의미를 가지고 있다. JavaScript, TypeScript 의 실행 컨텍스트 는 코드의 문맥, 즉 코드의 실행환경 이라고 할 수 있다. 실행 컨텍스트는 이러한 환경에서 실행될 코드에 대한 모든 정보들을 모아둔 객체이다.

  • Global Execute Context

    실행 컨텍스트의 가장 기본인 전역 컨텍스트이다. 코드가 특정 함수에 들어가 있지 않다면 그 코드의 컨텍스트(실행환경)은 전역 컨텍스트이다.
    다시 한번 정리하면, 처음 코드를 실행하는 순간 모든 것을 포함하는 전역 컨텍스트가 생긴다. 모든 것을 관리하는 환경이다.
  • Funtion Execute Context

    함수가 호출될 때 마다 해당 함수에 대해 생성되는 실행 컨텍스트이다. 함수가 호출이 되어야 만들어지며, 각 함수들은 자신만의 실행 컨텍스트를 가진다.

아래의 코드를 보자

var a = 10; // (1) 변수 선언, (3) 할당
fun1(); // (4) 함수 호출
function fun1 () { // (2) 함수 선언, 
    function fun2 () { // (5) 함수 선언
        console.log(a); // (8) 변수 출력 - undefined
        var a = 50; // (7) 변수 선언, (9) 할당 - 50
    }
    fun2(); // (6) 함수 호출
    console.log(a); // (10) 출력 - 10
}

console.log(a); // (11) 출력 - 10

주석의 괄호 안 숫자가 코드가 실행되는 순서이다. 진짜 어지럽다.
차근차근 살펴보자.

처음 코드가 실행될 때 모든 것을 관리하는 환경인 전역 컨텍스트 가 생성된다.
이후, 함수 호출 시 마다 함수 실행 컨텍스트가 생성된다.
( 컨텍스트 생성 시, 컨텍스트 안에는 변수객체, scope chain, this가 생성된다 )
컨택스트 생성 후 함수가 샐행되는데, 사용되는 변수들은 해당 스코프 안에서 값을 찾고,
없다면 스코프 체인을 따라 올라가며 찾는다. (지역 변수라고 생각하면 된다)
함수 실행이 마무리되면 해당 컨텍스트는 사라지고, 모든 코드의 실행이 종료되면
전역 컨텍스트가 사라진다.


전역 컨텍스트

먼저 코드를 보고 실행 순서와 결과를 예측해보자.

var name = "JonDoe"; // (1) 변수 선언, (4) 할당

function sayHello(word) { // (2) 함수 선언
    console.log(`${word}~! ${name}`); // (10) 출력
}

function say() { // (3) 함수 선언
    var name = "Sam"; // (6) 변수 선언, (7) 할당
    console.log(name); // (8) 출력
    sayHello("Hello"); // (9) 함수 호출 - 함수 컨텍스트 생성
}

say(); // (5) 함수 호출 - 함수 컨텍스트 생성

전역 컨텍스트가 생성된 후, 변수 객체, scope chain, this가 들어온다.
(1) ~ (4) 까지 :

'전역 컨텍스트': {
  변수객체: {
    arguments: null,
    variable: [{
      name: 'JonDoe' 
    }, { 
      sayHello: Function 
    }, {
      say: Function 
    }]
  },
  scopeChain: ['전역 변수객체'],
  this: window,
}

(5) ~ (8) 까지 :

'say 컨텍스트': {
  변수객체: {
    arguments: null,
    variable: [{ name: 'Sam' }], 
  },
  scopeChain: ['say 변수객체', '전역 변수객체'],
  this: window,
}

(9) sayHello("Hello") 는 say 컨텍스트 안에서 sayHello 변수를 찾을 수 없다.
따라서 scope chain을 따라 올라가 상위 변수객체인 전역 변수객체에서 찾는다.
전역 컨텍스트.변수객체.variable의 sayHello 함수를 호출한다.

(9) ~ (10)

'sayHello 컨텍스트': {
  변수객체: {
    arguments: [{ word : 'Hello' }],
    variable: null,
  },
  scopeChain: ['sayHello 변수객체', '전역 변수객체'],
  this: window,
}

console.log(${word}~! ${name}) 의 word는 arguments에서 찾을 수 있고,
name은 sayHello 변수 객체에 존재하지 않는다. 따라서 역시 scope chain 을 따라
상위 변수객체로 올라가고, 전역 변수객체의 variable의 name인 JonDoe가 출력된다.

sayHello 함수 종료 후, sayHello 컨텍스트가 사라지고, say 함수가 종료된다.
따라서 say 컨텍스트도 사라지고, 마지막 전역 컨텍스트 또한 사라진다.


호이스팅과 실행 컨텍스트

  • 함수 선언식
console.log(name); // (3) 변수 출력

hoistFunction(); // (4) 함수 호출

function hoistFunction() { // (1) 함수 선언
    console.log(a); // (6) 출력
    var a = "variable"; // (5) 변수 선언, (7) 할당
    console.log(a); // (8) 출력
}

var name = "James"; // (2) 변수 선언, (9) 할당

(1) ~ (3) 까지 :

'전역 컨텍스트': {
  변수객체: {
    arguments: null,
    variable: [{ 
      hoistFunction: Function 
    }, {
      name: undefined
    }],
  },
  scopeChain: ['전역 변수객체'],
  this: window,
}

(4) ~ (8) 까지 :

'hoistFunction 컨텍스트': {
  변수객체: {
    arguments: null,
    variable: [{ a: "variable" }],
  },
  scopeChain: ['hoistFunction 변수객체', '전역 변수객체'],
  this: window,
}
  • 함수 표현식
hoistFunction(); // (3) 함수 호출
myFunction(); // (8) 함수 호출 - TypeError: fun2 is not a function

var myFunction = function() { // (1) 변수 선언
  console.log("call Myfunction() Success!");
}

function hoistFunction() { // (2) 함수 선언 (및 초기화)
    console.log(a); // (5) 출력
    var a = "variable"; // (4) 변수 선언, (6) 할당
    console.log(a); // (7) 출력
}

(1) ~ (2) 까지 :

'전역 컨텍스트': {
  변수객체: {
    arguments: null,
    variable: [{ 
      myFunction: undefined
    }, {
      hoistFunction: Function 
    }],
  },
  scopeChain: ['전역 변수객체'],
  this: window,
}

(3) ~ (7) 까지 :

'hoistFunction 컨텍스트': {
  변수객체: {
    arguments: null,
    variable: [{ a: "variable" }],
  },
  scopeChain: ['hoistFunction 변수객체', '전역 변수객체'],
  this: window,
}

(8) myFunction 함수 호출 :

'전역 컨텍스트': {
  변수객체: {
    arguments: null,
    variable: [{ 
      myFunction: undefined
    }, {
      hoistFunction: Function 
    }],
  },
  scopeChain: ['전역 변수객체'],
  this: window,
}

myFunction 함수가 대입되기 전에 호출하므로 TypeError: fun2 is not a function 발생.


클로저

클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.

"lexical" 이란, 어휘적 범위 지정(lexical scoping) 과정에서 변수가 어디에서 사용 가능한지 알기 위해 그 변수가 소스코드 내 어디에서 선언되었는지 고려한다는 것을 의미한다. 단어 "lexical"은 이런 사실을 나타낸다. 중첩된 함수는 외부 범위(scope)에서 선언한 변수에도 접근할 수 있다.

아래의 코드를 보자.

var makeClosure = function() { // (2) 변수 선언, (3) 함수 할당
  var name = '홍길동'; // (5) 변수 선언, 할당
  return function () { // (6) 함수 반환, 
    console.log(name); // (8) 출력 - 홍길동
  }
};
var closure = makeClosure(); // (1) 변수 선언, (4) 함수 호출, (6) function(){ console.log(name); }를 closure 변수에 할당
closure(); // (7) 함수 호출

(1) ~ (5) 까지 :

"전역 컨텍스트": {
  변수객체: {
    arguments: null,
    variable: [{ 
      makeClosure: Function 
    }, {
      closure: undefined
    }],
  },
  scopeChain: ['전역 변수객체'],
  this: window,
}
"makeClosure 컨텍스트": {
  변수객체: {
    arguments: null,
    variable: [{ name: '홍길동' }],
  },
  scopeChain: ['makeClosure 변수객체', '전역 변수객체'],
  this: window,
}

(6) ~ (8) 까지 :

"closure 컨텍스트": {
  변수객체: {
    arguments: null,
    variable: null,
  },
  scopeChain: [ 'closure 변수객체', 'makeClosure 변수객체', '전역 변수객체' ],
  this: window,
}

var closure = makeClosure();closure();를 주목하자.
function(){ console.log(name); }를 반환받은 closure 내부엔 자신만의 지역 변수가 없다.
그런데 함수 내부에서 scope chain을 통해 외부 함수의 변수에 접근할 수 있기 때문closure() 역시 부모 함수 makeClosure()에서 선언된 변수 name에 접근할 수 있다. 만약 closure()가 자신만의 name변수를 가지고 있었다면, name대신 this.name을 사용했을 것이다.

마지막으로 한가지 예시만 더 살펴보자.

var counter = function() { // (1) 변수 선언, (3) 함수 할당
  var count = 0; // (5) 변수 선언, 할당
  function changeCount(number) { // (6) 함수 선언, 할당
    count += number;
  }
  return { // (7) 반환
    increase: function() { // (7-1) 함수 선언
      changeCount(1);
    },
    decrease: function() { // (7-2) 함수 선언
      changeCount(-1);
    },
    show: function() { // (7-3) 함수 선언
      alert(count);
    }
  }
};
var counterClosure = counter(); // (2) 변수 선언, (4) 함수 호출, (7) function changeCount(number) {} 반환, 할당
counterClosure.increase(); // (8) 함수 호출 ~~~~
counterClosure.show(); // 1
counterClosure.decrease();
counterClosure.show(); // 0

counter()함수를 호출할 때, counterClosure 컨텍스트counterClosurecounter가 담긴 scope chain이 생성된다. 그렇게 되면 이제 counterClosure에서 계속 count로 접근할 수 있다. return 안에 들어 있는 함수들count 변수나, changeCount 함수 또는 그것들을 포함하고 있는 스코프에 대한 클로저라고 부를 수 있다.

클로저는 프론트엔드 단에서 중요한 개념이므로 알아두도록 하자.


결론

이 모든 정리는 결국 var 를 사용하지 말자는 것이다. 이유인 즉슨,,,

  • 코드의 가독성과 유지보수를 위해 Hoisting이 일어나지 않도록 해야한다.
  • 호이스팅을 제대로 모르더라도 함수와 변수를 가급적 코드 상단부에서 선언하면, 호이스팅으로 인한 스코프 꼬임 현상은 방지할 수 있다.
  • 따라서 var는 사용하지 말고, Block Scope 를 갖는 letconst를 사용하자.

[참고자료]

좋은 웹페이지 즐겨찾기