1. 개요
var, let, const는 JavaScript에서 변수를 선언할 때 사용하는 키워드이다.
ES5 이전까지는 var만 쓰였으나, ES6 버전에 let과 const가 새롭게 추가되었다.
2. 미리 알아두어야 하는 개념
2.1. 선언과 초기화
우선 선언과 초기화에 대한 개념을 살펴볼 필요성이 있다.
선언은 변수나 함수를 프로그램에서 사용할 수 있도록 이름을 등록하는 과정을 의미하며, 초기화는 변수가 생성된 이후에 처음으로 값을 대입하는 과정을 의미한다.
2.2. 호이스팅(Hoisting)
JavaScript의 엔진은 코드를 실행시키기 전, 코드의 정보를 미리 저장해 놓는다.
따라서 코드가 실행되기 전에 변수, 함수, 클래스, import 등의 정보가 수집된다.
이런 특성 덕택에 JavaScript는 특정 변수 및 함수 선언문을 코드 최상위에 올려놓은 것과 같은 효과가 나타나며, 이에 따라 호이스팅이라는 이름이 붙게 되었다.
(참고로 호이스팅은 '끌어올림'이라는 뜻을 가진다)
예를 들어 아래의 코드를 살펴보자. (실행 결과는 '더 보기' 클릭)
function foo(x) {
console.log(x);
var x;
console.log(x);
var x = 2;
console.log(x);
}
foo(1);
1
1
2
위의 코드는 아래와 같다고 해석할 수 있다.
(변수의 정보가 미리 수집되어 저장되기 때문에 마치 아래 코드처럼 동작한다)
function foo(x) {
var x;
var x;
var x;
x = 1;
console.log(x);
console.log(x);
x = 2;
console.log(x);
}
foo();
다음은 '함수 선언문'이 '호이스팅' 되는 경우이다. (실행 결과는 '더 보기' 클릭)
function foo() {
console.log(b);
var b = 'abc';
console.log(b);
function b() {}
console.log(b);
}
foo();
[Function: b]
abc
abc
보면 알겠지만, 우리의 직관과는 상이한 결과가 출력되었다.
그 이유는 함수 선언문도 호이스팅의 대상이기 때문에 아래 코드처럼 동작했기 때문이다.
function foo() {
var b;
var b;
b = b() {};
console.log(b);
b = 'abc';
console.log(b);
console.log(b);
}
foo();
3. 키워드 알아보기
3.1. var
JavaScript는 ES5까지 변수를 선언하기 위해서는 var 키워드만 사용할 수 있었다.
var 키워드는 아래와 같은 특징을 지닌다.
1. 선언과 동시에 undefined로 초기화가 이루어진다.
var의 가장 큰 특징으로 선언과 동시에 undefined로 초기화가 이루어진다.
예를 들어 다음과 같은 코드가 있다고 가정해 보자.
console.log(a); // undefined
var a = 10;
변수 a는 console.log보다 밑에서 선언되었지만, console.log의 출력 결과는 undefined이다.
앞서 살펴본 호이스팅 원리로 생각해 보면 위의 코드는 아래와 같이 동작한다고 볼 수 있다.
var a;
console.log(a); // undefined
a = 10;
일단 호이스팅 되었으니 console.log는 변수 a를 참조할 수 있다.
하지만 변수 a를 선언만 했을 뿐, 초기화는 하지 않았으나 undefined가 뜬 것을 볼 수 있는데(undefined도 하나의 값이다), 이는 var가 선언과 동시에 undefined로 초기화시키는 역할도 동시에 하기 때문이다.
2. 재선언이 가능하다.
한 번 var로 선언한 변수는 또다시 var로 선언할 수 있다.
var a = 'hello';
var a = 'world';
console.log(a); // world
3. 함수 범위를 갖는다.
범위(scope)는 변수가 참조될 수 있는 영역을 의미한다.
var로 선언된 변수는 함수 범위를 갖는다. (즉 함수로 둘러싸인 중괄호 내부에서만 독립적인 범위를 갖는다)
foo 함수에서 선언되고 정의된 변수 a는, 범위가 foo 함수 내부이기 때문에 bar 함수 내에서 참조되지 않는다.
function foo() {
var a = "hello";
}
function bar() {
console.log(a); // ReferenceError: a is not defined
}
foo();
bar();
if문은 함수가 아니다.
따라서 전역에 위치한 변수 a와 if문 내부의 변수 a는 모두 같은 범위에 속해있으며, 이에 따라 a가 다른 값으로 할당된 것을 볼 수 있다.
(같은 범위에 속해있기 때문에 var 특성상 재선언 되었다고 봐야 한다)
var a = "hello";
if (true) {
var a = "world";
console.log(a); // world
}
console.log(a); // world
4. 재할당이 가능하다.
같은 변수에 여러 번 값을 재할당할 수 있다.
(재선언과의 차이점은 변수 앞에 키워드가 붙었는지의 여부이다)
var a = 'hello';
a = 'world';
console.log(a); // world
3.2. let
ES6에 등장한 키워드이다.
let 키워드는 아래와 같은 특징을 가지고 있다.
1. 선언과 동시에 undefined로 초기화되지 않는다.
var와의 첫 번째 차이점이다. let은 선언과 동시에 바로 undefined로 초기화되지 않는다.
따라서 다음과 같은 현상을 목격할 수 있다.
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a;
let은 a와 다르게 선언과 동시에 undefiend로 초기화되지 않는다.
따라서 console.log를 실행하는 시점에서 변수 a를 참조하였으나, 변수 a가 초기화되지 않았기 때문에 출력할 값을 띄울 수가 없어 위와 같은 에러가 발생한 것이다.
의문 1. let은 호이스팅 되지 않아 발생한 에러가 아닌가?
아니다. let도 호이스팅이 발생한다.
만약 호이스팅 되지 않았다면 console.log를 실행하는 시점에서 변수 a를 참조할 수 없기 때문에 '정의되지 않음' 에러가 발생해야 한다.
대표적인 경우가 아래 코드이다. (console.log가 변수에 참조할 수 없는 상황을 구현함)
console.log(a); // ReferenceError: a is not defined
let b;
더 나아가 이런 경우를 TDZ(Temporal Dead Zone)이라 부른다.
변수의 선언과 초기화 사이에 일시적으로 값을 참조할 수 없는 구간을 의미한다.
의문 2. 아래와 같은 코드는 값이 명시적으로 할당되지 않았는데 undefined가 출력되었다. 왜 그런 것인가?
let b;
console.log(b); // undefined
let의 경우, JavaScript는 코드 실행과정에서 변수 선언문을 만났을 때 초기화가 수행된다.
따라서 let b; 부분을 실행하며 b에 undefined가 할당되었고, 그 결과 console.log에 undefined가 출력된 것이다.
반면 아래 코드는 let b;를 만나기 전에 console.log를 먼저 만났다.
앞서 살펴본대로 변수 b는 아직 초기화되지 않았기 때문에 TDZ 상태이며, 이에 따라 에러가 발생하게 된다.
console.log(b); // ReferenceError: Cannot access 'a' before initialization
let b;
다시 한 번 복습을 해보면 var는 선언과 동시에 undefined로 초기화가 되기 때문에 TDZ가 없다.
console.log(b); // undefined
var b;
2. 재선언이 불가능하다.
한 번 let으로 선언한 변수는 또다시 let으로 선언할 수 없다.
let a = 10;
let a = 10; // SyntaxError: Identifier 'a' has already been declared
3. 블록 범위를 갖는다.
블록이란 중괄호로 둘러싸인 공간을 의미한다.
함수 내부, 조건문 내부, 반복문 내부 등등 모두가 블록에 해당한다.
if문에 의해 중괄호로 둘러쌓인 영역 역시 하나의 독립된 블록 범위가 된다.
따라서 이곳 안에서 선언된 let a는 전역 범위에 속한 let a와 서로 다른 범위에 속한다.
(서로 다른 범위기 때문에 재선언에 대한 오류가 발생하지 않는 것은 덤)
let a = "hello";
if (true) {
let a = "world";
console.log(a); // world
}
console.log(a); // hello
4. 재할당이 가능하다.
같은 변수에 여러 번 값을 재할당할 수 있다.
let a = 10;
a = 20;
console.log(a); // 20
3.3. const
ES6에 등장한 키워드이다.
const 키워드는 let키워드와 성질이 비슷하나 단 한 가지 명확한 차이점이 있다.
선언과 동시에 명시적으로 초기화를 해줘야 하며, 한 번 초기화하면 절대로 재할당 할 수 없다.
즉 아래 처럼 사용할 수 없다.
const a; // SyntaxError: Missing initializer in const declaration
const b = 1;
b = 2; // TypeError: Assignment to constant variable.
const 키워드에 대한 특징도 살펴보자.
1. 선언과 동시에 undefined로 초기화되지 않는다.
const 역시 호이스팅이 발생하나, let과 같이 TDZ가 존재한다.
따라서 다음과 같은 오류를 목격할 수 있다.
console.log(a); // ReferenceError: Cannot access 'a' before initialization
const a = 10;
2. 재선언이 불가능하다.
한 번 const로 선언한 변수는 또다시 const로 선언할 수 없다.
const a = 10;
const a = 10; // SyntaxError: Identifier 'a' has already been declared
3. 블록 범위를 갖는다.
let과 마찬가지로 블록 범위를 갖는다.
const a = "hello";
if (true) {
const a = "world";
console.log(a); // world
}
console.log(a); // hello
4. 재할당이 불가능하다.
앞서 미리 설명한 것과 같다.
const는 재할당이 불가능하다.
3. var 사용을 지양해야 하는 이유
eslint의 규칙에 'no-var'가 있을 정도로, 모던 웹 개발에서는 웬만한 경우가 아니고서는 var 사용이 금기시된다.
var는 다양한 버그를 발생시킬 가능성이 높기 때문이다.
주로 어떤 부분에 있어서 var가 문제가 되는지 살펴보자.
1. 선언과 동시에 undefined가 할당되는 문제
var는 선언과 동시에 undefined가 할당되기 때문에 TDZ가 없다는 특성을 앞에서 언급한 바 있다.
우리가 글을 읽을 때 위에서 아래로 글을 읽듯, 코드의 논리 흐름도 위에서 아래로 최대한 이어지는 것이 자연스럽고 가독성도 높다.
가독성이 높다는 것은 코드를 이해하기 쉽다는 것이며, 코드를 이해하기 쉬워야 버그 발생을 줄일 수 있다.
글을 읽을 때의 순서가 한 방향으로 이어지지 않고, 왔다 갔다 뒤죽박죽이라면 읽기에도 피곤할뿐더러 이해하기도 어려워진다.
코드도 마찬가지라고 생각한다.
let과 const에서 TDZ에러가 발생하는 것은, 부수적으로 코드를 위에서 아래로 짤 수 있게끔 강제하는 효과가 있다고 생각한다.
두 번째로 undefined가 뜨는 경우의 수를 줄일 수 있다는 점에 있다고 생각한다.
JavaScript에서는 정말 생각치도 못한 다양한 이유로 undefined를 마주할 수 있는데, 이 원인을 찾아가는 과정이 꽤 많은 시간과 노력을 필요로 한다.
var를 사용하지 않는다면, 그 특성에 의한 undefined 출력을 방지할 수 있게 되어 디버깅하기 용이해진다고 생각한다.
2. 함수 범위 && 재선언이 가능하다는 특성
개발자를 힘들게 만드는 원인 중 하나가 바로 변수의 이름 짓기라 생각한다.
코드의 규모가 커지면 같은 변수 이름을 여러 곳에서 사용하게 되는 경우가 생길 수 있다.
var는 함수 범위를 가지고 있고 재선언이 가능하다.
따라서 특정한 곳에서 변수의 이름이 겹치고(그리고 개발자가 이를 인지하지 못했을 경우) 매우매우 찾기 힘든 버그를 발생시킬 수 있다.
var a = 1;
// 중간에 엄청 많은 코드들이 있다고 가정
if (true) {
var a = 2;
}
// 중간에 엄청 많은 코드들이 있다고 가정
console.log(a); // 어? 왜 1이 아니라 2가 나오지???
또한 var가 블록 범위가 아닌 함수 범위를 갖는다는 특성은, 우리의 직관과 일치하지 않는 코드 결과를 얻을 수 있음을 내포하고 있다.
아래 코드를 살펴보자.
for문에서 사용된 변수 i는 for문 내에서만 유효할 것이라는 우리의 직관과 다르게, for문 바깥에서도 참조되고 있음을 볼 수 있다.
변수 i를 var로 선언했기 때문이다. (따라서 아래 코드의 경우, 변수 i는 전역에서 접근 가능하다)
for (var i = 0; i < 5; i++) {}
console.log(i) // 5
또한 아래 코드의 실행 결과는 더더욱 우리의 직관과 멀어진다.
var arr = [];
for (var i = 0; i < 5; i++) {
arr[i] = function () {
return i;
};
}
console.log(arr[0]()); // 5
console.log(arr[1]()); // 5
console.log(arr[2]()); // 5
console.log(arr[3]()); // 5
console.log(arr[4]()); // 5
이런 일이 발생하는 이유는 var가 함수 범위 스코프를 갖기 때문이다.
각 arr에 할당되는 익명 함수가 리턴하고 있는 변수 i는 앞서 언급하였듯 전역에서 접근 가능하다.
for문이 종료된 이후에도 변수 i는 소멸되지 않았으며, 그 값은 5가된다.
또한 변수 i는 각 arr에 할당된 익명함수가 참조하고 있다.
따라서 모든 결과가 5가 나오는 것이다.
위의 문제를 해결하기 위해서는 var를 let로 변경하면 된다.
let은 블록 범위를 가지므로 변수 i는 for문 내에서만 유효하다.
따라서 익명함수는 반복문이 진행되는 순간순간의 변수 i를 참조하게 되고, 따라서 그 결과가 1, 2, 3, 4, 5가 나오게 된다.
var arr = [];
for (let i = 0; i < 5; i++) {
arr[i] = function () {
return i;
};
}
console.log(arr[0]()); // 1
console.log(arr[1]()); // 2
console.log(arr[2]()); // 3
console.log(arr[3]()); // 4
console.log(arr[4]()); // 5
'JavaScript' 카테고리의 다른 글
클로저 (Closure) (0) | 2024.01.01 |
---|---|
실행 컨텍스트 (Execution Context) (0) | 2023.12.28 |
프로토타입(Prototype) (0) | 2023.10.07 |
옵셔널 체이닝(Optional Chaining) (1) | 2023.10.04 |
단축평가(Shortcut Evaluation) (0) | 2023.10.03 |