들어가며
이 글은 dev.to 의 cclintris가 작성한 JavaScript: Demystify Hoisting in JS 라는 글을 한글로 재작성하였습니다.
원문은 https://dev.to/cclintris/javascript-demystify-hoisting-in-js-306b 에서 확인 할 수 있습니다.
이 글을 쓰게 된 가장 큰 이유 중 하나는 몇몇 동료들이 JS에서의 호이스팅에 대해 이야기하는 것을 들은 적이 있기 때문입니다. 당시에는 동료들의 관점을 이해하기가 너무 어려워 인터넷에서 관련 글들을 검색해보기 시작했습니다. 이것은 몇 분 안에 이해할 수 있는 것이 아닌 것 같아서 자바스크립트의 호이스팅을 제대로 이해해보고자 이 글을 쓰기 시작했습니다.
호이스팅은 무엇일까요?
더 이상 고민하지 않고 예를 살펴보겠습니다. 선언되지 않은 변수에 액세스하려고 하면 어떻게 될까요?
console.log(a);
// ReferenceError: a is not defined
브라우저에 레퍼런스 에러로 a 는 아직 정의되지 않았다는 메시지가 표시됩니다. 하지만 만약에
console.log(a); // undefined
var a;
어떻게 이런 일이 일어날 수 있을까요? 논리적으로 말해서 코드는 한 줄씩 실행되기 때문에 위와 같이 레퍼런스 에러가 발생해야 합니다. undefined가 출력되는 이유는 무엇일까요?
이것은 자바스크립트의 호이스팅, state lifting 때문입니다. state lifting은 var a 라인이 맨 위로 "들어 올려진다"는 의미입니다. 따라서 JS 엔진(V8)이 이 코드를 구문 분석할 때에는 실제로 다음과 같이 동작하게 됩니다.
var a;
console.log(a); // undefined
하지만 참고하세요! JS 엔진이 코드를 물리적으로 옮겼다는 것은 상상에 불과합니다!
코드를 다음과 같이 변경하면 어떻게 될까요?
console.log(a); // undefined
var a = 10;
여전히 undefined 가 출력됩니다. 왜 그럴까요? var a = 10 라인이 호이스팅 된다면 10이 출력되어야 하지 않을까요?
실제로 호이스팅이 일어날 때, 변수 선언만 호이스팅되고 할당은 호이스팅되지 않습니다.
따라서 실제로 위의 코드는 JS 엔진에서는 다음과 같이 동작합니다.
var a;
console.log(a); // undefined
a = 10;
따라서 실제로 var a = 10은 두 단계로 분해될 수 있습니다. 먼저 선언 부분인 var = a가 먼저 호이스팅되고 두 번째 단계인 a = 10은 유지되어 호이스팅에 포함되지 않습니다.
지금까지는 쉽게 이해 되실 수 있지만, 이제 조금 혼란스러우실 수 있습니다. 다음 예를 살펴보겠습니다.
function func(h) {
console.log(h);
var h = 3;
}
func(10);
위 예제는 어떻게 출력될까요? 아래와 같이 호이스팅 될까요?
function func(h) {
var h;
console.log(h);
h = 3;
}
func(10);
당연히 undefined 가 출력될 것이라고 예상하셨을 수 있습니다. 하지만 실제 출력 결과를 보면 충격적이게도 10이 출력됩니다.
사실 호이스팅 과정은 맞지만 우리가 간과한 것은 함수의 호출 부분 입니다. 이 코드는 실제로 아래와 같이 동작합니다.
function func(h) {
var h = 10;
var h;
console.log(h);
h = 3;
}
func(10);
하지만 여전히 조금 이상합니다. var h가 10으로 할당되고 다시 var h가 할당없이 정의되는데, 이러면 undefined가 호출되어야 하는 것 아닐까요?
좀 더 심플한 예를 들어 보겠습니다.
var a = 10;
var a;
console.log(a); // 10
위의 예시는 실제로 아래와 같이 동작합니다.
var a;
var a;
a = 10;
console.log(a);
위와 같이 동작하기 때문에 10이 출력됩니다. 이걸 보고 우리는 도대체 무슨 일이야! 이건 불합리한 규칙이야! 누가 이걸 알고 쓴단말이야! 라고 생각할 수 있습니다. 이 기분을 참고 마지막 예를 살펴보겠습니다.
console.log(a);
var a;
function a() {}
위에 배운것을 참고해서 아래와 같이 동작한다고 생각해보겠습니다.
var a;
console.log(a);
function a() {}
출력은 undefined여야 합니다. 그렇게 출력되었을까요? 결과는 ƒ a() {} 가 출력됩니다. 이 시점에서 또다른 충격에 빠집니다. 호이스팅에는 변수 선언 외에도 함수도 적용됩니다. 또한, 함수 선언의 리프팅의 매칭 우선 순위는 변수 선언의 우선 순위보다 높습니다. 따라서 실제로 위의 코드는 다음과 같이 상상해야 합니다.
function a() {}
var a;
console.log(a);
여러가지 예시로 설명을 드렸는데, 조금 정리하자면 다음과 같습니다.
- 변수 선언과 함수 선언 모두 호이스팅 됩니다.
- 변수는 할당이 아닌 선언부만 호이스팅 됩니다.
- 함수에는 전달 된 매개변수가 있다는 사실을 잊으면 안됩니다.
let & const 와 호이스팅
위에서 호이스팅의 개념에 대해서 소개할 때 변수 선언으로 var 키워드를 사용하였습니다. 그러나 ES6에서는 let과 const가 도입되었고 주류에서는 더 이상 var 사용을 권장하지 않고 대신 let을 const와 함께 사용한다는 것은 누구나 알고 있습니다.
let과 const의 경우 실제로 호이스팅의 개념은 비슷합니다. 여기에서 몇 가지 예를 살펴보겠습니다.
console.log(a);
let a;
// ReferenceError: a is not defined
출력 결과는 ReferenceError: a is not defined 입니다. 사실 이게 더 상식적입니다. 하지만 이것이 let 키워드를 사용하여 함수를 선언할 때 호이스팅이 일어나지 않는다는 것을 의미할까요? 그렇다면 좋겠지만 불행히도 그렇지 않습니다. 아래의 예를 살펴보겠습니다.
var a = 10;
function func() {
console.log(a);
let a;
}
let이 호이스팅되지 않는다면 출력은 10이어야 합니다 그렇죠? 외부에 var a = 10이 있고 let a는 호이스팅이 되지 않기 때문입니다. 또 틀렸습니다. 출력은 undefined입니다. 그래서 실제로 let도 호이스팅 되지만 let의 경우 동작이 var와 다르기 때문에 언뜻 보기에는 호이스팅이 없는 것처럼 보입니다. 동작 방식에 대해서는 나중에 알아보겠습니다.
여기에서 잠시 멈추고 호이스팅이 무엇인지 알아보겠습니다. 사실 let, const를 잘 사용하고 활용한 다음 변수 할당을 적절하게 선언하면 문제가 없을 것입니다. 그러나 더 철저하고 더 깊이 이해하고 싶다면 아래 내용을 계속 읽어 주십시오. 다음으로, 우리는 먼저 호이스팅에 대한 두 가지 중요한 질문에 대해 알아보겠습니다.
왜 호이스팅 되는가
위에서 언급한 호이스팅의 몇 가지 규칙과 개념을 검토하면 그것이 가져오는 이점을 알 수 있습니다. 이 질문에 답하기 위해 다른 측면에서 생각할 수 있습니다. "호이스팅이 없으면 어떻게 될까요?"
호이스팅이 없으면 변수를 사용하기 전에 선언해야 합니다. 그러나 이것은 실제로 매우 좋습니다. 결국 프로그래밍 할 때 모두가 이렇게 씁니다. 아무도 코딩하지 않을 것이라고 생각하고 그 동안 JS의 호이스팅 메커니즘을 생각한 다음 변수를 선언하지 않고 직접 사용하지 않습니까? 그래서 이것은 실제로 좋습니다.
호이스팅 없이는 함수를 사용할 때 위에서 선언하고 정의해야 한다고 규정하고 있습니다. 얼핏 보면 별거 아닌 것 같지만 사실 좀 번거롭습니다. 왜냐하면 이것은 모든 함수를 맨 위에 놓는 것만으로도 아래에서 호출되는 모든 함수가 정상적으로 실행될 수 있다는 것을 완전히 보장할 수 있다는 것을 의미하기 때문입니다. 이제 좀 지저분하지 않나요?
마지막 포인트가 더 흥미롭습니다. 호이스팅 없이는 함수 간에 서로를 호출할 수 없습니다. 이것은 무엇을 의미 할까요? 아래 코드를 살펴보십시오.
function loop_1() {
console.log("loop 1");
loop_2();
}
function loop_2() {
console.log("loop 2");
loop_1();
}
이 코드는 이해하기 어렵지 않습니다. loop_1과 loop_2는 서로를 호출합니다. 그러나 문제가 있습니다. 호이스팅이 없으면 어떻게 loop_1이 loop_2 위에 있고 동시에 loop_2도 loop_1 위에 있을 수 있을까요? 이 코드는 호이스팅 없이는 동작하지 않습니다.
결론적으로 이러한 문제를 해결하기 위한 것이 호이스팅입니다.
호이스팅은 정확히 어떻게 동작할까요?
먼저 JavaScript의 Execution Context라는 개념을 이해해야 하며, 이를 EC로 약칭합니다. EC의 개념은 함수가 입력될 때마다 함수에 EC가 있고 EC가 스택에 푸시된다는 것입니다. 함수가 실행되면 EC가 팝업됩니다.
일반적으로 EC는 각각의 function에 대한 정보를 저장합니다. 함수가 무언가를 필요로 할 때, 그것을 찾기 위해 자체 EC로 갈 것입니다.
각 EC에는 해당 VO(Variable Object)가 있습니다. 이 VO는 함수의 변수, 함수, 함수의 매개변수를 포함한 모든 정보를 저장하는 것입니다. VO 검색 메커니즘은 다음을 의미합니다.
위의 var a = 10을 예로 들면 첫 번째 단계는 VO에 새 속성 a를 추가한 다음 a라는 속성을 찾아 10으로 설정하는 것입니다.
Step1: var a
Step2: a = 10
글쎄, 함수에는 너무 많은 것들이 있는데, 각 EC의 VO에 모든 것을 넣는 것은 어떻게 작동합니까?
매개변수의 경우 VO에 직접 입력되며 일부 매개변수가 값과 함께 전달되지 않으면 해당 값이 정의되지 않은 상태로 초기화됩니다. 다음 예를 확인해보겠습니다.
function func(a, b, c) {
...
...
}
func(10)
위의 함수가 호출되면 VO는 다음과 같이 보일 것입니다.
// VO
{
a: 10,
b: undefined,
c: undefined
}
함수에 다른 함수 선언이 있으면 VO에도 추가되므로 문제가 없습니다. 그러나 함수의 이름이 변수 이름과 우연히 같은 경우에는 어떻게 될까요?
function func(a) {
function a() {
...
...
}
}
func(10)
VO는 다음과 같습니다.
{
a: function a
}
따라서 위의 예와 같이 함수 선언이 변수 선언보다 우선한다는 것을 알 수 있습니다. 매개변수 a는 함수 a에 의해 덮어쓰여집니다.
함수 내부의 변수 선언의 경우 마지막에 VO에 넣습니다. VO에 동일한 이름의 속성이 이미 있는 경우 변수는 직접 무시되고 원래 값은 수정되지 않습니다.
요약하자면, 위에서 언급한 VO의 동작을 기능을 실행하기 전에 선행 작업으로 생각할 수 있습니다. 순서는 다음과 같습니다.
- 1단계: 매개변수를 VO에 넣은 다음 각각 들어오는 값이 있는지 확인합니다. 매개변수는 선언된 순서대로 일치합니다. 일치하지 않으면 undefined 값이 할당됩니다.
- 2단계: 함수에서 멤버 메서드, 즉 다른 함수를 찾아 VO에 넣습니다. 현재 VO에 있는 속성과 이름이 같은 경우 이전 속성을 덮어씁니다.
- 3단계: 마지막으로 함수에서 변수 선언을 찾아 VO에 넣습니다. 현재 VO에 있는 속성과 이름이 같은 경우 현재 상태가 우선합니다.
위에서 언급한 예를 다시 살펴보겠습니다.
function func(h) {
console.log(h);
var h = 3;
}
func(10);
각 기능의 실행은 실제로 두 단계로 나눌 수 있습니다. 먼저 함수의 실행 컨텍스트에 진입한 다음 자체 VO를 준비하기 시작합니다. 위의 예의 경우 우선 호출에 매개변수가 있으므로 VO에서 h라는 변수를 먼저 선언하고 값은 10이 됩니다. 그러면 함수에서 멤버 함수를 찾을 수 없으므로 변경되지 않은 상태로 유지됩니다. 마지막으로 var h = 3을 찾으면 변수 선언문이므로 VO에 추가해야 하지만 이때 VO는 이미 h라는 변수이기 때문에 VO는 변하지 않습니다. 이 함수의 VO가 설정되었습니다.
// func() VO
{
h: 10
}
VO를 생성한 후 이 함수를 실행합니다. 코드가 console.log(h)로 실행될 때 VO를 조회하고 값이 10인 h라는 변수가 있음을 발견하므로 10입니다. 그래서 위의 질문에 대한 답변이 나왔고 실제로 10이 출력되었습니다.
코드가 이렇게 변경되면 어떻게 될까요?
function func(h) {
console.log(h);
var h = 3;
console.log(h);
}
func(10);
첫 번째 출력은 물론 10이고 두 번째 출력은 3입니다.
사실 VO를 설정하는 과정은 위와 같기 때문에 실행시 첫 번째 출력은 10이 됩니다. 그리고 3행이 실행될 때 VO의 h를 변경하기 때문에 두 번째 출력은 당연히 3입니다!
여기까지 호이스팅에 대해 알아보았습니다. 여러분의 코딩 여정에 도움이 되었으면 좋겠습니다.
읽어주셔서 감사합니다.