[JavaScript] Scope Chain - Scope

joong dev·2021년 1월 10일
0

Javascript

목록 보기
5/5

바로 이전 글인 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()

console.log("casual greet : " + greeting1)와 같은 Lexical Environment인 function sayGreeting 안에(Function Execution Context 안에) greeting1 변수가 없으므로 Global Execution Context에서 찾는다는 것이었다.

어떻게 가능한 것일까? 바로 Scope Chain 때문이다.

Scope Chain

역시나 한 줄로 결론부터 말하자면, Scope Chain이란 자신의 조상 Lexical Environment에는 접근 가능하다는 것이다.

var scopeData = "Scope";

function one() {
    var oneData = "one"
    // 1번 위치
    function two() {
        var twoData = "two"
        // 2번 위치
        function three() {
            var threeData = "three"
            // 3번 위치
        }
    }
  
    function four() {
        var fourData = "four"
        // 4번 위치
    }
 
    ...
}

위 코드에서 변수나 함수의 Lexical Environment가 어디인지부터 확인하고 보자.

  • Global Execution Context
    변수 scopeData, 함수 one
  • One Function Execution Context
    변수 oneData, 함수 two, 함수 four
  • two Function Execution Context
    변수 twoData, 함수 three
  • three Function Execution Context
    변수 threeData
  • four Function Execution Context
    변수 fourData

그 다음엔 조상 Lexical Environment가 뭔지를 알아야 한다. Lexical이란 자신이 선언된 곳이 어디냐를 나타내는 것이라고 했었다.

위 코드에서 3번 위치를 보자. 3번 위치는 three라는 함수의 Function Execution Context에 속해있고, 이 three라는 함수는 two라는 함수의 Function Execution Context에 속해있다. 즉, 3번 위치의 부모 Lexical Environment는 two라는 함수의 Environment이다.

추가로 two라는 함수의 부모 Lexical Environment는 one이라는 함수의 Environment인데, 이것은 3번 위치의 부모의 부모 Environment이다.

위에서 정의할 때 조상 Lexical Environment라고 한 이유는 여기서 나온다.

즉, 3번 위치에서는 부모 Environment에 속해있는 twoData를 사용할 수 있으며 부모의 부모 Environment에 속해있는 oneData를 사용할 수 있다. 하지만 부모의 친구인 four의 Environment에 속해있는 fourData는 사용할 수 없다는 것이다.

4번 위치 역시 자신의 조상, 부모 Environment에 속해있는 scopeData나 oneData는 사용이 가능하지만 부모의 다른 자녀 Environment인 two 함수 내부의 것들은 아무것도 사용할 수 없다.

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()

따라서 이런 특성이 있기 때문에 sayGreeting 함수 안에서도 부모 Lexical Environment에 속하는 greeting1이라는 변수를 찾을 수 있었던 것이다.

오해 금지

내가 정리한 그림은 이렇다. 통로를 통해 상위 Lexical Environment로는 나갈 수 있으나 옆동네로는 갈 수 없는 것 이것이 Scope Chain이다.

여기서 오해하지 말아야 할 것이 있다. 통로라고 해서 쌍방으로 왔다갔다할 수 있는 것이 아니다.

var scopeData = "Scope";

function one() {
    var oneData = "one"
    // 1번 위치
    function two() {
        var twoData = "two"
        // 2번 위치
        console.log(threeData) // ★ 라인
        function three() {
            var threeData = "three"
            // 3번 위치
        }
    }
  
    function four() {
        var fourData = "four"
        // 4번 위치
    }

    ...
}

똑같은 예시 코드에서 2번 위치를 보면 조상 Environment로는 one의 Environment와 Global이 있고, 자녀로 three Environment가 있다. 이때, 2번 위치에서는 조상에 속해있는 oneData와 scopeData를 자유롭게 사용할 수 있지만 자녀에 속해있는 threeData는 불러와서 사용할 수 없다.

