호이스팅

김맥스·2023년 3월 11일
0

자바스크립트

목록 보기
1/5
post-thumbnail

출처 : You Don’t Know JS

1. 호이스팅

개념

  • 함수 안에 있는 선언들을 모두 끌어올려서 해당 함수 유효 범위의 최상단에 선언하는 것
  • 자바스크립트 함수는 실행되기 전에 함수 안에 필요한 변수값들을 모두 모아서 유효 범위 최상단에 선언한다. 자바스크립트 Parser가 함수 실행 전 해당 함수를 한 번 훑고 함수 안에 존재하는 변수/함수선언에 대한 정보를 기억하고 있다가 실행시킨다
  • 유효 범위 → 함수 블록 { }
  • 다시 말해서, 함수 내에서 아래쪽에 존재하는 내용 중 필요한 값들을 끌어올리는 것이다.
  • 실제로 코드가 끌어올려지는 것은 아니며, 자바스크립트 Parser 내부적으로 끌어올려서 처리하는 것.
  • 실제 메모리에서는 변화가 없다
  • 자바스크립트에서 호이스팅(Hoisting)이란 변수와 함수의 선언문을 해당 스코프의 꼭대기로 끌어올리는 동작을 의미한다. 이 과정에서 선언문 자체는 옮겨지지만, 함수 표현식의 대입문을 포함한 모든 대입문은 끌어올려지지 않는다.

대상

  • var 변수 선언과 함수선언문에서만 호이스팅이 일어난다
  • var 변수/함수의 선언만 위로 끌어 올려지며, 할당은 끌어 올려지지 않는다.
  • let/const 변수 선언과 함수표현식에서는 호이스팅이 발생하지 않는다.
  • 자바스크립트는 모든 선언(var, let, const, function, functiton*, class)을 호이스팅한다.

예시

// 1번 코드
a = 2;
var a;
console.log(a); // ?

var a 선언으로 인해 해당 변수가 재정의되어 기본값인 undefined가 출력될 것이라고 예상할 것이다. 하지만, 이 코드의 실행 결과로 2가 출력된다. 한 번 더 결과를 예측해보자.

// 2번 코드
console.log(a); // ?
var a = 2;

출력 결과는 2나 ReferencError가 아닌, undefined이다. 이러한 동작을 살펴보기 위해선 3단계에 걸친 변수의 생성 과정에 대해 살펴볼 필요가 있다.

변수의 생성과정 3단계 - 자세한 내용은 PoiemaWeb의 Execution Context를 통해 알아보자.

  1. 선언 단계 (Declaration Phase)변수를 실행 컨텍스트의 변수 객체(Variable Object)에 등록한다. 이 변수 객체는 스코프가 참조하는 대상이 된다.
  2. 초기화 단계 (Initialization Phase)변수 객체(Variable Object)에 등록된 변수를 위한 공간을 메모리에 확보한다. 이 단계에서 변수는 undefined로 초기화된다.
  3. 할당 단계 (Assignment phase)undefined로 초기화된 변수에 실제 값을 할당한다.

위의 예제와 같이 var 키워드로 선언된 변수는 선언 단계와 초기화 단계가 한번에 이루어진다.

  1. 즉, 스코프에 변수를 등록(선언 단계)하고 메모리에 변수를 위한 공간을 확보한 후, undefined로 초기화(초기화 단계)한다.
  2. 따라서 변수 선언문 이전에 변수에 접근하여도 스코프에 변수가 존재하기 때문에 에러가 발생하지 않고, undefined를 반환한다.
  3. 이후 변수 할당문에 도달하면 비로소 값이 할당된다.

이러한 현상을 변수 호이스팅(Variable Hoisting)이라 한다.

https://poiemaweb.com/es6-block-scope

이에 근거해서 위의 예제를 해석하면 다음과 같다.

// 1번 코드
a = 2;
var a;
console.log(a);

// 실제 동작
var a; // 호이스팅, 선언과 초기화가 동시에!
a = 2;
console.log(2);
// 2번 코드
console.log(a);
var a = 2; // 이 구문은 실제로는 var a; + a = 2; 두 개의 구문으로 나누어진다.

// 실제 동작
var a; // 호이스팅, 선언과 초기화가 동시에!
console.log(a);
a = 2;

책의 비유를 빌리자면, 달걀(선언문)이 닭(대입문)보다 먼저인 것이다

2. var vs let

한편, var과 달리 let 키워드로 선언된 변수는 선언 단계와 초기화 단계가 분리되어 진행된다.

  1. 즉, 스코프에 변수를 등록(선언 단계)하지만, 메모리 확보와 undefined로 초기화하는 작업(초기화 단계)은 변수 선언문에 도달했을 때 이루어진다.
  2. 초기화 이전에 변수에 접근하려고 하면 참조 에러(ReferenceError)가 발생한다. 이는 변수가 아직 초기화되지 않았기 때문이다.다시 말하면 변수를 위한 메모리 공간이 아직 확보되지 않았기 때문이다.
  3. 따라서 스코프의 시작 지점부터 초기화 시작 지점까지는 변수를 참조할 수 없다.

