Web/Vanilla JS

호이스팅(Hoisting)

KimMinJun 2023. 9. 3. 02:24

1. 호이스팅이란?

[ MDN - Hoisting ]

 

호이스팅 - MDN Web Docs 용어 사전: 웹 용어 정의 | MDN

자바스크립트 호이스팅은 인터프리터가 코드를 실행하기 전에 함수, 변수, 클래스 또는 임포트(import)의 선언문을 해당 범위의 맨 위로 이동시키는 과정을 말합니다.

developer.mozilla.org

 

위 MDN 사이트에 따르면 호이스팅은 "인터프리터가 코드를 실행하기 전에 함수, 변수, 클래스 또는 임포트(import)의 선언문해당 범위의 맨 위로 이동시키는 과정" 이라고 한다.

 

위 정의를 읽고 "아 그냥 변수나 함수들을 맨 위에 갖다놓고 사용하는거구나~" 라고 생각하고 넘어갈 수도 있다. (나도 그랬다...)

 

하지만 '선언문' 이라는 키워드를 굳이 넣어야 했을까? 라는 의문에서 시작해서 찾아보고, 자세히 알게 된 것을 정리해보려 한다.

 

1) 선언문, 할당문, 표현문

먼저 의문이 시작된 '선언문' 부터 알아보면서 시작해보자.

 

선언문은 변수나 함수를 선언할 때 사용된다. 다음과 같은 선언문을 위해 사용하는 키워드들이 있다.

  • 변수 선언 : var, let, const
  • 함수 선언 : function
var a; // a라는 변수를 선언하였다.

 

하지만 우리가 변수를 선언하는 이유는, 어떠한 값을 사용하기 위해 그 값을 담을것을 만들기 위해서이다.

따라서 이 과정에서 변수에 값을 '할당' 하는 과정도 필요하다.

따라서 할당문은 변수에 값을 할당하기 위해서 사용한다.

a = 10; // a라는 변수에 10을 할당하였다.

 

또한, 변수에 대한 여러 연산들이 있을 수 있다. 이러한 변화를 표현하기 위해 표현문이 존재한다.

표현문은 표현식을 포함하는 문장이다. 예를 들면 다음과 같은 예시가 있다.

여기서 표현식이란, 어떠한 결과를 반환하는 식이다.

x = x + 10; // x + 10에 대한 표현식의 결과를 반환하여 x에 저장하는 표현문이다.

 

2) 의문점

위에서 선언문, 할당문, 표현문을 알아보았다. '선언문'에 대한 개념을 알았으니 그럼 다시 의문이 들 수도 있다. 예를 들면 다음과 같은 간단한 코드가 있다고 해보자.

var a = 10; // 표현문

function f() { // 선언문
    var b = 10; // 표현문
}

var c = 30; // 표현문

 

 

위 예시를 보고, 호이스팅의 정의를 다시 한번 보자.

"인터프리터가 코드를 실행하기 전에 함수, 변수, 클래스 또는 임포트(import)의 선언문해당 범위의 맨 위로 이동시키는 과정"

 

정의에서 분명 선언문해당 범위의 맨 위로 이동시킨다고 하였다. 일단 선언문을 모두 위로 옮겨보자.

var a; // 선언문
function f() { // 선언문
    var b; // 선언문
    b = 10; // 할당문
}
var c; // 선언문

a = 10; // 할당문
c = 30; // 할당문

 

표현문은 선언문 + 할당문의 조합이라고 생각해볼 수 있다. 따라서 위와 같이 나눈다음에 선언문만 맨 위로 이동시켰다. 그런데 우리가 정의에서 아직 안짚고 넘어간 곳이 있다. 바로 '해당 범위의 맨 위로 이동' 이다.

 

여기서 말하는 해당 범위란, 스코프를 말한다. 따라서 선언문이 속해있는 스코프의 맨 위로 이동한다는 뜻이다.

'var'는 함수 스코프이다. 따라서 현재 속해있는 함수의 맨 위로 이동한다는 뜻이다.

 

