변수 선언 키워드 - let, const 그리고 var
재선언 | 재할당 | Scope | |
var | 가능 | 가능 | Function |
let | 불가능 | 가능 | Block |
const | 불가능 | 불가능 | Block |
JS를 사용할 때 var, let, const의 차이를 구분하여 사용하고 있는가? 막연하게 var는 예전 방식이니까 쓰지 말아야지 라는 생각을 갖고있진 않는가? 오늘은 var, let, const의 차이에 대한 이야기를 해보겠다. 이 포스팅을 요약하면 위의 표와 같다.
1. 선언과 할당
var a = 'str';
var a = 0; //OK
let b = 'str';
let b = 0 //Error!
재선언이라는 것은 변수를 다시 선언한다는 의미이다. 위 코드처럼 var는 같은 이름의 변수를 여러번 선언할 수 있다. 그러나 let, const는 단 한번만 선언 가능하며 재 선언시 오류가 발생한다(단, 크롬 개발자 콘솔의 경우 let 변수를 다른 줄에 재선언 시 오류를 발생시키지 않는다. 참고링크)
var a = 'str';
a = 0; //OK
let b = 'str';
b = 0 //OK
const c = 'str';
c = 0 //Error!
재할당은 변수에 새로운 값을 다시 할당해준다는 의미이다. 위 코드에서 var와 let으로 선언된 변수는 변수의 값을 추후에 변경가능하다. 그러나 const는 변경할 수 없다.(const가 객체와 같은 Reference type을 갖고있을 때 객체 내부의 값을 변경 가능한 것은 주소를 저장하기 때문에 주소가 변경된 것이 아니기 때문이다. 더 자세한 내용은 이전 포스팅 불변성/가변성 포스팅1, 불변성/가변성 포스팅2에서 알아보자)
1-1. 더 자세히
[변수 생성과정]
1. 선언: JS 엔진에 변수의 식별자(이름)를 등록한다.
2. 초기화: 메모리 값을 undefined 초기화 (undefined)
3. 할당: undefined로 초기화된 메모리에 다른 값을 할당한다.
변수의 생성 과정을 자세히 알아보자. 변수는 선언 > 초기화 > 할당 3단계에 걸쳐 생성된다. 선언은 JS 엔진에 변수의 이름을 등록하여, 이 이름을 변수로 사용하고 있다는 것을 알리는 단계로, 메모리에 이름을 붙이는 단계이다. 초기화는 메모리를 undefined라는 값으로 초기화하는 단계이다. 마지막으로 할당은 생성된 변수에 개발자가 원하는 값을 넣어주는 단계이다.
var a;
a = 0;
let b;
b = 0;
const c; //error!
c = 0;
변수생성과정에서도 var와 let, const가 차이가 있다. var는 선언과 초기화가 동시에 진행되며, let은선언과 초기화가 분리되어 진행된다. const는 let과 유사하나, 코드 상에서 반드시 선언과 할당이 동시에 이루어져야한다. 이는 호이스팅 결과의 차이로 드러난다.
1-2. 호이스팅
function fn() {
console.log(a); //호이스팅 -> 출력: undefined
var a = 0;
}
호이스팅이란 변수의 선언을 스코프의 최상단으로 끌어올리는(그렇게 보여지는) 것이다. 예를 들어 함수 내에서 정의된 변수는 함수의 최상단으로 선언이 이동한다. 위 코드에서 var로 선언된 변수 a는 선언 이전에 접근하여 console.log()로 값을 출력하고 있다. 이 경우 에러가 발생하지 않고 undefined가 출력된다. 그 이유는 변수 a의 선언이 함수의 최상단으로 호이스팅되었기 때문이다. 이 때 var는 선언과 초기화가 동시에 진행되므로 a의 값이 undefined로 출력된 것이다.
function fn() {
console.log(a); //호이스팅 -> error!
let a = 0; //const도 마찬가지
}
변수를 let으로 선언하면 어떨까? let은 선언과 초기화가 분리되어 진행된다. 따라서 호이스팅되어도 메모리가 아직 할당되어있지 않다. 따라서 undefined 출력이 아닌, 에러가 발생하는 것이다. 이 때 변수 a는 Temporal Dead Zone에 위치한다고 말한다. Temporal Dead Zone은 변수가 선언되었으나 초기화되지 않은 변수가 위치하는 구간으로 선언 전에 변수를 사용하지 않도록 하기위한 개념상의 공간이다. 호이스팅된 let과 const 변수가 이에 속한다.
2. Scope
function fn(){
var a = 0;
if(a===0) { var b = 0; a--; }
console.log(a, b); //-1 0
}
위 코드에서 console.log()에서 b에 접근 가능한 이유는 var로 선언된 변수는 Function Scope를 갖기 때문이다. function scope란 함수 내에서 해당 변수에 접근 가능하다는 것을 의미한다. a와 b는 fn()이라는 함수 내에서 var라는 키워드로 선언되어있으므로 fn() 내라면 어떤 곳에서든 접근 가능하다.
function fn(){
let a = 0;
if(a===0) { let b = 0; a--; }
console.log(a, b); //error!
}
let으로 변수를 선언하면 어떨까? console.log()에서 b에 접근 불가능하므로 에러가 발생한다. let으로 선언된 변수는 Block Scope를 갖기 때문이다. block scope란 if, while문 등 중괄호로 둘러싸인 블록에서 해당 변수에 접근 가능하다는 것을 의미한다. 따라서 b는 if문 블록에서만 유효하고, console.log()에서는 유효하지 않다.
한편 if문 블록 내의 a는 어떨까? JS는 변수를 참조할 때 먼저 자신이 속한 Scope에서 찾고, 해당 스코프에 존재하지 않다면 상위의 스코프에서 찾아나간다. 이를 스코프 체인이라고 한다. if문 내에 a가 존재하지 않으므로 더 상위의 스코프에서 a를 가져와 연산을 수행한다. 따라서 if문 내의 a--; 문장은 오류를 발생시키지 않는다.
3. var를 쓰면 안되는 이유
function fn() {
console.log('hoisting', a); //?
var a = 0;
if(a === 0) { var a = 'str'; var b = 2; }
for(var i = 0; i<2; i++)
console.log('redeclaration, scope', a, b); //?
console.log(i); //?
}
이제 var를 쓰면 안 되는 이유를 생각해보자. 먼저 var는 재선언이 가능하다. 따라서 a라는 변수를 이전에 선언한 것을 잊어버리고 a라는 변수를 재선언하는 상황도 가능한 것이다. 이러한 경우 코드를 이해하기 어렵게 만들며, 오류가 발생할 가능성도 크다. 심지어 호이스팅 되기 때문에 변수를 선언하기도 전에 접근할 수 있어 결과를 예측하기 어렵다. 변수가 function scope를 가짐으로써 격리된 환경에서 변수를 사용할 수 없으므로 이 또한 코드를 예상하기 어렵게 만든다. 따라서 var의 사용을 지양하고 변수는 let, 상수는 const로 선언하는 것이 좋다!