[Javascript] 스코프 & 호이스팅

스코프에 대해 알아보기 전에, 자바스크립트는 왜이렇게 복잡할까 생각을 해봤다. var과 let, const는 참조할 수 있는게 다르다고? var는 이게 된다고? 싶은 것들이 많았다. 자 그래서 왜 그렇게 되는지 알아보기 위해서는 스코프, 호이스팅에 대해 이해할 필요가 있었다. 알아보자.

스코프란?

스코프란, 식별자가 유효한 범위를 말한다. 식별자를 검색할 때 사용하는 규칙을 말한다.

식별자란, 메모리에 있는 값을 참조할 수 있도록 사용하는 이름일 뿐이다. 변수, 함수, 클래스 등 여러 이름을 뜻한다. 코드 내부에서 이 이름이 겹치면 어떤 값을 가져와야 할 지 모른다. 따라서 우리는 스코프를 알아야 하는 것이다. 이 이름이 언제까지, 어디서 까지 사용이 가능한지 알기 위해서.

코딩을 좀만 하다보면 빨간색 밑줄이 뜨며 당황한 경험이 여럿 있을텐데 if문 안에서 변수를 선언해놓고 if문 밖에서 이를 사용하려고 했다거나 하는 경우가 이 스코프에 대한 실수라고 볼 수 있다.

  • 식별자 결정(identifier resolution): 엔진은 어떤 변수를 참조해야 할 것인가를 결정하는 것
  • 렉시컬 환경(lexical environment): 코드가 어디(전역, 지역, 함수 등)서 실행되며 주변에 어떤 코드가 있는지에 대한 환경. 코드의 문맥은 렉시컬 환경으로 이루어진다.
  • 실행 컨텍스트(execution context): 렉시컬 환경을 구현한 것이 실행 컨텍스트이다. 즉, 실제 자료구조이다. (이는 다른 포스트에서 다룰 예정이다.) 모든 코드는 실행 컨텍스트에서 평가되고 실행된다.
  • 식별자는 어떤 값을 구별해야 한다 → 유일해야 한다. → 따라서 식별자인 변수 이름은 중복될 수 없다. → 하나의 값은 유일한 식별자에 연결 되어야 한다.(name binding)

자바스크립트에서는 스코프에 대한 이해가 있어야 다른 응용 개념들을 이해할 수 있다. 따라서 하나씩 알아보자.

전역 스코프 (Global Scope)

  • 전역이란 코드의 가장 바깥 영역을 말한다.
  • 전역에 선언된 식별자는 어디서든 참조할 수 있다.

지역 스코프 (Local Scope)

  • 전역 이외의 스코프. 다른말로 함수 몸체 내부를 말한다.
  • 해당 스코프에서 선언된 식별자는 하위 스코프에서 모두 참조가 가능하다.

함수 레벨 스코프(Function Level Scope)

  • 지역 스코프는 함수 몸체 내부를 말한다. 즉 함수 내부 어디서든 참조가 가능하다는 뜻이다.
  • var로 선언한 변수는 함수 레벨 스코프를 갖는다.
function foo() {
    var a = 10;
    if(a >= 10) {
        var b = 20;
    }
    console.log(a, b);
}
foo();    // 10, 20
  • var b 는 함수 레벨 스코프를 갖기 때문에 if문 내부에서 선언했다고 상관없다. 함수 몸체 내부에서는 모두 동일한 스코프로 인식 되는 것이고, if문 외부에서도 참조가 가능한 것이다. 20이 정상적으로 출력된다.

블록 레벨 스코프(Block Level Scope)

  • 블록 레벨 스코프는 다른 언어들과 마찬가지이다. if, for while, try/catch 등으로 묶인 부분을 코드 블록이라고 말하고, 이들에 대해 독립적인 지역 스코프를 만드는 특성을 블록 레벨 스코프 라고 한다.
  • ES6의 let, const는 블록 레벨 스코프를 갖는다.
function foo() {
    const a = 10;
    if(a >= 10) {
        const b = 20;
    }
    console.log(a, b);
}
foo();    // Error
  • function foo() 블록에는 b 라는 식별자가 유효하지 않기 때문에 에러가 발생한다. var과 다른 스코프를 갖는 것을 알 수 있다.

렉시컬 스코프(Lexical Scope, Static Scope)

함수의 상위 스코프를 결정하는 방법에 따라 동적 스코프와 정적 스코프로 나뉜다.

  • 동적 스코프: 함수를 어디서 호출 했는지에 따라 상위 스코프를 결정한다.
  • 정적 스코프: 함수를 어디서 정의 했는지에 따라 상위 스코프를 결정한다.
var x = 1;

