var

전역범위에서 접근이 가능한 변수를 선언하는 키워드입니다. 아무런 키워드없이 변수를 선언 및 할당하게 되면 var를 쓴 것과 같은 효과를 가집니다. 이 키워드를 사용하여 선언된 변수는 변수가 선언되는 위치에 따라 변수의 유효범위가 달라질 수 있습니다. 예를 들어, 함수 내에서 var를 사용하여 변수를 선언하면 해당 변수는 함수 내부에서만 접근할 수 있습니다. 그러나 함수 내부의 블록 (예: if 문이나 for 문) 안에서 선언된 경우에도 해당 변수는 함수 전체에서 접근할 수 있습니다. 이는 var가 함수 스코프(function scope)를 가지기 때문입니다.

스코프란?
스코프는 변수를 찾아내기 위한 규칙입니다. 규칙 중에서도 범위에 대한 규칙이죠. 함수는 자신이 선언된 위치에 의해서 자신이 유효한 참조가 될 수 있는지 정해집니다. 블록 레벨 스코프라면 코드 블록(중괄호로 표현되는 범위)안에서 선언되고 참조 가능하게 됩니다. 이와 달리 함수 레벨 스코프는 함수가 실행되는 동안 계속 참조가 가능하다는 것을 의미합니다. 물론 전역에서 선언된다면 크게 신경 쓸 필요가 없겠지만, 지역 변수라면 참조가 가능한 범위가 정해지기 때문에 아주 중요합니다.

참고로 자바스크립트에서 전역으로 선언된 var 변수는 window의 프로퍼티가 됩니다.
var globalVar = "전역 변수";

function scopeExample() {
  console.log(globalVar); // Output: 전역 변수
  if (true) {
    var localVar = "로컬 변수";
    console.log(localVar); // Output: 로컬 변수
  }
  console.log(localVar); // Output: 로컬 변수
}

scopeExample();
console.log(globalVar); // Output: 전역 변수
console.log(localVar); // ReferenceError: localVar is not defined

전역 변수로 할당된 globalVar와는 달리 로컬변수 localVar는 자신이 생성된 함수를 벗어나면 사용할 수 없게됩니다.

호이스팅

변수를 선언하는 것은 다른 코드가 실행 되기 전에 처리되기 때문에, 코드 안에서 어디서든 변수 선언을 하던지와는 상관없이 가장 먼저 처리됩니다. 10000번째 줄에서 var를 통해 변수를 선언하든 첫 번째 줄에서 선언하든 다를게 없죠. 그 이유는 자바스크립트의 변수가 가지는 특징인데, 이를 호이스팅이라고 합니다. 호이스팅은 변수에서만 발생하지는 않습니다. 함수, 변수, 클래스 또는 임포트(import)의 선언문을 해당 범위의 맨 위로 이동시키죠.

console.log(variable); // undefined

var variable = '변수'

console.log(variable); // 변수

 

분명히 첫 번째의 출력이후에 변수가 선언되는 데도 불구하고 마치 변수가 이미 선언된 것처럼 ReferenceError가 아닌 undefined가 출력됩니다. 이렇게 var로 선언된 변수는 실행시에 가장 위로 '끌어올려'지기 때문에 호이스팅이라는 이름이 붙었습니다. var의 이러한 특성때문에 코드의 예상치 못한 동작이 나타나곤 하죠. 물론 개발자가 변수의 선언 전에 해당 변수를 사용하지 않으면 예상치 못한 동작이 일어나지는 않겠지만 모든 것을 제어할 수는 없는 노릇입니다. 이 때문에 ES6(ES2015)에서 let과 const가 등장하였습니다.

 

참고로, 함수 안에서 선언된 var가 호이스팅이 발생하면 함수의 내부에서 가장 처음 실행됩니다.

let과 const

let과 const를 사용하면 호이스팅으로 발생하는 문제점을 방지할 수 있습니다. 엄밀히 따지면 let과 const를 사용한 변수 선언에서도 호이스팅이 발생하지만 "시간상 사각지대"(Temporal Dead Zone, TDZ)를 통해 이를 막습니다. 여기서 말하는 '시간상'은 실행 순서를 의미합니다. 해당 변수가 초기화(최초 할당)되기 전까지 사용이 불가능한 상태가 되고 그 상태가 지속 되는 것을 TDZ라고 표현하는 것이죠. 만약 변수가 초기화 되면 그 이후는 TDZ의 영역에서 벗어나기 때문에 자유로운 사용이 가능합니다.

