Web언어/JS

[JS] ES6 달라진 문법 ( let, const ) + 호이스팅(Hoisting)

IT록흐 2022. 4. 22. 05:26
반응형

 

 'var' 를 사용하지 말자!

 

ES6부터 let과 const가 등장한 이유는 무엇일까?

 

 

▶ 실행 원리

 

JavaScript 엔진은

Stack 메모리와 Heap 메모리를 사용한다.

 

 

 

1. 호출 스택 ( Call Stack )

 

 

 

 

스택은 LIFO이다.

마지막에 PUSH된게 먼저 POP된다.

 

이런 구조는 함수 호출 구조와 같다.

 

 

Js파일이 호출되면 

foo()가 호출되고

그다음 foo1() 함수가 호출된다. 

 

그리고

 

foo1() 함수가 종료되고

foo()가 종료된 뒤

js파일이 종료된다. 

 

 

 

그러므로

 

Stack 메모리에 PUSH와 POP이 되는 객체는

함수의 정보를 담은 객체로

 

이를,

실행컨텍스트(Execution Context; EC)라 부른다. 

 

실행컨텍스트는 스택에 쌓이는 프레임으로

함수단위로 생성된다. 

 

 

 

 

2. 호이스팅(Hoisting)

 

 

javascript 엔진은 코드를 실행하기 전

독특한 작업을 한다.

 

바로 호이스팅(Hoisting)이다. 

 

호이스팅이란,

선언된 함수와 변수를 코드를 실행하기 전

Heap 메모리에 미리 할당에 놓는 작업을 의미한다. 

 

 

 

자바스크립트 엔진은 코드를 훑으며

var, const, let, function 같이 선언문을 탐색해

Heap메모리에 공간을 만들어 할당한다.

 

이렇게 생성된 메모리 영역을

'환경레코드'라 부른다. 

 

 

 

호이스팅이 끝나고

스크립트 파일을 한 줄씩 실행하면

 

엔진은 환경레코드에 기록된 변수에

값을 할당한다. 

 

 

3. 호이스팅이 필요한 이유는 무엇일까?

 

 

호이스팅이 필요한 이유는

JavaScript는 Top-Down 방식으로 실행되기 때문이다.

 

var x = 1;
var y = 2;

foo(); // 함수 호출

function foo(){
}

 

만약 호이스팅이 없다면

foo() 함수가 호출되어도

 

foo()의 존재 자체를 알 수 없다.

 

왜냐하면

foo 함수의 정의는 호출문 아래에 있기 때문이다.

 

void hello();    // 선언
int main()
{
    hello();    // hello 함수 호출

    return 0;
}

void hello()    // 정의
{
    printf("Hello, world!\n");    // Hello, world! 출력
}

 

동일한 TOP-Down 방식인 C언어는 

이런 문제를 해결하고자

선언문과 정의문을 분리하여 사용한다. 

 

이처럼

호이스팅이 없다면 Top-Down 방식인 

JavaScript는 모든 함수를 위에 작성해야 한다. 

 

그러나

호이스팅이 있기에, 코드를 실행하기전 

환경레코드에 함수가 정의된 위치를 파악할 수 있다.

 

 

호이스팅은 이러한 이유로 필요하지만

문제는 변수까지 호이스팅 한다는 것이다. 

 

 

 

변수를 호이스팅하는 과정에서

발생하는 문제는

const와 let을 등장시켰다. 

 

1. undefined

 

 

var로 선언된 변수는

호이스팅 과정에서 undefined로 초기화된다.

 

다른 언어에서 

할당되지 않은 변수를 참조하려하면

NullPointExeception이 걸린다. 

 

그러나

var 변수는 실행되기 전

이미 undefined로 초기화가 되어 있기에

 

참조가 가능하다. 

 

var x; // 할당 X
console.log(x); // undefined 출력

 

x는 할당되지 않았지만

console.log 함수가 참조할 수 있다. 

 