function foo() {
    var x = 10;
    bar();
}

function bar() {
    console.log(x);
}

foo(); // ?
bar(); // ?
  • 자바스크립트는 정적 스코프, 즉 렉시컬 스코프를 따른다. 따라서 function bar()의 상위 스코프는 global이다. 따라서 global에 선언되어있는 var x = 1을 스코프 체이닝에 의해 참조하고 출력하게 된다. 즉, 두 물음표는 모두 1이다.
  • foo() → bar()로 호출 되는 과정에서 foo의 스코프는 x값 참조에 아무런 영향을 주지 못한다.

스코프 체인(Scope Chain)

  • 스코프 체인이란, 스코프가 계층적인 구조를 갖고 연결된 것을 말한다. 즉, 중첩함수와 같이 함수 안에 함수가 있고, 이는 스코프 안에 스코프가 있는 것이고 이게 계층적으로 연결된다는 뜻이다.
  • 변수를 참조할 때 js 엔진은 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.
  • 자바스크립트 엔진은 코드를 실행하기 전에 렉시컬 환경 “자료구조“를 물리적으로 생성 → 변수 선언이 실행되면 변수 식별자가 이 자료구조에 key로 등록되고, 변수 “할당“이 일어나면 이 자료구조의 변수 식별자에 해당하는 값을 변경한다.

호이스팅

호이스팅이란, 변수나 함수의 선언 단계가 먼저 실행되어 마치 해당 스코프의 최상단으로 코드를 옮겨져 동작하는 것처럼 보이는 것을 말한다.

  • 호이스팅이라는 말이 끌어올림이라는 뜻이다. 무엇을 끌어올리는가? → 선언 단계
  • 자바스크립트에서 엔진은 식별자를 선언 단계와 초기화 단계로 분리한다.
```jsx
function foo() {
    console.log(a);
    var a = 2;
}
foo();
```

이 코드에서 `console.log(a)`는  `undefined`이다. 호이스팅이란, 선언 단계만 끌어올린다. `var a; a = 2;` 이렇게 선언단계와 초기화 단계로 나뉘어지고, `var a;` 만 위로 끌어올려지는 것이다.

변수 호이스팅

  • 위의 예시가 변수 호이스팅이다.
  • let, const는 조금 특이하게 동작한다. 마치 변수 호이스팅이 발생하지 않는 것처럼 진행된다.
    • var의 경우에는 엔진이 선언단계 → 초기화 단계(undefined)를 한번에 진행한다.
    • let의 경우에는 선언단계만 먼저 실행된다. 따라서 렉시컬 환경에 선언을 했다는 정보는 있지만 초기화가 되어있지 않아서 ReferenceError를 반환한다.
    • 할당 하기 전까지를 일시적 사각지대(Temporal Dead Zone, TDZ)라고 부른다.
    • 그리고 let foo; 를 만나 초기화를 해야 그제서야 undefined로 초기화를 한다.
      let foo = 1;
        
      { // 블록 레벨 스코프
          console.log( foo );
          let foo = 2;
      }
    
  • 변수 호이스팅이 되지 않는 것처럼 보일 수 있는데, 변수 호이스팅이 되지 않았다면 스코프 체인에 의해 1을 출력했어야 하는데 변수 호이스팅이 되기 때문에 ReferenceError를 반환한다.

함수 호이스팅

  • 함수 선언문이 코드의 선두로 끌어올려진 것처럼 동작하는 특징을 함수 호이스팅이라고 한다.
  • 모든 “선언문”은 런타임 이전에 엔진에 의해 실행된다. 고로 함수 선언문으로 함수를 정의하면 런타임 이전에 함수 객체가 먼저 생성된다.
  • 변수 호이스팅은 undefined로 초기화 되지만, 함수 호이스팅은 함수 객체로 초기화 된다. 그래서 정상적으로 함수 내부의 로직이 수행이 가능하다. 따라서 function foo() { ... } 는 어디에 위치해도 된다는 점.
  • 반면 함수 표현식은 변수만 호이스팅 된다. 함수는 “값”으로 런타임에 평가된다. 즉, 함수 표현식으로 코딩하면 변수 호이스팅이 발생한다.

클래스 호이스팅

  • 클래스는 함수로 평가된다.
  • 클래스 선언문으로 정의한 클래스는 런타임 이전에 평가되어 함수 객체를 생성한다. 다만 클래스는 클래스 정의 이전에 참조할 수 없다. 마치 let, const처럼.
  • 이 때 생성된 함수 객체는 생성자 함수, constructor로서 호출할 수 있는 함수이다. 이때 프로토타입도 더불어 생성된다. constructorprototype은 동시에 존재하며 서로를 참조한다.