핵심적인 내용만 정리해보자.
식별자, 즉 변수가 유효한 범위를 뜻한다. 기본적으로 먹이사슬처럼 스코프는 중첩에 의해 계층적 구조를 이루고, 하위 스코프에서 상위 스코프로 나아가며 변수를 검색하는 형식이다. 즉 상위 스코프에선 하위 스코프의 변수를 참조할 수 없지만 그 반대는 가능하다. 상속과 유사하다.
자바스크립트는 코드 블록에 의해 지역 스코프를 만드는데, 여기서 var 키워드는 오로지 함수의 코드 블록만을 지역 스코프로 인정하고 다른 것들은 블록 레벨 스코프를 따른다.
var i = 5;
if (true) {var i= 10;}
이런 경우 i는 전역 변수로 작동하여 10이 되는 형태다.
렉시컬 스코프
그리고 자바스크립트의 특징은 렉시컬 스코프이다. 간단히 함수의 상위 스코프는 함수를 호출한 곳이 아니라 함수를 정의한 곳에 의해 결정된다는 특징이다.
간단한 예제로 정리해보자.
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x);
}
foo();
bar();
이 경우 1이 두번 출력될 것이다. foo 함수 호출 시, bar 함수는 foo 함수 안에서 호출된다. 여기서 bar 함수는 x를 출력할 것이지만, JS는 렉시컬 스코프를 따르기 때문에 foo 함수의 지역 변수값인 10이 아닌 전역 변수 1을 출력하는 셈이다.
변수에게는 생성 - 소멸하는 생명 주기가 있다. 함수 내부의 지역 변수는 함수가 호출되면 생성되고 함수가 종료하면 소멸된다. 지역 변수의 생명 주기는 함수의 생명주기와 같다는 것만 다뤄보자. 조금 구체적으로 말하면, 변수의 생명 주기는 메모리 공간이 확보된 시점부터 메모리 공간이 해제되어 가용 메모리 풀에 반환되는 시점까지이다.
여기서 호이스팅을 조금 자세하게 다뤄보면,
function foo(){
console.log(x);
var x = 'local';
}
foo();
이 예제에서 foo를 실행하였을 때 무슨 값이 출력될까? 정답은 JS의 호이스팅은 스코프를 단위로 동작하기 때문에 undefined가 출력된다. 즉, 호이스팅은 변수 선언이 스코프의 선두로 끌어올려진 것처럼 동작하는 것을 말한다. 생명 주기를 생각하자면 x라는 변수는 foo가 호출됐을 때 생성되고 코드 블록이 끝나면서 소멸되는 것이다.
var 키워드로 선언한 전역 변수의 생명 주기는 전역 객체, 즉 브라우저의 생명 주기와 같다고 생각하면 된다. 그렇기에 생기는 문제점이 있다. 변수의 유효 범위가 넓어 생기는 위험이 존재하고, 생명 주기가 길기에 메모리를 오래 소비하고, 스코프 체인의 꼭대기이기 때문에 검색 속도가 느리고, 다른 파일 내에서 동일한 이름의 변수가 존재한다면 오류가 발생할 수 있다. 그러므로 이것의 사용을 억제하는 방법 중에서 주로 라이브러리를 만드는데 사용되는 즉시 실행 함수를 이용한 방법을 보자.
(function (){
code..
}());
와 같이 코드의 모든 내용을 즉시 실행 함수로 감싸면 전역 변수가 아닌 즉시 실행 함수의 지역 변수로써 동작한다. 이 외에 모듈을 이용한 방법은 이후 클로저를 다룰 때 살펴보겠다.
var 키워드는 문제가 존재한다. 변수의 중복 선언을 허용하고, 아까 언급한 함수 레벨 스코프만을 가지고 변수 호이스팅이 일어난다. 그렇기에 ES6에서 새로 도입된 키워드가 let 과 const 이다.
먼저 let 키워드를 살펴보면, 변수의 중복 선언이 일어나지 않고 블록 레벨 스코프를 가진다. 그리고 var 키워드일 때 undefined가 출력되는 상황에서 ReferenceError를 출력한다.(호이스팅이 일어나지 않는 것처럼 동작함) var 키워드는 변수의 선언 단계에서 임의로 undefined로 초기화를 하는 개념에 반해 let은 선언과 초기화를 분리하기 때문에 ReferenceError가 출력되는 것이다. 예시를 보자.
console.log(foo);
let foo;
console.log(foo);
foo = 1;
console.log(foo);
이와 같은 상황에서 각각의 출력결과는 어떨까? 먼저 맨 처음은 호이스팅이 일어나지 않는 것처럼 동작하기 때문에 ReferenceError일 것이다. 그 다음은 선언 후 초기화 하지 않았으므로 undefined, 마지막은 물론 1일 것이다.
여기서 맨 처음이 ReferenceError임을 자세히 알아보자. let 키워드는 TDZ라는 일시적 사각지대가 존재한다. 이것은 스코프의 시작 지점부터 변수의 초기화가 시작되기 전까지(선언)의 구간을 뜻하는데, 이 구간동안 변수를 참조하면 ReferenceError를 띄운다. 즉 이 예제에선 첫 번째는 선언 전이므로 ReferenceError, 두 번째는 선언만 하였으므로 undefined, 마지막은 1인 셈이다.
그렇다면 const를 살펴보자. 이것은 상수를 위한 키워드이다. 여기서 상수란 단순히 재할당이 금지된 변수라고 생각하면 편하다. 반드시 선언과 동시에 초기화 해야 하며, 블록 레벨 스코프를 가진다. 재할당이 금지되었다는 것이 무슨 뜻인가에 대해 조금 생각해보자. 만약 원시 값을 할당했다고 가정하면, 원시 값은 변경이 불가능하기 때문에 이것은 불변한 상수로 전혀 변경할 수 없을 것이다. 그렇지만 객체를 할당했다고 가정하자. 이것은 원시 값이 아니고, 재할당을 할 필요 없이 객체의 프로퍼티에 접근해 값을 변경하는 것이 가능하다. 예를 들면 다음과 같을 것이다.
const person = {
name = 'kim'
};
person.name = 'lee';
person.weight = 100;