[Javascript] 호이스팅(hoisting)에 대하여

박기영·2022년 12월 10일
4

Javascript

목록 보기
27/45

자바스크립트를 깊게 공부하려고 할 때, 자주 등장하는 단어 중 하나가 호이스팅(hoisting)이다.
많은 게시글들에서 함수 혹은 변수를 가장 상단으로 올려주는 효과를 내준다는 말을 한다.
오늘은 이 개념이 정확히 어떤 것인지에 대해 정리해보자.

Sukhjinder Arora님의 글을 참조하였습니다

호이스팅(hoisting)이란?

우선 hoist라는 단어의 뜻부터 살펴보자.

hoist : (흔히 밧줄이나 장비를 이용하여) 들어[끌어]올리다

그렇다. 끌어올린다는 뜻을 가지고 있다.
그렇다면 자바스크립트에서 hoisting은 어떤 의미를 가지는걸까?

JavaScript에서 호이스팅(hoisting)이란, 인터프리터가 변수와 함수의 메모리 공간을 선언 전에 미리 할당하는 것을 의미합니다.
- MDN docs -

다른 게시글들을 참고해보면 아래와 같이 말하기도 한다.

변수와 함수의 선언부가 각자의 현재 스코프 영역 최상단으로 옮겨지는 것

결국, 같은 말이다.
코드가 시작되기 전에 변수, 함수 선언이 해당 스코프의 최상단으로 끌어 올려지는 것을 말한다.
그러나! 정확히 말하면 끌어 올려지는 것처럼 보이는 현상을 말한다.
즉, 물리적으로 코드가 위로 옮겨진다는 것이 아니다.
코드는 그 자리에 그대로 있다.

??????

이 뭔 개-소리야?(대충 사극 밈이라는 뜻)
뭐 어쩌라는건지 모르겠다. 아니 그래서 옮겨진다는거야 아니라는거야?
이 것을 이해하기 위해서는 자바스크립트 엔진이 어떻게 코드를 실행하는지 알아봐야한다.

Lexical Environment

During compile phase, just microseconds before your code is executed, it is scanned for function and variable declarations. All these functions and variable declarations are added to the memory inside a JavaScript data structure called Lexical Environment. So that they can be used even before they are actually declared in the source code.
- Sukhjinder Arora 's Medium -

컴파일 단계에서, 당신의 코드가 실행되기 몇 마이크로 초 전에, 함수와 변수 선언이 스캔된다.
스캔된 모든 함수, 변수 선언은 Lexical Environment라고 불리는 자바스크립트 데이터 구조 내 메모리에 추가된다.
따라서, 함수, 변수가 소스 코드 내에서 실제로 선언되기 이전부터 사용 할 수 있다.
- 필자의 미약한 번역 -

외국인 개발자 분의 Medium 블로그 게시글을 가져왔다.
자바스크립트 엔진은 코드를 실행하기 전에 Lexical Environment 내에 있는 메모리에 함수, 변수 선언을 추가한다고한다.

이 덕분에, hoisting이라는 개념을 우리가 접할 수 있고, 눈으로 볼 수 있는 것이었다!

그러면 Lexical Environment라는 것은 어떻게 생겨먹은 것일까?

A lexical environment is a data structure that holds identifier-variable mapping. (here identifier refers to the name of variables/functions, and the variable is the reference to actual object [including function object] or primitive value).
- Sukhjinder Arora 's Medium -

Lexical Environment는 "identifier-variable" 매핑 정보를 가지고 있는 데이터 구조이다.
"identifier"는 변수, 함수의 이름을 가리키며, "variable"은 실제 객체 혹은 원시값을 가리킨다.
- 필자의 미약한 번역 -

즉, 아래와 같은 구조라는 것이다.

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

코드가 실행되는 동안 변수와 함수가 살고있는 공간이라고 보면 되겠다.

흠...그러면 이제 예시 코드를 보면서 알아보자.

호이스팅 예시

함수 선언식의 호이스팅

helloWorld(); // Hello World!

function helloWorld(){
  console.log('Hello World!');
}

앞서 말했듯이, 함수 선언부는 컴파일 단계에서 메모리에 추가된다.
덕분에, 실제 함수 선언을 만나기 전에 함수에 접근할 수 있다.

Lexical Environment에서의 상황을 보면 아래와 같다.(컴파일 직전 몇 마이크로 초 전의 상황 🤔)

lexicalEnvironment = {
  helloWorld: < func >
}

따라서, 자바스크립트 엔진이 helloWorld()를 마주치면,
엔진은 Lexical Environment를 살펴보고,
함수를 찾아서 그 것을 실행시킬 수 있게된다.

