[JavaScript] Hoisting - Lexical Environment

joong dev·2021년 1월 9일
0

Javascript

목록 보기
4/5

JavaScript에는 Hoisting이라는 성질이 있다. 한 줄로 소개하자면 "변수를 제일 위에 선언하는 것"이라고 할 수 있다.

이 내용을 가장 잘 함축하고 있다고 생각되는 코드를 먼저 보자.

var greeting = "hi"
var sayGreeting = function () {
    console.log("casual greet : " + greeting)
    var greeting = "hello"
    console.log("formal greet : " + greeting)
}

sayGreeting()

이 코드를 실행하면 너무 쉽게 아래처럼 결과가 나올 것으로 생각할 수 있다.

casual greet : hi
formal greet : hello

하지만 놀랍게도 틀렸다. 결과는 다음과 같다.

casual greet : undefined
formal greet : hello

Hoisting 때문인데 이것을 알기 전에 이해에 필요한 Lexical Environment가 뭔지부터 보자.

Lexical Environment

Lexical Environment란 변수나 함수가 어디 작성되어 있는지를 나타내는 것이다. 주의해야 할 부분은 코드가 어디에서 실행되었느냐가 아니다.(어디에서 실행되었는지를 나타내는 단어는 Dynamic Scope라고 부름)

자세한 설명을 위해 앞 글인 Runtime의 Call Stack 예제를 다시 가져왔다.

function a() {
    return "This is A data";
}

function b() {
    const aData = a();
    return aData
}

function c() {
    const bData = b();
    return bData;
}

c()

이 코드를 실행하면 Call Stack이 어떤 순서대로 쌓이는지 확인했던 것을 기억할 것이다.

여기서 anonymous라고 Call Stack의 맨 밑에 깔려 있는 것을 볼 수 있다. JS 파일을 실행하면 맨 처음 Stack에 들어가는 이 anonymous를 Global Execution Context라고 이해하면 되고, 그 위에 하나씩 함수가 실행될때마다 쌓이게 되는 것을 Function Execution Context라고 이해하면 된다.

Lexical Environment란 변수나 함수가 작성된 곳을 나타내는 말이라고 했었다. 즉, 들여쓰기 없이 그냥 쓰여진 함수 a, b, c의 Lexical Environment는 Global Execution Context인 것이다. (Global이기에 함수 a, b, c는 어디에서나 불러 쓸 수 있는 것이라고 생각하면 쉽다.)

반면에 bData의 function b 내부에 작성되어 있다. 즉, bData의 Lexical Environment는 함수가 실행되면서 생기는 Function Execution Context인 것이다.

Hoisting

Hoisting을 좀 더 정확하게 다시 정의해보면 변수나 함수가 자신이 속한 Lexical Environment의 맨 위에서 선언되는 것을 의미한다.

이 3줄짜리 코드부터 해보자.

greet = "hi"
var greet
console.log(greet)

실행되면 한 줄 한 줄 읽으며 var를 찾을 것이다. 그리고 2번째 줄의 var greet을 만나고 Hoisting을 할것이다. greet의 Lexical Environment는 Global이기 때문에 맨 위에서 선언된다.

var greet = undefined
greet = "hi"
console.log(greet)

선언된 후 greet에 "hi"를 할당했고, 이것을 출력하는 코드가 되기 때문에 전혀 문제가 없었던 것이다.

다시 돌아가서 조금 더 복잡한 맨 처음 들었던 예시를 보자.

var greeting = "hi"

var sayGreeting = function () {
    console.log("casual greet : " + greeting)
    var greeting = "hello"
    console.log("formal greet : " + greeting)
}

sayGreeting()

여기서 Lexical Environment가 Global인 녀석들을 찾아보면 변수 greeting과 sayGreeting이 있다. 따라서 Hoisting이 되면,

var greeting = undefined
var sayGreeting = undefined

greeting = "hi"
sayGreeting = function () {
    console.log("casual greet : " + greeting)
    var greeting = "hello"
    console.log("formal greet : " + greeting)
}

sayGreeting()

이렇게 되는 것이다. 그리고 마지막 줄에서 sayGreeting이 호출될 때, Function Execution Context가 생성되고 여기에 속하는 변수와 함수가 다시 Hoisting이 된다.

sayGreeting = function () {
    var greeting = undefined
    console.log("casual greet : " + greeting)
    greeting = "hello"
    console.log("formal greet : " + greeting)
}

그 후에 console.log를 실행시키게 되니 첫 번째 log에는 undefined가 찍히게 되고 두 번째 log에는 hello가 찍히게 되는 것이다. 이것이 Hoisting이다.

물론 위의 예시에서는 같은 변수명을 써서 저렇게 헷갈리는 상황이 발생한 것이다. 변수명만 살짝 바꾸고 로그 하나만 더 찍어보자.

var greeting1 = "hi"

var sayGreeting = function () {
    console.log("casual greet : " + greeting1)
    console.log("formal greet : " + greeting2)
    var greeting2 = "hello"
    console.log("formal greet : " + greeting2)
}

sayGreeting()

이것을 Global, Function Execution Context까지 모두 Hoisting된 것을 한 번에 표현해보면 다음과 같을 것이다.

var greeting1 = undefined
var sayGreeting = undefined

greeting1 = "hi"
sayGreeting = function () {
    var greeting2 = undefined
    console.log("casual greet : " + greeting1)
    console.log("formal greet : " + greeting2)
    greeting2 = "hello"
    console.log("formal greet : " + greeting2)
}

sayGreeting()

