JavaScript(3) - Hoisting 과 TDZ

용스·2022년 5월 20일
0

JavaScript

목록 보기
3/5

JavaScript를 공부하면서 Hoisting과 TDZ에 대한 이야기를 들어본 적이 있을 것이다.
이 Hoisting과 TDZ를 이해하기 위해서는 사용하고 있는 var, let,const에 대해 잠시 살펴보고 넘어가겠다.

1. var

JavaScript에서 변수를 선언할 때 가장 많이 쓰이는 var에서 ES6에 let과 const가 추가되었다.
let과 const를 사용하기를 권장하는데 그 이유는 무엇일까?

다음 코드로 살펴보자

var person = 'jigom';

console.log( person );
// 출력 결과 : jigom

//----------- 10,000 라인 뒤 ------------//
var person = 10;
console.log( person );
// 출력 결과 : 10

이렇게 변수가 선언되어 있는데 중복으로 사용이 가능하다. 이렇게 변수 재선언을 막기 위해 나온 것이 바로 letconst다.

2. let, const

let은 변수를 중복 선언해서 사용할 수 없다.

let person = 'jigom';

console.log( person );

//----------- 10,000 라인 뒤 ------------//
let person = 10;
console.log( person );
// 출력 결과 : SyntaxError: Identifier 'person' has already been declared

const도 변수를 중복 선언해서 사용할 수 없고, const로 선언된 변수는 값을 수정할 수 없다.

const person = 'jigom';

console.log( person );
// 출력 결과 : jigom

//----------- 10,000 라인 뒤 ------------//
person = 10;
console.log( person );
// 출력 결과 : TypeError: Assignment to constant variable.

이외에 varlet, const는 변수 유효 범위와 Hoisting의 차이가 있다.

3. function-scoped? block-scoped?

흔히 var는 function-scoped라고도 표현한다.

다음 코드를 보자

function print(){
  var test = 123;
  console.log( test );
}
console.log( test );
// 출력 결과 : test is not defined

이렇게 함수 외부에서 호출하게 되면 에러를 발생한다.
하지만 if, else와 같이 block-scoped( { .. } 로 묶인 부분 )에서 실행하면 어떨까?

if( true ){
    var test = 123;
    console.log( test );
  	// 출력 결과 : 123
}
console.log( test );
// 출력 결과 : 123

이렇듯 var는 function-scoped의 범위를 따른다.

그렇다면 block-scoped인 letconst는 어떨까?

// let
function print(){
    let test = 123;
    console.log( test );
}
console.log( test );

if( true ){
    console.log( test );
}
// const
function print(){
    const test = 123;
    console.log( test );
}
console.log( test );

if( true ){
    console.log( test );
}
// 출력 결과 : test is not defined

{ }로 쌓여있는 모든 부분에서 접근할 수가 없다.

4. Hoisting

호이스팅(Hoisting)의 사전적 의미는 끌어 올리다 라는 뜻을 가지고 있다.
여기서도 같은 의미로 쓰인다. 함수 안에 있는 변수나 함수 맨위로 끌어올린다는 것이다.

실제로 코드가 끌어올려지는 것은 아니며, 자바스크립트가 내부적으로 끌어올려서 처리한다.

그럼 이런 경우는 어떨까? 한번 추측해보자

console.log(a); // undefined

a = 3;
console.log(a); // 3

var a = 1;
console.log(a); // 1

정상적으로 숫자를 출력한다. 그렇다면 letconst는 어떨까?

// let
console.log(a); // ReferenceError: Cannot access 'a' before initialization

a = 3;
console.log(a); // ReferenceError: Cannot access 'a' before initialization

let a = 1;
console.log(a);	// 출력 결과 : 1 ( 단, let a = 1 이전의 코드는 모두 주석처리 한다 )

//const 
console.log(a); // ReferenceError: Cannot access 'a' before initialization

a = 3;
console.log(a); // ReferenceError: Cannot access 'a' before initialization

const a = 1;
console.log(a);	// 출력 결과 : 1 ( 단, const a = 1 이전의 코드는 모두 주석처리 한다 )

그렇다면 letconst는 Hoisting이 되지 않은걸까?

다음 예제를 보면 let도 Hoisting이 된다는 것을 확인할 수 있다.

let a = 10;
{
  console.log(a); // ReferenceError: Cannot access 'a' before initialization
  let a = 20;
}

호이스팅이 안되는 것이라면 console.log(a) 에서 a가 상위 스코프의 a를 참조하여 10을 출력해야 하지만 ReferenceError가 난다는 것은 해당 스코프의 let a; 가 상위로 끌어 올려졌다는 것을 의미한다.

그럼 왜 letconst는 ReferenceError를 나타낼 수 있는 것일까?
그것은 바로 TDZ( Temporal Dead Zone ) 때문이다.

5. TDZ( Temporal Dead Zone )