스코프의 시작 지점부터 초기화 시작 지점까지의 구간을 ‘일시적 사각지대(Temporal Dead Zone; TDZ)’라고 부른다.

https://poiemaweb.com/es6-block-scope

다음의 예제를 살펴보자. 위에서 var 키워드로 선언된 변수와 확연한 차이를 보인다.

// https://poiemaweb.com/es6-block-scope

// 스코프의 선두에서 선언 단계가 실행된다.
// 아직 변수가 초기화(메모리 공간 확보와 undefined로 초기화)되지 않았다.
// 따라서 변수 선언문 이전에 변수를 참조할 수 없다.
console.log(foo); // ReferenceError: foo is not defined

let foo; // 변수 선언문에서 초기화 단계가 실행된다.
console.log(foo); // undefined

foo = 1; // 할당문에서 할당 단계가 실행된다.
console.log(foo); // 1

이렇게만 보면 ES6에서는 호이스팅이 발생하지 않는 것처럼 보인다. 하지만, 그렇지 않다. 다음의 코드를 보자.

// https://poiemaweb.com/es6-block-scope

let foo = 1; // 전역 변수

{
  console.log(foo); // ReferenceError: foo is not defined
  let foo = 2; // 지역 변수
}

위 예제의 경우엔 전역 변수 foo의 값이 출력될 것처럼 보인다. 하지만, ES6의 선언문도 여전히 호이스팅이 발생하기 때문에 참조 에러(ReferenceError)가 발생한다.

  1. ES6의 let으로 선언된 변수는 블록 레벨 스코프를 가지므로 코드 블록 내에서 선언된 변수 foo는 지역 변수이다.
  2. 따라서 지역 변수 foo도 해당 스코프에서 호이스팅되고 코드 블록의 선두부터 초기화가 이루어지는 지점까지 일시적 사각지대(TDZ)에 빠진다.
  3. 결국 전역 변수 foo의 값이 출력되지 않고 참조 에러(ReferenceError)가 발생한다.

3. 함수 선언문 vs 함수 표현식

지금까지 변수에 관련된 호이스팅만 언급했지만, 함수에서도 마찬가지로 호이스팅이 발생한다. 함수 foo의 선언문은 호이스팅되었기 때문에 첫째 줄에서 foo를 호출할 수 있다. 이때, 호이스팅이 스코프별로 작동한다는 점을 명심해야 한다.

foo();

function foo() {
    console.log(a); // undefined
    var a = 2;
}

// 위 코드의 실제 동작
function foo() {
    var a;
    console.log(a);
    a = 2;
}

foo();

함수 선언문은 이와 같이 호이스팅되지만, 함수 표현식은 다르다.

foo(); // TypeError: foo is not a function

var foo = () => {
    console.log(a);
    var a = 2;
}

변수 식별자 foo는 호이스팅되어 글로벌 스코프에 묶이므로 foo( ) 호출은 실패하지 않고, ReferenceError도 발생하지 않는다. 그러나 foo는 아직 값을 갖고 있지 않은 데 반해(함수 표현식이 아닌 진짜 선언문으로 생성된 것처럼), foo( )가 undefined 값을 호출하려해서 TypeError라는 에러를 발생시킨다.

재밌는 것은 ES6 방식으로 변수를 선언하면 ReferenceError가 나온다.

foo(); // ReferenceError: Cannot access 'foo' before initialization

const foo = () => {
    console.log(a);
    var a = 2;
}

지금까지의 내용을 이해한 사람이라면 출력된 에러가 왜 차이가 나는지 알 것이라고 믿는다.

  1. 함수가 먼저 호이스팅된다.

함수와 변수 선언문은 모두 호이스팅되지만, 함수 선언문이 먼저 호이스팅되고 다음으로 변수 선언문이 호이스팅된다는 특징이 있다.

foo(); // 1
var foo;

function foo() {
    console.log(1);
}

foo = () => {
    console.log(2);
}

자바스크립트 엔진은 위의 코드를 다음과 같이 해석한다.

function foo() {
    console.log(1);
}

foo();

foo = () => {
    console.log(2);
}

책에서는 중복 함수 선언문에 대한 내용도 언급되었지만, 여기서 다루는 건 혼란을 가중시킬 수 있다는 생각이 있어 마치고자 한다. 중요한 것은 중복 선언을 피해야 한다는 것이고, 특히 일반 변수 선언과 함수 선언을 섞어 사용하면 더 위험하다.

profile
3년차 백엔드 개발자의 공부 기록

0개의 댓글