JS 호이스팅 ( Hoisting )

조 은길·2021년 10월 14일
0

Javascript 정리

목록 보기
12/48
post-thumbnail
  • Achievement Goals
    • 호이스팅(Hoisting)이란 무엇인지 이해한다.
    • 함수선언문과 함수표현식에서의 호이스팅 차이를 이해한다.
    • let/const와 var 변수 선언에서의 호이스팅 차이를 이해한다.
    • 같은 이름의 var 변수 선언과 함수 선언에서의 호이스팅에 대해 이해한다.

JS를 공부할 때, 항상 이상하다고 생각했던게 있다. 분명 코드는 위에서 아래로 읽어가는데, 밑에서 선언한 함수를 위에서 사용하는 것이 가능하다. 어째서 이런 일이 가능한가??

우리는 이런 현상을 호이스팅 ( hoisting )이라고 부른다. 호이스팅은 영어로 "끌어 올리기"라는 뜻이다. 다시 말해서, 무언가를 끌어올린다는 것인데, 지금부터 자세하게 살펴보자!!


호이스팅

니콜라스 자카스의 <모던 자바스크립트>에 따르면, 호이스팅이란 자바스크립트 엔진이 블록을 조사할 때, var 변수 선언을 발견하면, 함수의 최상단 혹은 전역 스코프로 호이스팅하는 현상을 의미한다. 이 말을 좀 더 쉽게 풀어보자! 함수 선언과 변수 선언은 코드를 실행할 때, 해당 선언 스코프 최상단으로 끌어올려진다. 이런 현상을 호이스팅이라고 한다.
=> "선언"만 올라간다!! 할당은 올라가지 않는다!!

그렇다면, 호이스팅은 모든 경우에서 다 작동하는 건가??

우선 함수의 경우부터 알아보자!!

함수의 두 가지 리터럴

1. 함수선언

함수 선언은 function 키워드 뒤로 함수의 이름을 적어서 사용한다.

2. 함수 표현식

함수 표현식은 function 키워드 뒤로 이름을 적지 않고 사용합니다. 이름이 없기
때문에 익명 함수라고 부르기도 한다.

이 2 가지 리터럴이 호이스팅을 일으키면, 어떻게 되는지 다음 예제를 통해서 알아보자.

  • ex1)

정답

  • "test" , TypeError

둘다 아직 선언도 되지 않은 함수들을 먼저 출력 후, 바로 밑에서 선언하려고 시도했다. 그런데, 함수 선언식은 정상적으로 작동되는 것을 볼 수 있지만, 표현식은 타입에러가 났다. 왜 그럴까??

함수 선언은 코드를 실행할 때 함수를 포함하는 스코프 최상단으로 끌어올려진다. 예를 들어, 함수가 전역 공간에 있다면, 전역 공간 가장 윗 쪽으로 끌어올려진다. 그래서 함수 선언 전에 함수를 실행해도 에러가 발생하지 않는다.

그에 반해 함수 표현식은 변수를 통해서 함수를 참조하기 때문에 호이스팅이 일어나지 않는다.

변수를 통해서 참조하기 때문이라니... 대체 이게 뭔소리인가???

다음 예제를 통해서 확인해보자!!

  • undeclared는 선언이 안되있고, testValue는 바로 밑에서 선언이 되있다.

  • ex2)

정답

testValue는 분명히 변수이고, 아래에서 값을 할당해줬다. 그런데, 위에서 값에 아무것도 들어있지 않다고 나온다.

다시 말해서, 선언하지 않은 변수는 당연히 에러가 난다. 하지만 선언한 변수도 ‘값’ 까지 끌어올려지지는 않는다. 선언을 한 변수의 그 이름만 끌어올려진다.

그렇기 때문에,

ex1)에서 "testValue" 역시 변수이기 때문에, 할당된 값까지 끌어올려지지는 않은 것이다.

이 사실을 알고 조금 더 응용된 예제를 보자.

  • ex3)

이번에는 결과값으로 "hoist test"가 출력될 것인가??

정답