함수 표현식의 호이스팅

함수 선언식이 hoisting이 되는 것에 반해, 함수 표현식은 hoisting이 되지않는다.

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

var helloWorld = function () {
  console.log("Hello World!");
};

...var로 해서 그런걸지도 몰라! let이나 const로 해보자!

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

const helloWorld = function () {
  console.log("Hello World!");
};

let, const 모두 에러가 발생한다. 그런데..에러 내용이 다르다!
이 부분에 대해서는 더 뒤에서 나중에 다뤄보자.

아무튼, 함수 표현식은 호이스팅이 되지않는 것을 알 수 있다.
왜 그럴까?

As JavaScript only hoist declarations, not initializations (assignments), ....
- Sukhjinder Arora 's Medium -

자바스크립트는 오직 선언만을 hoist한다. 초기화(할당)를 hoist하는 것이 아니다.
- 필자의 미약한 번역 -

위와 같은 이유로 인해서, helloWorld함수가 아니라 변수로 취급이 된다.
helloWorldvar 변수이기 때문에, hoisting 중에는 엔진이 undefined를 할당 할 것이다.

따라서, 함수 표현식을 실행하기 위해서는 아래와 같이 사용해야한다.

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

helloWorld(); // Hello World!

함수를 먼저 만들고, 사용은 그 아래에서 해야한다.

var의 호이스팅

이번에는 많은 분들이 공부하면서 자주 봤을 var 변수에 대한 hoisting을 알아봅시다!

아래 코드를 보자.

console.log(a); // undefined

var a = 3;

변수가 선언되기 전에 변수를 사용했기 때문에 에러가 발생할 것이라고 생각했다.
그러나, 에러는 발생하지 않았고, undefined가 출력되었다.
왜 그럴까? 우린 이미 답을 알고있다.

자바스크립트는 선언을 hoist하는 것이지, 초기화(할당)을 hoist하지는 않는다.

반복해서 말하지만 컴파일 단계에서 오직 변수, 함수 선언을 저장하는 것이지
초기화(할당)을 저장하는 것이 아니다.

왜 undefined인가요?

When JavaScript engine finds a var variable declaration during the compile phase, it will add that variable to the lexical environment and initialize it with undefined and later during the execution when it reaches the line where the actual assignment is done in the code, it will assign that value to the variable.
- Sukhjinder Arora 's Medium -

자바스크립트 엔진이 컴파일 단계에서 var 변수 선언을 찾았을 때,
엔진은 변수를 Lexical Environment에 저장하고 undefined로 초기화한다.
나중에 코드를 실행하며 실제로 할당이 이뤄지는 코드 라인에 도달했을 때,
엔진은 변수에 값을 할당한다.
- 필자의 미약한 번역 -

따라서, 초기 Lexical Environment는 아래와 같다.

lexicalEnvironment = {
  a: undefined
}

이로 인해서, 위 예시에서 undefined가 출력된 것이다.

엔진이 코드를 실행하며 할당이 이뤄지는 코드 라인을 만나면
Lexical Environment 내에서 변수의 값을 업데이트하는데,
그 때의 Lexical Environment는 아래와 같다.

lexicalEnvironment = {
  a: 3
}

즉, 아래와 같은 형태로 코드가 작동하고 있는 것이라고 볼 수 있겠다.

var a;

console.log(a); // undefined

a = 3;

다만, 주의할 것은 이 내용은 varhoisting에 대한 것이지,
모든 변수hoisting에 대한 것이 아니라는 점이다.

let, const의 호이스팅

아래 예시를 보자.

console.log(a); // ReferenceError: Cannot access 'greeting' before initialization

let a = 3;
console.log(a); // ReferenceError: Cannot access 'greeting' before initialization

const a = 3;

이번에는 var에서와는 다르게 에러가 발생했다.
let, consthoisting이 안되는걸까?

All declarations (function, var, let, const and class) are hoisted in JavaScript, while the var declarations are initialized with undefined, but let and const declarations remain uninitialized.
- Sukhjinder Arora 's Medium -

자바스크립트에서 모든 선언(함수, var, let, const, class ..)은 hoist된다.
var 선언이 undefined로 초기화되는 반면, let과 const 선언은 uninitialized로 남아있다.
- 필자의 미약한 번역 -

그렇다. hoisting이 안되는 것이 아니다. 모두 hoisting이 된다.
그러나, varundefined로 초기화까지 되는 반면
let, constuninitialized로 남아있다는 것이다.

