본문 바로가기
JavaScript/모던 자바스크립트(Deep Dive)

[Deep Dive] var, let, const의 차이 / hoisting(호이스팅)에 대해

by junvely 2023. 7. 11.

오늘 목표 : var, let, const의 차이 / hoisting(호이스팅)

모던 자바스크립트 Deep Dive 책을 이용해 변수들과 호이스팅에 대해 공부한 내용을 정리해 보았다.


1. var의 등장 배경 / 문제점

1) 중복 선언 

같은 이름의 변수를 중복해서 선언해도 정상적으로 동작한다. 또 재할당이 아니라 이전의 변수가 덮어쓰여진다. 규모가 큰 프로젝트에서 변수명이 중복될 경우에도 Error가 발생하지 않는다. 이는 누군가 실수로 변수를 중복 선언하여 의도치 않은 결과를 초래할 수 있다.

2) var 호이스팅

- var변수의 선언문이 스코프 내의 최상단으로 끌어올려진 것처럼 동작하여 변수를 정의하기 전에 사용해도 에러가 발생하지 않는다.

3) 함수 레벨 스코프 

- var 키워드로 정의된 변수는 함수 스코프이기 때문에 함수를 벗어난 영역에서 사용하면 에러가 발생한다.

- 함수 블록 내에서 선언된 변수는 함수 내부의 어느 곳에서든지 참조할 수 있다.

function foo() {
  var x = 10;
  if (x > 5) {
    var y = 20; // 함수 스코프에 선언된 변수
    console.log(x, y); // 10, 20
  }
  console.log(x, y); // 10, 20 (함수 외부에서도 접근 가능)
}

foo();
console.log(x, y); // ReferenceError: x is not defined, ReferenceError: y is not defined

- 함수 블록 내에서 키워드(var, let, const) 없이 변수를 선언하고 값을 할당하면 해당 변수는 전역 변수로 취급된다.

4) 블록 스코프 미지원

for문, while문, switch문, if문 등 블록 내에서 정의된 변수는 반복문이 끝난 이후에도 계속 남아 있고, 외부에서도 접근 가능하다.

 

이러한 문제점들 때문에 ES6(ES2015)부터는 let과 const 키워드가 도입되었으며, 이를 사용하여 변수 선언을 권장다. let과 const는 블록 스코프를 가지고 있고, 중복 선언을 허용하지 않으며, 호이스팅 문제를 완화시켜주는 등 var의 문제점을 해결하는 역할을 수행한다.

 

2. var, let, const의 차이 

💡요약
var는 변수 재선언(중복 선언), 재할당 모두 가능하다.
let은 변수 재선언(중복 선언)은 불가능재할당은 가능하다.
const는 변수 재선언(중복 선언), 재할당 모두 불가능 하다.
- var는 함수 레벨 스코프이고, let과 const는 블록 레벨 스코프 이다.

 

3. 함수 레벨 스코프 vs 블록 레벨 스코프의 차이 

1) 함수 레벨 스코프 - var

  • 함수 내에서 선언된 변수는 함수 블록 내에서 선언되었더라도 호이스팅(hoisting)에 의해 함수의 최상위로 끌어올려지는 특징이 있다.
  • 함수 내에서 선언된 변수는 해당 함수 전체에서 유효하며, 함수 내 어디서든 접근 가능하다.
  • 함수 내에서 선언된 변수가 함수 외부에서는 유효하지 않다.

2) 블록 레벨 스코프 - let, const

  • 블록 레벨 스코프는 중괄호 {}로 둘러싸인 블록 내에서 변수의 유효 범위를 제한하는 개념
  • if, for, while, switch 등의 제어문이나 중괄호로 둘러싸인 코드 블록 내에서 선언된 변수는 해당 블록 내에서만 유효하며, 블록 외부에서는 접근할 수 없다.
  • 블록 레벨 스코프를 갖는 변수는 변수가 선언된 블록 내에서만 유효하고, 블록을 벗어나면 해당 변수는 사라진다.
  • 블록 레벨 스코프는 변수의 유효 범위를 블록 단위로 제한하여 변수 이름 충돌 등을 방지하고 예상치 못한 동작을 줄여준다.

 

 