주석처리 되어 있는 ★ 라인의 log는 결국 threeData를 찾을 수 없기에 "threeData가 선언되어 있지 않다"는 오류를 발생시킨다.

Scope

지금까지의 내용을 보면 기준은 항상 Function, 즉 변수나 함수가 속한 Execution Context가 어디인지에 따라서 접근 가능 여부가 정해졌음을 알 수 있다.

JavaScript에서는 이것을 Function Scope를 따른다고 한다. Java나 Golang의 경우엔 Block Scope를 따른다. 이것의 차이를 잠깐 보고 가자.

정확히는 앞서 언급했던 ES6부터 JavaScript도 Block Scope를 따를 수 있다.

Function Scope

1번 코드

if (true) {
    var greeting = "hi"
}
console.log(greeting)

2번 코드

function scope() {
    var greeting = "hi"
}
console.log(greeting)

1번 코드를 보면 if문이 들여쓰기 없이 바로 작성되어 있다. 이 코드를 실행시키면 Global Execution Context에서 실행될 것이다. 그렇기때문에 if문 안에서 선언된 greeting 변수에 if문 밖의 console.log에서 접근하여 출력할 수 있다.

반면에 2번 코드를 보면 greeting변수가 function 안에 선언되어 있다. 이 코드는 실행시키면 greeting 변수는 Lexical Environment가 Function Execution Context이므로 Global Environment에 속해있는 console.log에서 접근할 수 없다. 이유는 위 Scope Chain에서 자식이 조상으로는 접근 가능해도 조상에서 자식으로는 접근할 수 없다는 것을 본 것과 같다.

즉 1번 코드는 정상적으로 작동하지만 2번 코드는 오류나는 것, 나누는 기준이 Execution Context인 것Function Scope라 하는 것이다.

Block Scope

1번 코드

if (true) {
    const greeting = "hi"
}
console.log(greeting)

2번 코드

function scope() {
    const greeting = "hi"
}
console.log(greeting)

같은 내용의 코드이지만 이번엔 varconst로 변경해서 가져왔다. 이번엔 1번, 2번 코드 모두 오류를 반환한다.

Block이라는 단어 그대로 { }를 기준으로 구역을 나눠서 판단한다고 보면 된다. 1번, 2번 코드 모두 greeting은 { } 내부에서 선언되어 있고 접근하려는 console.log는 { } 외부에 있다. 그렇기 때문에 찾지 못하는 것이다.

이것이 Block Scope이다. ES6부터 생긴 constlet은 이 Block Scope를 따른다.

조심

여기 무언가 이상한 코드가 있다.

function one() {
    two = "second data";
    console.log(two);
}

one();

Java나 C, Golang같은 경우 two처럼 어디서도 선언하지 않은 변수에 값을 할당하는 것은 명백히 오류를 내뱉을 것이다. 하지만 JavaScript에서는 그렇지 않다.

위 코드를 실행할 때 Scope Chain에 따라 one Function Environment 내에서 two라는 변수가 존재하는지 찾아본 후 없으므로 Global Environment로 가서 찾을 것이다. 하지만 그곳에도 없으니 Global Environment는 사용자가 two를 사용할 수 있게 자동으로 선언해준다.

이것은 사용자에게 엄청난 혼란을 야기할 수 있다. 코드란 협업으로 이뤄지는 경우가 많으니 누군가가 저렇게 사용해버린다면 다른 사용자는 "자신이 원하는 결과가 왜 안나오지?" 하며 좌절을 맛볼 수 있다.

그래서 선언하지 않고는 쓰지 않는 편이 가장 좋다. 그리고 후에 JavaScript에서도 이것을 막을 수 있는 방법이 나오기도 했다.

'use strict'
function one() {
    two = "second data";
    console.log(two);
}

one();

상단에 'use strict'를 입력하면 two 변수가 선언되지 않았다고 오류가 나온다.

0개의 댓글