x = 3;  // 선 할당
var x; // 후 선언

console.log(x); // 에러 X

 

심지어는 할당이 먼저 되고 

후에 선언이 되었는데도

 

실행이 가능하다. 

 

모두 호이스팅(Hoisting) 때문이다.

 

이런 현상은

다른 언어에서는 비상식적인 현상으로

이런 현상을 없애고자

 

ES6 이후부터는

호이스팅 과정에서 undefined를 할당하지 않은

const와 let이 등장했다. 

 

const와 let은 위와같은 상황에서

에러를 발생시킨다.

 

 

2. 함수 스코프와 블록 스코프

 

 

앞서 우리는 호이스팅 과정에서

환경레코드가 힙 메모리에 형성된다고 말했다.

 

이때, 환경레코드가 형성되는 기준은

'블록'이다. 

 

 

전역적인 범위의 환경레코드와

if문이나 for문 같은 블록 단위에도

환경레코드가 생성된다.

 

핵심은!

 

선언된 변수는 어느 환경레코드에 기록되느냐이다.

 

var로 선언된 변수는 

함수 스코프 변수이다. 

 

고로,

if 블록에 선언되었든, for블록에 선언되었든

전역 환경레코드에 기록이 된다. 

 

 

 

var z는 if문 안에서 선언되었지만

기록되는 환경레코드는 전역 환경 레코드이다. 

 

이 결과 기이한 현상이 펼쳐진다. 

 

var x = 1;
var y = 2;

if(true){
    var z;
    z = 2;
}
cosole.log(z) // 2 출력

 

 

분명 if문은 로컬 영역이고

console.log 함수는 전역 영역에 위치한다.

 

그런데

console.log 함수가 로컬영역에서

선언된 z변수를 참조할 수 있다.

 

그 이유는

var는 함수 스코프 변수로 

어느 블록에 선언되었든

 

전역 환경레코드에 기록되기 때문이다. 

 

다른 언어에서는 상상도 못할 일이다. 

 

 

 

 

 

let과 const는 블록 스코프 변수이다. 

즉, 선언된 블록의 환경레코드에 기록된다.

 

let z는 if문에서 선언되었다.

 

그러면 z는 if문의 환경레코드에 기록된다. 

 

 

let x;
let y = 2;

if(true){
    let z;
    z = 2;
}

console.log(z) 
// not defined ERROR

 

console.log 함수는 z에 접근하지 못한다.

전역 환경 레코드에 z가

기록되어있지 않기 때문이다. 

 

반면 if문에서 전역 환경레코드는 참조 가능하다.

 

if문 환경레코드는 자신의 상위 환경레코드의 

주소를 갖고 있기 때문이다.

 

이를, 외부 렉시컬 참조라 부른다. 

 

그리고

전역 환경레코드는 자신을 호출한 함수의

환경레코드의 주소를 갖고 있다.

 

 

 

 

 

foo1의 for문 환경레코드는

foo1의 전역 환경레코드를 참조할 수 있다.

 

foo1 전역 환경레코드는 

자신을 호출한 foo 함수의 전역환경레코드를 참조할 수 잇다.

 

foo 함수의 전역 환경레코드는

자신을 호출한 js파일의 전역환경레코드를 참조할 수 있다.

 

고로

js파일에 선언된 x변수를

foo1의 for문에서 참조할 수 있다. 

 

 

이렇게 사슬처럼 엮인 관계를 이르러,

스코프 체이닝(Scope Chaining)이라 부른다. 

 

 

 


 

 

let과 const는 호이스팅으로 인해 발생하는 javascript의 기이한 현상을 바로 잡고자 등장하였다. 

 

 

참고자료

 

https://www.youtube.com/watch?v=EWfujNzSUmw 

https://okayoon.tistory.com/entry/%EC%8A%A4%EC%BD%94%ED%94%84Scope%EB%9E%80

https://velog.io/@shroad1802/environment-record

 

반응형