4. hoisting(호이스팅)

💡요약
- 자바스크립트 엔진이 런타임 이전(코드를 실행하기 전)에 컨텍스트의 최상단에 변수와 함수 선언을 식별자와 초기값 undefined으로 등록하는데, 이로 인해 해당 스코프의 최상단으로 끌어올려진 것처럼 동작하는 것을 호이스팅 이라고 한다.

자바스크립트 코드가 엔진에 의해 스크립팅 될 때, 파서는 자바스크립트 코드를 읽고 구문을 분석하여 추상 구문 트리(Abstract Syntax Tree, AST)를 생성한다. 이 과정에서 변수와 함수 선언을 컨텍스트에 등록하고, 해당 식별자와 초기값 undefined를 할당하는 역할을 수행한다. 인터프리터는 호이스팅된 변수와 함수 코드를 한줄 씩 순차적으로 해석해 실제로 코드를 실행한다.

- 컴파일과 인터프리팅 : https://honggom.tistory.com/158 

인터프리팅은 컴파일과 다르게 소스 코드를 한 번에 읽어서 번역하지 않고, 런타임 상태에 소스코드를 한 줄 한 줄 번역하면서 프로그램을 구동하는 방식이다. 한 줄 한 줄 번역한 코드가 바로 기계어가 되는 것은 아니고 중간 코드(
intermediate code)로 번역 된다. 이 중간 코드는 다른 프로그램에 의하여 기계어로 번역되어 실행된다.
대표적으로 python, js 등이 있다.

인터프리팅 언어는 컴파일과 다르게 런타임 상황에서 코드를 번역하기 때문에 일반적으로 컴파일 언어보다 실행시간이 느리다. 또 컴파일 언어는 런타임 이전에 예외를 확인할 수 있었으나, 인터프리팅 언어는 이와 다르게 런타임 이전 에러를 알 수는 없지만, 런타임 시 에러가 발생하면 그 이후 작성된 코드를 살펴보지 않는다는 차이점이 있다.

 

hoisting(호이스팅)에 대해 이해하기 위해서는, 자바스크립트에서의 변수 선언과 호이스팅에 대한 깊은 이해가 있어야 한다.

변수 선언은 2가지 단계로 이루어 진다.

 

1) 선언 단계 = 변수를 생성

값을 저장하기 위해 메모리 공간을 확보하고, 메모리 공간과 주소를 연결하여 값을 저장할 준비를 한다.

2) 값 초기화 단계

변수 선언 후, 처음 값이 할당되는 과정. 선언 후에 값을 할당하지 않은 변수에 접근하고자 할 때는 참조 에러가 발생한다. -> Reference Error...

하지만 자바스크립트에서는 암묵적으로 undefined라는 값이 할당되어 초기화 된다는 특징이 있다.

단, 선언과 초기화 단계가 동시에 이루어 지느냐에 따라 *TDZ(일시적 사각지대)가 발생할 수 있다.

*TDZ(일시적 사각지대) : 스코프 시작 지점부터 초기화 시작 지점까지 변수를 참조할 수 없는 구간, 참조 에러 발생

 

여기서 중요하게 알고 넘어가야 할 점은 변수 호이스팅 이다.

 

3) 변수 호이스팅 = 모든 선언(식별자)는 호이스팅 된다.

선언 후, 값을 할당하지 않은(초기화 되지 않은) 변수에 접근할 경우에는, 참조 에러가 발생한다. 하지만 자바스크립트 엔진은 모든 선언을 런타임 이전, 즉 소스 코드가 한 줄씩 순차적으로 실행되는 이전 시점의 실행 준비 단계에서 선언 단계를 먼저 실행한다.

이처럼 변수 선언문이 코드의 선두로 끌어올려진 것처럼 동작하는 자바스크립트의 고유 특징을 '변수 호이스팅'이라고 한다. 사실 변수 뿐만 아니라 모든 선언(식별자)은 호이스팅 한다.