이 TDZ를 설명하기 위해서는 간단히 변수 라이프 사이클에 대해 살펴보아야 한다.
변수는 크게 1. 선언단계 2. 초기화 단계 3. 할당 단계로 나뉜다.

  • var 변수의 경우 선언 단계 - 초기화 가 동시에 이루어지는 반면, let/const 변수의 경우 선언 단계와 초기화 단계가 나누어서 이루어짐
  • let/const 변수의 선언 단계와 초기화 단계 사이를 일시적 사각 지대 (Temporal Dead Zone; TDZ)라고 부름
  • 실제 코드에서 let 변수의 선언 또는 const 변수의 선언 및 할당 (const 의 경우 선언과 동시에 값 할당이 되어야 함)이 나오기 전까지는 해당 변수는 TDZ에서 관리 한다고 생각하면 됨
  • 해당 코드가 나오기 전에 미리 사용을 하려고 할 경우 TDZ에서 ReferenceError를 발생 시킴

이런 이유로 코드에 대한 안정성을 두고자 letconst를 사용해야 한다.

6. 실행 컨텍스트와 콜 스택, 스코프 체인

아직 이 부분은 지식이 얕다. 이는 자료 조사를 통해 내용을 가져왔다.

스코프 체인

스코프 체인(scope chain)이란?
스코프 체인(Scope Chain)은 일종의 리스트로서 전역 객체와 중첩된 함수의 스코프의 레퍼런스를 차례로 저장하고, 의미 그대로 각각의 스코프가 어떻게 연결(chain)되고 있는지 보여주는 것을 말한다.

하지만 스코프 체인(scope chain)을 이해하기 위해서 먼저 자바스크립트의 실행 컨텍스트(Execution context)를 알아야 한다.

실행 컨텍스트

  • 실행 컨텍스트는 LIFO( Last in, First out ) 구조체
  • 실행할 코드에 제공할 환경 정보들을 모아놓은 객체
  • 자바스크립트의 동적 언어로서의 성격을 가장 잘 파악할 수 있는 개념
  • 이런 컨텍스트는 call stack에 쌓이게 된다.

콜 스택

function multiply(x, y) {
    return x * y;
}
function printSquare(x) {
    var s = multiply(x, x);
    console.log(s);
}
printSquare(5);

이 코드가 실행될 때 콜 스택의 동작 단계는 다음과 같다.

만약 이렇게 재귀함수를 부르게 된다면

function foo {
  foo();
}
foo();


스택 오버플로우가 생겨 프로그램이 강제 종료 된다.

실행 컨텍스트에서 스코프 체인 동작

var v = "전역 변수";

function a() {
//function a Execution Context(EC)
	var v = "지역 변수";
    
    function b() {
    	//function b Execution Context
    	console.log(v);
    }
    
    b();
}
//Global Execution Context
a();

위 코드의 예제를 보면 먼저 글로벌 실행 컨텍스트(GEC)가 실행되고 스택에 쌓인다.

그런 다음 함수 호출 순으로 실행 컨텍스트 스택에 쌓이게 되고, 가장 나중에 호출된 b() 함수가 실행 컨텍스트 안에서부터 탐색을 시작한다.

그러면, b() 함수 안에서 변수 v를 탐색하기 시작하는데, 만약 변수 v가 없으면 b() 함수를 감싸고 있는 외부 함수 a() 함수를 탐색하기 시작한다.

이때 a() 함수 안에 변수 v가 존재하면 안에 있는 v를 참조하게 되고, 만약 없다면 마지막으로 전역 객체를 탐색하여 v를 찾아낸다.

반대로 찾았다면, 결과값은 a() 안에 변수 v가 존재하기 때문에 지역 변수라는 값이 출력이 된다.

하지만 만약, a() 함수 안에 변수 v를 제거한다면 전역 객체에 있는 변수 v의 값 전연 변수가 출력이 될 것이다.

이러한 과정들이 스코프에 담긴 순서대로 탐색하는 즉, 스코프 체인이라고 보면 된다.
[ 참고 링크 - 태기의 개발 Blog ]

정리하며

1: let b = 1;
2: 
3: function hi () {
4: 
5: const a = 1;
6: 
7: let b = 100;
8: 
9: b++;
10: 
11: console.log(a,b);
12:
13:}
14:
15://console.log(a);
16:
17:console.log(b);
18:
19:hi();
20:
21:console.log(b);

다음과 같은 코드가 있다고 하자.
주석을 처리하지 않은 상태에서 출력은 어떻게 나올까?

출력 결과
17번 라인 : 1 출력
19번 라인 : 1, 101출력
21번 라인 : 1 출력

  1. b는 전역 전수이기 때문에 17, 21번째 console.log(b)는 1번 라인의 let b = 1 을 참조한다.
  2. hi() 내의 함수는 1번 라인의 let b = 1의 전역을 보지 않고 함수 내부에 동일한 변수명으로 선언된 let b = 100를 참조하기 때문에 console.log( a, b )는 1과 101을 출력한다.

만약 15번 라인의 주석 처리를 없애면 어떨까?
a를 찾아보면 5번 라인의 const로 선언되어 있다.
const는 block-scope 단위이기 때문에, 5번 라인의 a 변수를 참조하지 못한다.
따라서, a is not defined가 출력된다.

profile
일단 해보자

0개의 댓글