참고로 let과 const가 var와 다른 점은 한 가지 더 있습니다. 바로 블록 레벨 스코프를 가지는 것이죠. 덕분에 클로저를 사용할 때, 예상치 못한 동작이 줄어들 수 있습니다.

const 더 알아보기

const를 상수로 생각하고 수정하지 않는 값을 할당할 때 사용하여야 한다고 생각할 수도 있습니다. 하지만 정확하게는 재할당과 재선언을 불가능하게 만들어주죠. 불변형 자료형이 할당된 변수를 수정하려면 재할당이 유일한 수정 방법이기 때문에 문제가 없을 것입니다. 그러나 객체를 할당하면 객체를 참조할 주소가 변수에 저장되기 때문에 그 주소에 있는 객체 내부의 값을 수정할 수 있다는 것을 알고 계셔야 합니다.

TDZ( Temporal Dead Zone )

소스 코드 전체에서 선언되지 않은 변수를 typeof를 이용해서 해당 변수의 타입을 알아보면 undefined가 되지만 TDZ 안에 있는 변수에 typeof를 사용한다면 ReferenceError가 발생합니다. TDZ에 있는 동안 변수에 접근하는 모든 동작을 금지하기 때문이죠. 이를 통해 호이스팅을 막는 것처럼 보이지만 실제로는 호이스팅된 변수에 접근하는 것을 막는 것입니다.

클로저

다음으로 넘어가기 전에 클로저에 대해 먼저 간략하게 알아보겠습니다. 바로 이해가 되지 않더라도 스코프와 함께 사용되는 예시를 보면 조금더 이해가 잘 될 것입니다.


자바스크립트에서 함수는 자신이 생성(선언)되었을 때의 문맥을 기억합니다. 이를 렉시컬 스코프라고 하죠. 그리고 렉시컬 스코프와 해당 스코프에서 생성된 함수를 통틀어 클로저라고 합니다.

function lexicalEnviromentFn() {
    const a = '렉시컬 환경'
    const functionWithClosure = function() { console.log(a) };
    return functionWithClosure
}

const closure = lexicalEnviromentFn();
closure(); // 렉시컬 환경

 

lexicalEnviromentFn 함수는 functionWithClosure를 만들어내는데, functionWithClosure 함수가 만들어진 환경인 lexicalEnviromentFn 내부의 변수등을 기억하기 때문에 closure에 할당된 함수가 '렉시컬 환경'이라는 문자열을 출력합니다.

 

모던 자바스크립트 Deep Dive, Closure에서 사용 예시를 보시면 좋습니다. 해당 페이지에는 클로저를 사용해 전역변수를 사용하지 않고 Counter기능을 구현한 코드가 있습니다.

스코프와 클로저(Closure)

var는 함수 레벨 스코프 let은 블록 레벨 스코프이기 때문에 다음과 같은 차이점이 발생합니다.

var arr = [];

for (var i = 0; i < 5; i++) {
  arr[i] = function () {
    return i;
  };
}

for (var j = 0; j < arr.length; j++) {
  console.log(arr[j]());
}

위의 코드는 5가 5번 출력됩니다.

const arr = [];

for (let i = 0; i < 5; i++) {
  arr[i] = function () {
    return i;
  };
}

for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]());
}

이 코드는 0부터 4까지 출력되죠


이러한 차이점은 언급했듯 var와 let의 스코프 차이에 의한 것입니다. var를 사용한 코드는 함수 레벨 스코프를 가지는데 for문의 반복이 끝나도 사라지지 않습니다. 코드에서 반복문마다 만들어지는 함수는 모두 같은 변수를 참조하죠. 그래서 5번의 반복에서 생성된 함수는 같은 렉시컬 환경, 즉 하나의 똑같은 변수를 공유합니다. 처음에는 i=0이므로 arr에 0이 들어가지만 i=1인 다음 반복에서 해당 변수가 1로 바뀝니다. arr에 들어간 첫번째 수도 같은 변수이기 때문에 1로 바뀝니다. 이를 5번 반복하니 결국 모든 숫자가 5가 되는 것이죠.

왜 4가 5개 나오는 게 아닌가요?
자바스크립트의 반복문이 실행되는 원리때문입니다. i는 i++에 의해 계속 1씩 증가한 뒤 i < 5의 조건문에 해당하는지 판단하게 됩니다. 결론적으로 i가 5일 때, 해당 반복문이 멈추기 때문에 4가 아닌 5가 최종적인 값이 됩니다.

참고 자료

MDN, [var, const, let, 호이스팅, Closure ]
모던 자바스크립트 Deep Dive, [Scope, Closure]

+ Recent posts