testValueVar는 변수이기 때문에, 값이 할당되지는 않았다. 즉, 콘솔 로그 시점에서 testValueVar는 그저 var testValueVar; 그 이상도 이하도 아니다!!

변수에 대해서 한가지만 더 짚고 넘어가보자.

  • ex4)

해당 예제의 핵심은 if절 자체가 실행이 안 되는데도, 호이스팅이 되서, test가 선언이 되있을지 아니면, 선언도 안 되서 레퍼런스 에러가 나올지이다.

정답

놀랍게도 선언이 되있는 undefined 상태가 되있다. 왜 그럴까??

JS에서 여러 종류의 스코프가 있지만, if문 안에 있는 공간은 전역 스코프같은 공간이라고 생각하면 된다. 그렇기 때문에, test는 전역 공간에 선언만 되어있는 거다. 즉, 값은 undefined이지만, test라는 변수가 있다는 것은 안다!!

이제 배운 것을 종합해서, 다음 예제를 보자

  • ex5)

정답

test()는 함수 선언식이기 때문에, 선언과 할당까지 모두 최상위로 끌어올려지지만, value의 경우는 var이기 때문에, return과 함께 함수가 종료되면서 값도 지워진다. 그래서 전역 공간에서는 value 값이 존재하지 않는다.

우리가 여기서 알 수있는 사실은 다음과 같다.

변수 선언 역시 코드를 실행할 때 변수를 포함하는 스코프 최상단으로 끌어올려집니다.
단, 함수안에 선언된 변수는 함수 스코프 안에서 최상단으로 끌어올려집니다.

호이스팅에 관한 정리 및 주의사항

  • 함수 선언식의 호이스팅에 지나치게 의존하는 것은 코드의 유지 보수 측면에서 좋지 않다. 함수 선언식의 경우, 어느 위치에나 함수를 선언할 수 있고 함수의 실행 위치도 중요하지 않기 때문이다. 즉, 코드 리뷰나 디버깅을 할 때 코드를 위 아래로 왔다 갔다하게 될 수 있다. 반면, 함수 표현식은 함수의 할당과 실행의 위치가 중요하기 때문에 코드의 위치가 어느 정도 예측 가능하다.
  • 호이스팅을 제외하면 함수 선언식과 함수 표현식의 차이는 크게 없어 보이고, 실제로도 그렇다. 다만 함수 표현식의 경우, 함수가 변수에 저장될 수 있다는 사실을 좀 더 분명하게 드러낼 수 있을 뿐이다.
  • 한편, 변수에 저장된 데이터는 함수의 인자로 전달되거나 함수 내에서 리턴값으로 사용될 수 있다. 앞서 함수가 변수에 저장될 수 있다는 사실로부터, 함수 역시 다른 함수의 인자로 전달되거나 다른 함수 내에서 리턴될 수 있다는 것을 알 수 있다.

호이스팅이 되지 않는 선언들

블록 스코프를 생성하는 let, const 는 호이스팅이 일어나지 않는다. class 또한 마찬가지 이다.

var 로 선언된 변수는 호이스팅되지만 let, const로 선언된 변수와 상수는 TDZ(Temporal Dead Zone. 임시 접근 불가구역) 구역에 배치된다. 이 값들은 선언이 실행된 후에 TDZ에서 제거되어 사용 가능한 상태가 된다.

정리해보자면, let, const, class 선언은 호이스팅 현상이 일어나지 않는다.

  • ex6)

정답

전부다 참조할 값을 찾지 못하고 에러가 뜬다!! => test1에서 에러가 나면, 나머지 코드가 실행되지 않기 때문에, 각각의 케이스를 독립적으로 실행해봐야 한다.


이해도 확인(중요)

Q1. 다음 코드의 콘솔창 출력결과는 무엇일까요?