근데 sayGreeting의 첫 번째 log인 console.log("casual greet : " + greeting1)에서 greeting1은 Function Execution Context 내부에 없는 변수이다. 즉, Lexical Environment가 다른 것이다.

이렇게 자신이 찾는 변수나 함수가 같은 Environment에 없는 경우 Global로 가서 찾는다. 결과적으로 오류 없이 casual greet : hi이라는 결과가 나오는 것이다.

sayGreeting의 두 번째, 세 번째 log는 우리가 학습해온 대로(생각하는 대로) 결과가 출력될 것이기에 위의 코드는 아래의 결과가 된다.

casual greet : hi
formal greet : undefined
formal greet : hello

var 와 const, let

또 다시 맨 처음 예시를 가져오겠다. 하지만 이번엔 sayGreeting 함수 안의 var greeting에서 var를 뺐다.

var greeting = "hi"
var sayGreeting = function () {
    console.log("casual greet : " + greeting)
    greeting = "hello"
    console.log("formal greet : " + greeting)
}

sayGreeting()

이거 하나로 결과가 어떻게 달라질까?

casual greet : hi
formal greet : hello

다들 예상했나. 이번에는 맨 첫줄 var greeting = "hi"가 적용 됐다. 이유는 Hoisting 된 결과를 생각해보면 된다.

var greeting = undefined
var sayGreeting = undefined

greeting = "hi"
sayGreeting = function () {
    console.log("casual greet : " + greeting)
    greeting = "hello"
    console.log("formal greet : " + greeting)
}

sayGreeting()

sayGreeting의 첫 console.log에서는 greeting이라는 변수를 같은 Lexical Environment(Function)에서 찾을 수 없으므로 Global에서 찾은 "hi" 넣어주는 것이다. 그리고 두 번째 console.log 전에 greeting 변수를 재할당해줬기 때문에 "hello"가 나온다.

여기서 var의 문제가 나타난다. 재선언(Re Declaration)해도 전혀 오류가 없기 때문에 헷갈리게 된다.

ES6에서는 이런 문제를 해결하기 위해서 const, let을 추가했다.

ES6는 ECMAScript6를 줄인 말이다.
JavaScript의 불편한 점, 오류 등을 수정해서 표준을 제안한 것이다.

또 가져왔다. 다시 처음에 본 그 예시다. 다만 var 자리를 let으로 바꿨다.

let greeting = "hi"
let sayGreeting = function () {
    console.log("casual greet : " + greeting)
    let greeting = "hello"
    console.log("formal greet : " + greeting)
}

sayGreeting()

이걸 실행해보면 greeting이 선언되지 않았다는 오류가 발생하게 된다. 버전별, 실행 환경 별 오류 메시지는 다르지만 나의 경우엔 이런 메시지다.

console.log("casual greet : " + greeting)
                                    ^
ReferenceError: Cannot access 'greeting' before initialization
    at sayGreeting (D:\project\test.js:124:37)
    at Object.<anonymous> (D:\project\test.js:129:1)

왜냐하면 sayGreeting이라는 Function Execution Context의 첫 번째 줄인 console.log에서 greeting을 호출하기 전에 greeting 변수가 선언되지 않았다고 판단하기 때문이다.

// 이게 아니라는 의미다!!!!!!
let sayGreeting = function () {
    let greeting = undefined // <- 이 부분이 var처럼 되지 않는다!!
    console.log("casual greet : " + greeting)
    greeting = "hello"
    console.log("formal greet : " + greeting)
}

이것이 var와 let의 큰 차이점이고 위의 문제를 해결한 방법이다. 하지만 Hoisting이 일어나지 않았다고 생각하면 안된다. 다만 Hoisting 방법이 var와 const, let이 다를 뿐이다.

var는 Hoisting이 되면 자신의 Lexical Environment 맨 위에 undefined로 선언된다고 했다.(이제와서 말하지만 변수 선언 후 undefined으로 할당된다는 것을 의미한다.)
const와 let은 정말로 선언만 되고 undefined조차 할당하지 않는다. 그렇기 때문에 initialization 전에는 사용할 수 없다는 오류가 나왔던 것이다.

let을 사용할 때 오류내지 않고 쓰려면 sayGreeting의 첫 번째 줄에 직접 greeting 변수를 선언해야 한다.

let greeting = "hi"
let sayGreeting = function () {
    let greeting // <- 여기에 선언
    console.log("casual greet : " + greeting)
    greeting = "hello"
    console.log("formal greet : " + greeting)
}

sayGreeting()

그럼 이렇게 말할 수 있다.
"var와 다른점이 쓰기전에 그냥 선언 한 번 더 해줘야 된다는 것이냐!!"

let greeting = "hi"
console.log(greeting)
let greeting = "hello"
console.log(greeting)

아니다. 이 예시를 실행해보면 다음 오류가 발생한다.

let greeting = "hello"
    ^
SyntaxError: Identifier 'greeting' has already been declared

let은 같은 Lexical Environment 내에서는 한 번만 선언이 가능하다. 위에서 가능하다고 한 것은 Global Context에서 한 번, Function Context에서 한 번 선언됐기 때문에 가능했던 것이다.

뭐 여튼, var는 사용하지 말고 const와 let을 사용해라.
Hoisting은 셋 다 하지만 var는 undefined로 할당까지 하는 것이고 const, let은 선언만 한다는 것을 기억하자.

  • var
    선언(Declaration)과 할당(Initialization) 모두 언제든지 다시 할 수 있는 것

  • const
    선언(Declaration)과 할당(Initialization)을 동시에 해야 하고 재할당을 할 수 없는 것

  • let
    선언(Declaration) 후 언제든지 할당(Initialization)을 할 수 있는 것

0개의 댓글