주의할 점은, 변수 선언은 호이스팅으로 런타임 시점(변수 선언 시점) 이전에 먼저 실행되어 컨텍스트 최상단에 등록되고, 변수 선언의 메모리 공간 확보는 호이스팅 후, 변수 선언문을 만날 시(런타임 시점)에 이루어진다. 값의 할당 역시 소스 코드가 순차적으로 실행되는 런타임 시점에 실행된다. 즉 선언만 끌어올리고, 메모리 공간 확보와 값 할당은 해당 문을 만나는 시점에 런타임 시점에서 실행 되는 것이다.

 

그럼 모든 선언은 호이스팅 되고, 값은 할당문을 만나는 시점에 할당되는데,

왜 var는 선언하고 값을 할당 하지 않아도 초기화가 자동으로 이루어져 undefined를 반환하고, let은 초기화 되지 않고 참조 에러가 발생하는 구간이 있는 것일까?

- var의 경우, 변수 선언에서 선언 단계와 초기화 단계가 동시에 진행된다. 이 때문에 호이스팅으로 인해 선언이 스코프의 최상위로 끌어올려지고, 선언과 동시에 초기화가 이루어지기 때문에 값을 할당하지 않더라도 undefined를 반환한다.

console.log(foo) // undefined -> 호이스팅 + 초기화

var foo;
console.log(foo); // undefined

foo = 1; // 하지만 값 할당 단계는 할당문을 만나야 실행된다.
console.log(foo) // 1

 

- 하지만 let의 경우, 선언 단계와 초기화 단계가 분리되어 실행된다. let의 선언은 호이스팅 되지만, 초기화는 실제 변수 선언문을 만나는 런타임 시점에 실행되어 undefined로 초기화 된다.

즉 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 선언 단계가 먼저 실행되지만, 초기화 단계는 변수 선언문에 도달했을 때 실행되는 것이다. 만약 초기화 단계가 실행되기 이전에 변수에 접근하면, 참조 에러가 발생한다.

-> 이렇게 변수 스코프의 시작 지점부터 초기화 단계 시작 지점까지 변수를 참조할 수 없는 구간을 일시적 사각지대, TDZ라고 부른다.

// 런타임 이전에 선언 단계 실행, 아직 변수 초기화 전
// 초기화 전의 일시적 사각지대 -> 변수를 참조할 수 없다.
console.log(foo) // 참조 에러 발생

let foo; // 선언문을 만나야 초기화 실행
console.log(foo) // undefined

foo = 1 // 할당문을 만나야 할당 단계가 실행된다.
console.log(foo) // 1

 

5) var, let, const 의 호이스팅 차이

var 호이스팅: 변수 선언부만 호이스팅되어 메모리에 할당됩니다. 변수에 값이 할당되기 전에 해당 변수를 사용하면 undefined로 평가됩니다.초기화: 변수 선언과 동시에 초기화되고 메모리 공간이 할당됩니다.

let 호이스팅: 변수 선언부만 호이스팅되어 TDZ(Temporal Dead Zone)에 놓입니다. 선언 이전에 해당 변수를 사용하려고 하면 ReferenceError가 발생합니다.초기화: 변수 선언문에 도달했을 때 실행 컨텍스트에 해당 변수가 등록되고, 변수의 위치를 결정하며, 이후 코드 실행 시에 해당 변수가 사용될 수 있도록 메모리 공간이 할당됩니다.

const 호이스팅: let과 마찬가지로 변수 선언부만 호이스팅되어 TDZ에 놓입니다. 선언 이전에 해당 변수를 사용하려고 하면 ReferenceError가 발생합니다.초기화: let과 동일하게 변수 선언문에 도달했을 때 실행 컨텍스트에 해당 변수가 등록되고, 변수의 위치를 결정하며, 이후 코드 실행 시에 해당 변수가 사용될 수 있도록 메모리 공간이 할당됩니다. 다만, const는 상수로 값을 변경할 수 없기 때문에 선언과 동시에 반드시 초기값을 할당해야 합니다.