함수();
function 함수() {
  console.log(안녕);
  let 안녕 = 'Hello!';
} 
  • 정답
    에러

  • 이유
    undefined라고 생각하신 분은 훌륭하신 분들입니다. 강의에서 그렇게 배웠으니까요.
    하지만 답은 에러입니다.
    함수는 잘 실행됩니다.
    하지만 함수내부 코드가 이상합니다.
    let 변수는 특이하게도 Hoisting이 되긴 하지만 undefined라는 값이 할당되지 않습니다.
    그래서 출력시 에러를 뿜습니다.

    let 변수는 hoisting 되지만 var 변수처럼 자동으로 undefined 라는 값을 할당(일명 initialization) 해주지 않습니다.
    선언과 할당 사이에 시간차가 있기 때문에 그런 현상이 일어나는 것이고 let 변수는 그래서 쓸 수 없습니다
    그래서 그냥 let const 변수는 그래서 엄격하게 쓸 수 있는 변수구나 외우시면 되겠습니다.

Q2.


함수();
var 함수 = function() {
  console.log(안녕);
  var 안녕 = 'Hello!';
} 
  • 정답
    에러

  • 이유??
    이번에도 답은 에러입니다.

정확히 말하면 함수가 아닌데요? 라는 에러입니다.

지금 둘째줄에 있는 함수 선언부분을 잘 보시면 function 키워드 대신 변수만드는 것 처럼 함수를 선언과 할당하고 있습니다.

역시 이렇게 함수를 만드셔도 Hoisting이 되는데, 근데 Hoisting은 변수의 선언부분만 된다고 했었습니다.

그래서 변수 선언부분만 맨 위로 끌어올려지는데

그 변수에다가 소괄호를 붙여봤자 아직 함수가 아니기 때문에 실행이 되지 않습니다. (에러가 납니다. 함수가 아닌 변수에다가 소괄호 붙이면 함수 아니라고 에러를 뿜어줍니다.)

Q3.

let a = 1;
var 함수 = function() {
  a = 2;
}
console.log(a);
  • 정답
    1

  • 이유??
    a는 1이 출력됩니다.

a는 1이라는 변수를 만들고

그 다음에 함수를 만들고 함수 안에서 a = 2라고 값을 변경시켰는데

함수를 정의만 했지 실행을 안시켜서 a = 2라는 부분은 없는 코드나 마찬가지입니다.

그래서 a 는 그냥 1입니다.

Q4.

let a = 1;
var b = 2;
window.a = 3;
window.b = 4;

console.log(a + b);
  • 정답
    5

  • 이유??
    a는 1, b는 4가 출력됩니다.

b가 4가 되는 이유는 var b = 2 이것과 window.b = 4 이건 거의 동일한 기능을 하는 코드기 때문에

b는 그냥 4로 재할당이 되었다고 보시면 되겠습니다.

a는 let 변수로 1을 할당하고 글로벌 변수로 3을 할당했습니다.

이 경우 우리가 a를 사용했을 때 조금 더 범위가 작고 가까운(더 상단에 있는) 1을 참조해서 사용합니다.

(자바스크립트 변수를 사용할 때 참조할만한 변수가 내 주변에 없으면 계속 상위 중괄호로 시선을 돌리면서 참조합니다.)


  • 콘솔창에 1초에 한번씩 1부터 5까지의 정수를 출력해주고 싶습니다.

저번 연습문제에서 setTimeout이라는 유용한 함수를 배운 것 같습니다.

그래서 코드를 이렇게 작성했습니다.

setTimeout(function() { console.log(1); }, 1000 ); 
setTimeout(function() { console.log(2); }, 2000 ); 
setTimeout(function() { console.log(3); }, 3000 ); 
setTimeout(function() { console.log(4); }, 4000 ); 
setTimeout(function() { console.log(5); }, 5000 ); 

그럼 1초마다 1~5까지의 숫자를 콘솔창에 출력해줍니다.

하지만 반복되는 코드가 보기 싫어서 반복문 안에 담았습니다.

for (var i = 0; i < 5; i++) { 
  setTimeout(function() { console.log(i); }, i*1000 ); 
}

논리적으로 완벽한 for 반복문입니다.

그런데 반복문으로 축약하자마자 제대로 작동하지 않습니다.

계속 5라는 숫자가 1초마다 출력되네요.

