스코프(scope)
는 변수에 접근할 수 있는 '범위'로, 크게 전역 스코프(Global scope)
와 지역 스코프(Local scope)
로 나누어진다. 또한 전역 스코프
에서 선언된 변수를 전역 변수
라고 하며, 지역 스코프
에서 선언된 변수를 지역 변수
라고 한다. 이러한 스코프가 중요한 이유는 바로 스코프의 적절한 설정을 통해 변수의 '생존 기간'을 제어할 수 있기 때문이다.
다음 예시를 통해 스코프의 작동 원리를 알아보자.
let user = 'lee';
if (user) {
let message = `hello, ${user}!`;
console.log(message); // 'hello, lee!'
}
console.log(message); // ReferenceError
if 문의 바디는 중괄호({})로 둘러싸여 있으며, 이러한 중괄호 안에 let을 이용해 message라는 변수가 선언되었다. 이렇게 if 문 내부에서 선언된 변수는 내부에선 문제없이 동작하지만, if 문 밖에서 호출하면 ReferenceError를 반환하는 것을 볼 수 있다. 그 이유가 무엇일까?
바로 변수의 접근 가능 범위가 다르기 때문이다. user 변수를 자세히 보면 if 문의 밖에서 선언된 것을 볼 수 있다. 이렇게 if 문 내부에서 let으로 선언된 message는 '블록 스코프'를 가지며, 외부 변수 user에 접근할 수 있다. 그러나 if 문 외부에서 '블록 스코프'를 가지는 내부 변수 message에 접근하려 하면 ReferenceError가 난다. 스코프는 이렇게 접근 가능한 범위를 정하는 기준이 된다.
ES6에서 블록 스코프(block scope)
와 let
, const
가 새롭게 등장하기 전까지 자바스크립트의 모든 스코프는 함수 스코프(function scope)
였다. 이러한 구분은 스코프의 범위를 블록(중괄호{})을 기준으로 할 것인지, 함수를 기준으로 할지에 따라 정해진다.
var
로 대표되는 전통적인 함수 스코프
에선 함수 내부에서 선언된 변수는 함수 안에서만 유효하다. 이러한 함수 밖에서 선언된 변수는 모두 전역 스코프를 부여하는 것이 함수 스코프
의 특징이다. 만약 동일한 이름의 변수가 함수의 내부와 외부에 동시에 선언된 경우, 내부에 선언된 변수를 먼저 참조한다. 또한 암묵적 전역(Implicit global) 원칙에 따라 선언되지 않은 변수를 사용해도 ReferenceError가 발생하지 않으며, 전역 객체인 window에 등록된다(window.message와 같은 형태로). 그렇다면 함수 스코프
와 이를 사용하는 var
는 어떠한 단점을 가지고 있었기에 새롭게 블록 스코프
방식이 추가된 것일까?
var
는 실수로 같은 이름의 변수를 중복 선언해도 오류를 내지 않기 때문에 다른 변수의 값을 의도치 않게 변경시킬 수 있다.이러한 문제들은 ES6에서 블록 스코프
가 추가되는 계기가 되었다.
블록 스코프
는 블록을 스코프의 기준으로 한다. 이러한 방식은 let
, const
의 등장과 함께 가능해졌으며, 변수를 사용하려는 사용처에 최대한 가까이 두고, 최소한의 유효범위를 가지는 것을 목적으로 한다. 기존에 변수 선언을 위해 사용되었던 var
는 변수를 중복 선언할 수 있고, 선언된 변수가 호이스팅된다는 문제가 있었다. 그러나 let
과 const
는 중복 선언이 불가능하고, 심지어 const
는 선언과 동시에 값을 '상수화'하여 중복 할당도 불가능하다(그렇기에 const
는 반드시 선언과 함께 값을 할당해 주어야 한다). 또한 호이스팅 과정이 다르다.
여기서 중요한 점은 let
, const
로 선언된 변수도 호이스팅 자체는 일어난다는 사실이다. 그러나 중요한 차이점이 있는데, 바로 변수 생성 과정이다. 일반적인 변수의 생성은 다음과 같이 3단계로 이루어진다.
var
의 경우 선언과 초기화가 동시에 이루어지며, 따라서 선언 이전에 접근하여도 undefined라는 초기화된 값을 반환한다. 그러나 let
, const
의 경우 선언과 초기화가 분리되어 진행된다. 즉, 선언까지는 호이스팅 되지만, 실제 초기화는 변수 선언문에 도달해야 이루어진다. 따라서 초기화 이전에 변수에 접근하려 하면 레퍼런스 에러를 반환한다. 변수를 위한 메모리 공간이 확보되지도 않았기 때문이다. 따라서 let
과 const
를 사용한 변수는 스코프의 시작점부터 초기화 지점까지 변수 참조가 불가능하다. 이러한 지점을 '일시적 사각지대(Temporal Dead Zone; TDZ)'라고 부른다.
그러나 이러한 방식이 호이스팅이 발생하지 않는 것과는 어떻게 다른 것일까? 다음 예제를 보면 let
과 const
에서도 적어도 호이스팅 자체는 일어나고 있다는 것을 알 수 있다.
let foo = 1; // 전역 변수
{
console.log(foo); // ReferenceError: foo is not defined
let foo = 2; // 지역 변수
}
위 예제에서 foo는 전역 변수 1이 출력될 것 같이 보인다. 그러나 let
으로 선언된 변수는 블록 스코프를 따르며, console.log의 foo는 지역 변수 foo 이기 때문에, 그리고 그 foo는 아직 초기화 지점을 만나지 못해 일시적 사각지대에 있으므로 레퍼런스 에러를 반환하고 있다. 만약 호이스팅이 아예 일어나지 않았다면, 뒤에 선언된 지역 변수는 고려되지 않은 채로 먼저 선언된 전역 변수 foo의 값 1이 반환되었을 것이다.