개요
지난번에 알아본 스코프(Scope)에 이어 자바스크립트의 개념 중 하나인 호이스팅(Hoisting)에 대해 알아본다.
들어가기에 앞서
지난 글에서는 스코프(Scope)에 대해 알아보았고, 스코프 개념에 대해 어느 정도 익숙해졌을 것이라고 생각한다.
내용을 간략히 정리해 보자면, 자바스크립트는 렉시컬 스코프 동작 방식을 따르며 우리가 작성한 코드에서 변수를 어떻게 그리고 어디서 선언하고 사용하는가에 따라 렉싱(Lexing) 타임에 스코프가 결정된다는 것이다.
자바스크립트의 경우 변수의 선언문이 어디에 위치해 있는가에 따라 스코프에 변수가 추가되는 과정이 조금 달라지는데, 이번 글에서는 이 부분에 대해 알아보려고 한다.
호이스팅(Hoisting)
아래 예시 코드를 살펴보며 이야기해 보자. 아래 예제 코드를 실행시켜 보면 어떤 결과가 출력될까?
name = '근둥';
var name;
function printName() {
console.log(name);
}
printName();
name을
선언하기 전에 값을 할당했고, 다시 name
변수를 선언했으니 undefined
값이 들어있을 것이라고 생각할 수 있다.
하지만 실행 결과는 근둥
이라는 문자열이 출력된다.
그렇다면, 순서를 바꾼 아래 예제 코드를 실행시키면 어떤 결과가 출력될까?
function printName() {
console.log(name);
}
printName();
var name = '근둥';
name
이 선언되기 전에 참조하고 있으니 ReferenceError
가 발생할 것이라고 생각할 수 있다.
하지만 이번 예제 역시 예상한 것과 다르게 아무런 에러가 발생하지 않고 undefined
가 출력된다.
이렇게 동작하는 이유는 호이스팅(Hoisting) 때문인데, 지금부터 위 코드가 어떻게 동작하게 되었는지 알아보도록 하자.
컴파일 타임(Compile Time)
호이스팅은 컴파일레이션(Compilation) 과정과 연관되어 있다.
(컴파일레이션에 대해 다시 살펴보려면 지난 스코프 게시글을 참고하길 바란다)
이 중 렉싱(Lexing) 과정에서 스코프가 결정되고, 이것이 렉시컬 스코프의 동작 방식이라고 설명했다.
이처럼 변수, 함수 등의 선언문이 코드가 실행되기 직전 단계인 컴파일레이션 단계에서 먼저 처리된다.
var a = 0;
이 코드는 자바스크립트에서는 아래와 같이 선언과 할당, 두 개의 구문으로 나눠져 동작한다.
첫 번째 선언문이 컴파일레이션 단계에서 처리되고, 두 번째 대입문은 우리가 작성한 코드와 동일하게 실행 시점에 동작하게 된다.
즉, 선언문은 코드의 범위(스코프) 내에서 최상단으로 끌어올려진다.
앞서 살펴본 예제 코드는 규칙에 따라 아래와 같이 동작하게 될 것이다.
이렇게 코드를 두고 보니 출력된 결과가 왜 그렇게 나왔는지 이해가 될 것이다.
선언문이 스코프의 최상단으로 끌어올려진 것을 확인할 수 있는데, 이것이 바로 호이스팅(Hoisting)이다.
두 예제는 전역 스코프 내에서의 호이스팅을 보여준 예시인데, 아래 예제를 하나 더 살펴보자.
function myFunc() {
console.log(globalValue, localValue);
var localValue = 1;
}
myFunc();
var globalValue = 0;
코드를 실행시켜 보면 두 개의 변수 값 모두 undefined
로 출력된다.
globalValue
변수의 선언문은 선언문이 속한 전역 스코프의 최상단으로 끌어올려지고, localValue
변수의 선언문은 선언문이 속한 함수 스코프(지역 스코프, Local Scope)의 최상단으로 끌어올려진 것을 볼 수 있다.
이 예제를 통해 알 수 있는 것은 호이스팅의 경우 스코프별로 이루어진다는 것이다.
예제에서는 "변수" 선언문을 예로 들었는데, 모든(함수, 클래스 등) 선언문에서 호이스팅이 이루어진다는 것을 알고 있어야 한다.
Temporal Dead Zone(TDZ)
앞서 살펴본 예제는 var
키워드로 선언한 변수를 예로 들어보았다. 변수 선언문 키워드를 let
이나 const
로 변경해 보면 동일하게 동작하지 않고 에러가 발생할 것이다.
let
, const
로 선언한 변수는 호이스팅이 일어나지 않아서 선언 전에 접근할 수 없나 싶을 수 있지만 그것은 틀렸다.
호이스팅은 동일하게 일어나지만, let
, const
로 선언한 변수는 초기화되기 전까지 TDZ(Temporal Dead Zone)라는 공간에 속하게 되며 이 공간에 속하고 있을 때에는 접근이 불가하다.
TDZ에 대해 알아보기 전에 변수를 선언하면 내부에서 어떤 일들이 일어나는지 살펴보자.
이 그림은 변수의 생명 주기를 나타낸 그림이다. 기본적으로 3가지 절차를 거친다.
- Declaration(선언)
- Initialization(초기화)
- Assignment(할당)
TDZ는 이 절차 중 선언과 초기화 사이에 위치하고 있으며 초기화 이전에 접근할 수 없도록 제한한다.
변수 선언이 호이스팅 되어 스코프 최상단으로 끌어올려졌지만, 초기화 이전까지는 TDZ에 속해있기 때문에 접근을 시도할 경우 ReferenceError 가 발생한다.
TDZ 적용 대상
TDZ가 적용되는 대상은 let
, const
로 선언한 변수뿐만 아니라 몇 가지 대상이 더 있는데 TDZ 영향을 받는 대상은 아래에서 확인해 보자.
let 변수
name; // ReferenceError
let name = '근둥';
const 변수
name; // ReferenceError
const name = '근둥';
클래스
Person; // ReferenceError
class Person {}
super
상속받은 클래스의 경우 super를 호출하기 전에 this
에 접근할 수 없는데, 이는 부모의 생성자(super)가 호출되기 전에는 this
바인딩이 TDZ에 속해있기 때문이다.
class Animal {
alive;
}
class Cat extends Animal {
constuructor() {
this.alive = true; // ReferenceError
super();
}
}
함수 기본 매개변수
함수에는 기본 매개변수 값을 지정할 수 있는데, 매개변수 본인 자신을 기본값으로 사용하려고 시도할 경우 ReferenceError 가 발생한다.
function myFunc(value = value /* ReferenceError */) {
}
마무리하며
호이스팅에 대해 이해하지 못한 상태로 프로그래밍을 할 경우 의도치 않은 동작을 마주할 수 있고, 대처하는 데 있어 어려움을 겪을 것이다. 반대로 호이스팅에 대해 이해하고 있다면 의도치 않은 동작을 사전에 미리 방지할 수 있을 것이다.
의도치 않은 동작으로부터 벗어나는 가장 쉬운 방법은 변수 선언 시에는 var
보다 let
, const
를 사용하고, 중복 선언을 피하는 것이다. TDZ를 통해 선언되지 않은 상태로 접근하는 것을 미리 방지할 수 있고, 호이스팅으로 인해 예기치 않은 값이 할당되는 것을 방지할 수 있다.
어려운 개념은 아니기 때문에 꼭 숙지하고 있길 바라며, 다른 주제로 찾아오도록 하겠다.