Q5. 위 코드는 왜 의도대로 동작하지 않는 것이죠? 해결할 방법은 무엇일까요?

  • 정답
    for문 안의 i를 let으로 선언한다.

  • 이유??

자바스크립트 입장에서 하단 코드를 해석해보겠습니다.

for (var i = 0; i < 5; i++) { 
  setTimeout(function() { console.log(i); }, i*1000 ); 
}

자바스크립트는 일단 반복문을 만나면 반복문 내의 코드를 반복해서 실행합니다.

지금 반복문이 i가 0부터 5가 되기 전까지 반복해주세요~라고 써놓았으니 총 5번 반복이 되겠네요.

근데 내부 코드는 setTimeout 어쩌구 입니다. X초후에 콜백함수 내의 console.log(i)를 실행해주세요~ 라는 코드입니다.

그래서 그 부분은 반복문과 동시에 실행되지 않습니다. 좀 나중에 실행되겠죠 뭐.

반복문 끝입니다.

반복문을 해석한 후.. 1초가 지나면 setTimeout 내의 console.log(i)가 발동됩니다.

근데 i를 채워넣고싶어서 주변을 살펴보았더니 i값은 5밖에 없는 것입니다.

왜냐면 아까 반복문이 5번 실행되면서 i값은 0,1,2,3 ... 이렇게 차례로 변하다가 i값이 5가 되어 종료했습니다.

그리고 i 값은 var로 만든 전역변수입니다.

그래서 i값을 쓰려고 봤더니 전역변수 i = 5밖에 없어서 5를 집어넣어서 계속 실행해서 콘솔창에 5가 계속 출력되던 것입니다.

해결책은 for 반복문에서 i변수를 만들 때 var 대신 let으로 바꾸는 것입니다.

let 변수는 범위가 중괄호랬죠? for반복문도 중괄호에 해당됩니다.

그럼 이제 1초 후 console.log(i)가 실행될 때 i값을 채우려고 살펴보면

i값이 for 반복문 내에 남아있기 때문에 그걸 가져다 쓰게 됩니다.

그래서 아까처럼 계속 5를 출력해주는게 아니라 1,2,3,4,5를 출력해줍니다.

(위의 예제는 0,1,2,3,4가 출력되겠군요 이런)


  • 버튼을 누르면 모달창을 띄우고 싶습니다.

버튼(button)과 모달창(div)가 3개 있습니다.

<div style="display : none">모달창0</div>
<div style="display : none">모달창1</div>
<div style="display : none">모달창2</div>

<button>버튼0</button>
<button>버튼1</button>
<button>버튼2</button>

<script>
  //?
</script>

지금 display : none 덕분에 모달창이 아무것도 안보이는 상태입니다.

자바스크립트를 잘 짜서

0번째 버튼을 누르면 0번째 모달창,

1번째 버튼을 누르면 1번째 모달창을 보여주고 싶습니다.

그럼 코드를 어떻게 짜면 될까요?

여기까진 기본 자바스크립트 내용이니 친절히 알려드리겠습니다.

<div style="display : none">모달창0</div>
<div style="display : none">모달창1</div>
<div style="display : none">모달창2</div>

<button>버튼0</button>
<button>버튼1</button>
<button>버튼2</button>

<script> 
var 버튼들 = document.querySelectorAll('button');
var 모달창들 = document.querySelectorAll('div');

버튼들[0].addEventListener('click', function(){
  모달창들[0].style.display = 'block';
});

버튼들[1].addEventListener('click', function(){
  모달창들[1].style.display = 'block';
});

버튼들[2].addEventListener('click', function(){
  모달창들[2].style.display = 'block';
});

</script>

document.querySelectorAll은 jQuery의 $('') 셀렉터와 매우 유사합니다. 동시에 여러 요소를 찾아 어레이 비슷한 자료형에 담아줍니다.

아무튼 이렇게 쭉 쓰면

0번째 버튼을 누르면 0번째 모달창,

1번째 버튼을 누르면 1번째 모달창을 보여줍니다.

그런데 비슷한 코드들이 좀 보이죠? 이걸 반복문 안에 담아서 한번 다시 개발해보겠습니다.