다음과 같은 다른 예시를 한번 보자.

f();

function f() {
    console.log('call f()');
}

f();

위의 코드는 다음과 같은 결과를 가진다.

따라서 코드는 다음과 같이 동작한다는 것을 알 수 있다.

function f() {
    console.log('call f()');
}

f();
f();

 

 

2. 변수 선언의 3단계

JS는 실행하기 전에 모든 식별자들을 미리 읽는다. 따라서 실행 전에 어떠한 변수가 있고, 어떠한 함수가 있는지 미리 알고있다고 볼 수 있다. 여기서 중요한 것은 식별자를 읽는 다는 것이다. 값까지 읽는 것이 아니다!

 

JS에서 변수는 기본적으로 다음과 같은 3단계를 통해 생성된다고 볼 수 있다.

  1. 선언
  2. 초기화
  3. 할당

하지만 이 3단계에는 'var' 키워드와 'let', 'const' 간의 차이점이 존재한다.

 

1) 'var' 의 변수 선언

'var'로 변수를 선언할 경우에는 위 3가지 단계 중, 선언과 초기화가 동시에 진행된다.

위 과정을 조금만 더 자세히 풀어보면, 호이스팅에 의해 var로 선언한 선언부는 맨 위로 올라오게 된다.

그와 동시에 'undefined'로 초기화가 된다. 다음 코드를 보자.

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

위의 결과는 다음과 같이 나타난다.

따라서 호이스팅에 의해 다음과 같은 코드 처럼 동작한다는 것을 알 수 있다.

var n = undefined;
console.log(n);
n = 10;
console.log(n);

여기서 중요한건 '처럼' 이다. 실제로 저렇게 바뀌어서 동작하는 것이 아니다.

 

2) 'let'의 변수 선언

'let'으로 선언된 변수는 'var'로 선언된 변수와 다르게, 선언과 초기화가 따로 진행이 된다.

위의 코드를 똑같이 쓰는데, 'var'만 'let'으로 바꿔보자.

console.log(n);
let n = 10;
console.log(n);

위의 결과는 다음과 같이 나타난다.

'n'을 초기화 하기 전에 접근할 수 없다는 것이다. 여기서 헷갈릴 수가 있다. 그럼 'let'은 호이스팅이 되지 않는 것인가?

호이스팅의 정의를 다시 한번 등장시켜보자.

 

"인터프리터가 코드를 실행하기 전에 함수, 변수, 클래스 또는 임포트(import)의 선언문 해당 범위의 맨 위로 이동시키는 과정"

 

선언문에 집중해야 한다. 그리고 에러를 다시보자.

 

"Cannot access 'n' before initialization"

즉, 초기화 전에 접근할 수 없다는 것이다. 이는 다시 말하면, 변수는 선언이 되었지만 초기화가 되지 않았기 때문에 메모리를 차지하지 않고, 그렇기 때문에 우리가 접근할 수 없다는 것이다.

 

n은 분명 console.log(n) 다음에 선언했지만, 선언이 되지 않았다는 에러가 아니라 초기화가 되지 않았다는 에러를 보여준다. 즉, 호이스팅에 의해 n도 블록의 맨 위로 이동했다는 뜻이 된다.

 

즉, 'let'도 호이스팅이 된다!!!

 

만약 'n'이 정의 자체가 되지 않은 다음과 같은 코드라면 다음과 같은 결과를 보여줄 것이다.

console.log(n);

 

위와 같이 어떠한 변수가 있다는 것은 알지만, 접근할 수 없는 영역을 TDZ(Temporal Dead Zone) 이라고 한다.

그 영역은 스코프의 처음 지점부터 초기화 시작 지점 까지의 구간을 말한다.

 

즉, 'let' 또한 선언전, 실행 컨텍스트 변수 객체에 등록이 되어 호이스팅이 되지만,

이 TDZ 구간에 의해 메모리가 할당이 되질 않아 참조 에러(ReferenceError) 발생하는 것이다.

 

이 TDZ에 의한 Error 유무가 'var'와 'let'의 차이점 중 하나이다.