모던 자바스크립트에서 호이스팅(Hoisting)

Dean H. Park·2020년 7월 28일
7

JS

목록 보기
6/8
post-thumbnail

이 글의 원문 저작권은 Sukhjinder Arora에게 있습니다.

많은 자바스크립트 프로그래머들은 호이스팅을 자바스크립트가 현재 스코프(함수 또는 전역)의 최상단에 선언부(변수 및 함수)를 이동시키는 행위라고 설명한다. 마치 코드의 상단으로 물리적 이동한다는 것인데, 실제로는 그렇지 않다.

예를들어,

console.log(a);
var a= 'Hello World!';

그들은 위 코드가 호이스팅 되면 아래와 같이 변한다고 말한다.

var a;
console.log(a);
a = 'Hello World!';

비록 호이스팅이 일어나고 있는 것처럼 보일지라도(코드가 정상 동작하기 때문에), 실제로 일어나는 것이 아니며, 코드는 아무데도 가지 않는다. 자바스크립트 엔진은 당신의 코드를 물리적으로 이동시키지 않으며, 타이핑한 곳에 그대로 있다.


그렇다면 호이스팅(Hoisting)이란 뭘까?

컴파일 단계 동안, 코드가 실행되기 마이크로 초 전, 함수와 변수 선언이 스캔된다. 모든 함수와 변수 선언들은 렉시컬 환경이라 불리는 자바스크립트 데이터 구조 내의 메모리에 추가된다. 그리고 소스 코드 내에서 실제 선언되기 전 일지라도 사용할 수 있게 된다.

렉시컬 환경이란 무엇일까?

렉시컬 환경은 식별자-변수 맵핑 데이터 구조이다.
(여기서 식별자는 변수/함수의 이름을 참조하고, 변수는 실제 객체 [포함된 함수 객체] 또는 원시값을 참조한다)

렉시컬 환경의 컨셉은 다음과 같다.

LexicalEnvironment = {
  Identifier:  <value>,
  Identifier:  <function object>
}

다시 말해, 렉시컬 환경은 프로그램이 실행되는 동안 변수와 함수가 존재하는 장소이다.

만약 렉시컬 환경에 대해 더 알고 싶다면 이 글을 확인해보자. (**번역본)

이제 우리는 호이스팅이 실제로 무엇인지 알게 됐고, 함수 및 변수(var, let and const) 선언에 대해 호이스팅이 어떻게 발생하는지 살펴보자.

호이스팅 함수 선언:

helloWorld();  // 'Hello World!'가 콘솔에 찍힌다
function helloWorld(){
  console.log('Hello World!');
}

컴파일 단계에서 함수 선언이 메모리에 추가된다는 것을 이미 알고 있기 때문에 실제 함수 선언 전에 우리 코드로 그 선언에 접근할 수 있다.

위 코드의 렉시컬 환경은 다음과 같다:

lexicalEnvironment = {
  helloWorld: < func >
}

자바스크립트 엔진이 helloWorld() 호출을 접하게되면, 렉시컬 환경 내부를 살펴보고, 함수를 찾은 후에, 실행 시킬수 있게 된다.

함수 표현식 호이스팅

오직 함수 선언부만이 자바스크립트 내부에서 호이스트 되고, 함수 표현식은 호이스트 되지 않는다. 예를 들어, 다음 코드는 동작하지 않는다.

helloWorld();  // TypeError: helloWorld is not a function
var helloWorld = function(){
  console.log('Hello World!');
}

자바스크립트는 오직 선언부만을 호이스트하고, 초기화하지 않는다. 그래서 helloWorld는 함수가 아닌 변수로 취급받는다. 왜냐하면 helloWorld는 var 변수이기에, 엔진은 호이스팅 동안 undefined 값을 할당할 것이다.

때문에 코드는 다음과 같이 동작할 것이다.

var helloWorld = function(){
  console.log('Hello World!');  // 'Hello World!' 출력
}
helloWorld();

var 변수 호이스팅

var 변수 호이스팅을 이해하기 위한 예제를 살펴보자.

console.log(a); // 'undefined' 출력
var a = 3;

3이 나올줄 알았지만, undefined가 나왔다. 왜 일까?

자바스크립트는 초기화가 아닌 오직 선언부만을 호이스트한다는 것을 기억하는가. 컴파일하는 동안, 자바스크립트는 할당값 대신, 오직 함수와 변수 선언부만을 저장한다.

그런데 왜 undefined 일까?

JavaScript 엔진이 컴파일 단계에서 var 변수 선언을 발견하면, 그 변수를 렉시컬 환경에 추가하고, 코드에서 실제 할당이 이루어지는 라인에 도달한 뒤, undefined인 변수에 그 값을 할당하여 초기화한다.

위 코드의 초기 렉시컬 환경은 다음과 같다:

lexicalEnvironment = {
  a: undefined
}

이것이 3 대신 undefined가 나오는 이유이다. 그리고 엔진이 실제 할당이 끝난 라인(실행 도중)에 도달하게되면, 렉시컬 환경의 변수값을 업데이트 하게 된다.