두근두근

<script> 
var 버튼들 = document.querySelectorAll('button');
var 모달창들 = document.querySelectorAll('div');

for (var i = 0; i < 3; i++){

  버튼들[i].addEventListener('click', function(){
    모달창들[i].style.display = 'block';
  });

}

</script>

이렇게 반복문으로 반복적인 코드를 축약가능합니다.

그런데 문법에 맞게 쓰긴 했는데 모달창이 제대로 작동하지않고 있습니다.

Q6. 위 코드는 왜 의도대로 동작하지 않는 것이죠? 해결할 방법은 무엇일까요?

  • 정답
    5번과 동일하게, for문 안의 i를 let으로 할당해준다.

  • 이유??

방금 전 문제랑 거의 똑같은 경우의 문제입니다.

자바스크립트 입장에서 하단 코드를 해석해보겠습니다.

for (var i = 0; i < 3; i++){

  버튼들[i].addEventListener('click', function(){
    모달창들[i].style.display = 'block';
  });

}

자바스크립트는 일단 반복문을 만나면 반복문 내의 코드를 반복해서 실행합니다.

지금 반복문이 i가 0부터 3이 되기 전까지 반복해주세요~라고 써놓았으니 총 3번 반복이 되겠네요.

근데 내부 코드는 addEventListener 어쩌구 입니다. 클릭 되면 콜백함수 내의 모달창들[i].style.display = 'block'; 을 실행해주세요~ 라는 코드입니다.

그래서 그 부분은 반복문과 동시에 실행되지 않습니다. 좀 나중에 클릭 되면 실행되겠죠 뭐.

반복문 끝입니다.

반복문을 해석한 후.. 누군가 버튼을 클릭하면 addEventListener 내의 모달창들[i].style.display = 'block'; 코드가 발동됩니다.

근데 i를 쓰고싶어서 주변을 살펴보았더니 i값은 3밖에 없는 것입니다.

왜냐면 아까 반복문이 3번 실행되면서 i값은 0,1,2,3 ... 이렇게 차례로 변하다가 i값이 3이 되어 종료했습니다.

그리고 i 값은 var로 만든 전역변수입니다.

그래서 i값을 쓰려고 봤더니 전역변수 i = 3밖에 없어서 3을 집어넣어서 계속 에러가 났던 것입니다.

해결책은 for 반복문에서 i변수를 만들 때 var 대신 let으로 바꾸는 것입니다.

let 변수는 범위가 중괄호랬죠? for반복문도 중괄호에 해당됩니다.

반복문이 돌고 나서도 let i = 어쩌구 값이 {for 반복문} 내에 남아있기 때문에 그걸 모달창들[i].style.display = 'block'; 의 i값으로 가져다 쓰게 됩니다.

그럼 이제 의도된 i값으로 코드가 잘 실행됩니다.


자료 출처

호이스팅을 deep하게 설명하려면, JS의 근간인 실행 컨텍스트에 대해서 알아야 하는데, 실행 컨텍스트를 건드리면, 전역 객체도 언급해야 되고, 꼬리에 꼬리를 물다보니... 호이스팅 그 자체에만 초점을 맞췄다. 기회가 되는데로 실행 컨텍스트에 대해서 정리해본 다음에, 호이스팅과 클로저를 어떻게 deep하게 이해할 수 있는지 서술해보겠다.

오늘 TIL은 코드스테이츠에서 학습한 내용과 개인적으로 보충 학습한 내용들을 바탕으로 작성됐다.

실행 컨텍스트와 클로저

MDN 호이스팅

{풀스택} JavaScript 5강 - 변수와 함수의 호이스팅이란?

{즉문즉설:자바스크립트} let도 호이스팅이 되나요?

자바스크립트 호이스팅, 흔한 오해 1가지

코드를 어떻게 나눠야 할까요? (feat. 호이스팅)

모던 자바스크립트 예제로 배우는 ECMAScript 6 핵심 기능

호이스팅이란

profile
좋은 길로만 가는 "조은길"입니다😁

0개의 댓글