They will only get initialized when their lexical binding (assignment) is evaluated during runtime by the JavaScript engine. This means you can’t access the variable before the engine evaluates its value at the place it was declared in the source code. This is what we call “Temporal Dead Zone”, A time span between variable creation and its initialization where they can’t be accessed.
- Sukhjinder Arora 's Medium -

let, const의 초기화는 오직 자바스크립트 엔진 작동 중 lexical binding(할당)이 발생했을 때에만 이루어진다.
이는 엔진이 소스 코드 내에서 let, const가 선언된 장소에서 그 값을 평가하기 전까지는
당신이 변수에 접근할 수 없다는 것을 의미한다.
변수의 생성과 초기화 사이의 기간에 접근 할 수 없는 것을 "Temporal Dead Zone"이라고 한다.
- 필자의 미약한 번역 -

let, const의 초기화는 자바스크립트 엔진이 해당 코드 라인에 도달했을 때에만 발생한다는 것이다.
그 전에는 우리가 변수에 접근할 수 없다.

만약, letconst가 선언된 부분에서 여전히 엔진이 값을 찾지 못한다면,
undefined가 할당되거나, 에러를 발생시킨다(const의 경우).

let a;

console.log(a); // undefined

a = 3;
const a;

console.log(a); // SyntaxError: Missing initializer in const declaration

a = 3;

let에 대한 예시를 단계별로 살펴보자.

컴파일 단계에서 자바스크립트 엔진이 변수 a를 만나면, Lexical Environment에 저장한다.
그러나, let으로 선언된 변수이므로 엔진은 이를 어떠한 값으로도 초기화하지않는다.
따라서 Lexical Environment는 아래와 같다.

lexicalEnvironment = {
  a: <uninitialized>
}

만약, 변수가 선언되기 전에 접근하려고 하면,
자바스크립트 엔진은 Lexical Environment에서 변수의 값을 찾으려고한다.
그러나 변수는 uninitialized 상태이므로 ReferenceError를 던진다.

코드가 실행되는 중에, 자바스크립트 엔진이 변수가 선언된 코드 라인에 도달하게 되면
엔진은 변수의 값을 찾는데, 변수는 현재 연결되어 있는 값이 없으므로 undefined를 할당한다.
그 때의 Lexical Environment는 다음과 같다.

lexicalEnvironment = {
  a: undefined
}

따라서, undefined가 콘솔에 찍힌다.
그 후, 3이라는 값이 변수 a에 할당되며, Lexical Environment는 변수의 값을 업데이트한다.
그 때의 Lexical Environment는 다음과 같다.

lexicalEnvironment = {
  a: 3
}

참고로, let, const가 선언되기 전에 접근할 수 있는 경우도 있다.

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

let a = 20;

foo(); // 20

물론 이 코드는 아래와 같이 사용하면 에러가 발생한다.

function foo() {
  console.log(a); // ReferenceError: Cannot access 'a' before initialization
}

foo();

let a = 20;

class 선언의 호이스팅

class 또한 hoisting이 발생한다.
그리고 let, const와 같이 선언 코드 라인을 만나기 전까지는 uninitialized이다.
즉, Temporal Dead Zone의 영향을 받는다.

예시를 통해 살펴보자.

let peter = new Person("Peter", 25); // ReferenceError: Cannot access 'Person' before initialization

console.log(peter);

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

초기화가 되기 전이므로 접근할 수 없다는 에러가 발생한다.
따라서, 접근하기 위해서는 아래와 같이 코드를 작성해야한다.

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 }

Lexical Environment 관점에서 다시 살펴보자.

컴파일 단계에서 Lexical Environment는 아래와 같을 것이다.

lexicalEnvironment = {
  Person: <uninitialized>
}

코드가 실행되기 시작하면, 아래와 같이 업데이트 될 것이다.

lexicalEnvironment = {
  Person: <Person object>
}

class 표현식의 호이스팅

함수 표현식의 hoisting과 같이, class 표현식 또한 hoisting이 되지않는다.

아래 코드는 에러를 발생시킨다.

let peter = new Person("Peter", 25); // ReferenceError: Cannot access 'Person' before initialization

console.log(peter);

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

따라서, class 표현식을 올바르게 사용하기 위해서는
사용하기 전에 선언을 해야한다.

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 }

결론

hoisting을 잘 이해하고 사용하자.
변수는 선언과 할당을 동시에 하도록 노력하고,
사용해야하는 스코프보다 위에서 미리 만들어두도록하자.

참고 자료

Sukhjinder Arora님 블로그
cada님 블로그
Tecoble 카일님 게시글

profile
나를 믿는 사람들을, 실망시키지 않도록

1개의 댓글

comment-user-thumbnail
2023년 12월 11일

잘읽었습니다

답글 달기