할당한 뒤 렉시컬 환경은 다음과 같다:

lexicalEnvironment = {
  a: 3
}

let 과 const 변수 호이스팅

몇가지 예제를 살펴보자:

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

//결과:
//ReferenceError: a is not defined

let과 const 변수는 호이스팅되지 않은 것일까?

해답은 조금 더 복잡하다. 모든 선언(function, var, let, const 및 class)은 JavaScript에서 호이스팅되며, var 선언은 undefined로 초기화되지만 let 및 const 선언은 초기화되지 않은 상태로 유지된다.

이들은 오직 렉시컬 바인딩 (할당)이 자바스크립트 엔진 런타임 도중 평가 될때만 초기화 된다. 이 뜻은 엔진이 소스코드에서 선언된 위치에 있는 변수를 평가기 전까지 접근할 수 없다는 것이다.

이는 "TDZ(Temporal Dead Zone)"이라 불리며, 변수 생성과 접근불가한 곳이 초기화되기 까지의 시간 간격을 의미한다.

만약 자바스크립트 엔진이 let, const 값을 선언된 라인에서 찾지 못하게되면, undefined 값을 할당하거나 error(const일 경우)를 반환한다.

예제를 살펴보자.

let a;
console.log(a); // outputs undefined
a = 5;

컴파일 단계에서, 자바스크립트 엔진은 변수 a와 마주쳐 렉시컬 환경에 저장하지만, let 변수이기 때문에 엔진은 어떤 값으로도 초기화하지 않는다.

컴파일 단계에서, 렉시컬 환경은 다음과 같다:

lexicalEnvironment = {
  a: <uninitialized>
}

만약 선언 전, 변수에 접근하게 되면, 자바스크립트 엔진은 렉시컬 환경에서 변수값을 fetch하려 시도할 것이다. 왜냐하면 변수는 초기화되자 않았고, error를 보낼것이기 때문이다.

변수가 선언되기 전에 변수에 접근하려고 하면 자바스크립트 엔진은 변수가 초기화되지 않았기 때문에 렉시컬 환경에서 변수의 값을 가져오려고 하고, 참조 에러가 발생한다.

실행 중, 엔진이 변수가 선언된 라인에 접근할 때, 바인딩(값)을 평가하려고 시도한다. 이때 변수와 연관된 값이 없기 때문에 undefined를 할당한다.

렉시컬 환경은 첫째 줄이 실행 된후 다음과 같다.

lexicalEnvironment = {
  a: undefined
}

그리고 undefined가 콘솔에 찍힌 뒤, 렉시컬 환경이 a 값을 undefined에서 5로 업데이트 하여 5로 할당될 것이다.


Note — 코드가 변수 선언 전 실행되지 않는 한, 코드(예: 함수 본문)의 let 및 const 변수를 선언하기 전에 참조하는 것이 가능하다.

예로, 다음 코드는 완벽히 동작한다.

function foo () {
  console.log(a);
}
let a = 20;
foo();  // This is perfectly valid

하지만 참조에러가 발생한다.

function foo() {
 console.log(a); // ReferenceError: a is not defined
}
foo(); // This is not valid
let a = 20;

Class 선언 호이스팅

let과 const 선언같이, 자바스크립트의 class도 또한 호이스팅이 되고, 평가 도중 초기화하지 않은 상태로 유지된다. 이 역시 "TDZ"의 영향을 받는다.

예제:

let peter = new Person('Peter', 25); 
// ReferenceError: Person is not defined

console.log(peter);

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

때문에 class에 접근하기 위해서는, 먼저 선언을 해야만한다.

예제:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
let peter = new Person('Peter', 25); 
console.log(peter);
// Person { name: 'Peter', age: 25 }

컴파일 단계에서, 위 코드의 렉시컬 환경은 다음과 같다:

lexicalEnvironment = {
  Person: <uninitialized>
}

그리고 엔진이 class문을 평가할때, class를 값으로 초기화 할 것이다.

lexicalEnvironment = {
  Person: <Person object>
}

Class 표현식 호이스팅

함수 표현식처럼, class 표현식은 호이스팅되지 않는다. 예를 들어, 이 코드는 동작하지 않는다.

let peter = new Person('Peter', 25); 
// ReferenceError: Person is not defined

console.log(peter);
let Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

올바른 방법은 다음과 같다:

let Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
let peter = new Person('Peter', 25); 
console.log(peter);
// Person { name: 'Peter', age: 25 }

결론

이제 코드에서 호이스팅이 자바스크립트 엔진에 의해 물리적으로 움직이는게 아니란 것을 알게 됐다. 호이스팅 메카니즘의 적절한 이해도를 갖는 것은 잠재적 버그들과 호이스팅으로 인한 혼란을 피할수 있도록 도와줄 것이다.

undefined 변수 또는 참조 에러와 같은 호이스팅의 사이드 이펙트 가능성을 피하기 위해서는, 항상 변수를 각 범위 상단에 선언하고 초기화해야 한다.

profile
Hi, I'm dean. Front-end developer who likes UI/UX Design.

2개의 댓글