ES5 문법에서 변수 선언은 var
키워드를 사용했다. 이후 2015년 ES6 문법부터는 let
, const
키워드가 도입된다. var
가 가지고 있던 단점들을 해결하는 더 나은 대안으로 인정받고 있다.
JavaScript에서 변수 선언 방식인 var
, let
, const
에 대하여 알아보자.
var
1. 함수 스코프를 가지며, 함수 내에서 선언된 변수는 함수 전체에 접근 가능하다.
변수가 선언된 함수 내에서만 유효하며, 함수 외부에서는 접근할 수 없다는 의미이다. 함수 외부에서 선언되었다면 전역(global scope)으로 사용될 수 있다.
var name = 'Heungmin';
function getNumber() {
var number = 7;
}
console.log(name); // Heungmin
console.log(number); // Uncaught ReferenceError: number is not defined
name
은 함수 외부에서, number
는 함수 내부에서 var
로 선언된 변수들이다. 따라서 변수 name
은 함수 외부에서 접근할 수 있지만, number
는 함수 외부에서 접근하려고 하면 ReferenceError
가 발생한다.다.
2. 블록 스코프를 무시하고, 변수가 호이스팅 되어 선언 전에도 사용할 수 있다.
var
는 블록 스코프를 무시하고 함수 스코프를 가지기 때문에, 블록 내에서 선언된 var
변수는 블록 외부에서도 접근 가능하다. 이것이 호이스팅 개념과 관련이 있다.
function getNumber() {
if (true) {
var number = "I'm number 7.";
}
console.log(number); // 블록 외부에서 `var` 변수에 접근 가능
}
getNumber(); // I'm number 7.
변수 number
는 if
블록 내에서 선언되었다. 하지만 console.log(number);
는 블록 외부에서 호출되었다. var
는 함수 스코프를 가지므로 블록 스코프를 무시하고 함수 전체 즉, 블록 외부에서 접근가능하다.
그리고 이 코드가 실행될 때 호이스팅이 일어나서 함수가 실행되기 전에 변수 선언이 끌어올려진다.
function getNumber() {
var number; // 호이스팅: 변수가 선언되고 undefined로 초기화됨
if (true) {
number = "I'm number 7."; // 실제 할당은 여기서 이루어짐
}
console.log(number); // 블록 내에서 선언한 'var' 변수를 외부에서 접근 가능
}
getNumber(); // I'm number 7.
var
가 가지고 있는 특성 중 호이스팅은 할당 이전에 언제나 undefined
를 반환하기 때문에 에러를 발생시키지 않지만, 프로그램의 가독성을 떨어뜨리고 어디서 에러가 발생했는지 예측하기 어렵게 만든다.
3. 중복 선언이 가능하며, 값을 재할당할 수 있다.
만약 var
로 선언한 전역변수 이름과 지역변수 이름이 같아 충돌한다면?
var name = "Global"; // 전역 변수
function example() {
var name = "Local"; // 전역 변수와 동일한 이름의 지역 변수 중복 선언
console.log(name); // Local (지역 변수 참조하여 지역 변수 출력)
}
example();
console.log(name); // Global (전역 변수 출력)
위 코드에서는 동일한 이름의 변수 name
이 전역 변수, 지역 변수로 선언되었다. 함수 내에서 name
을 출력하면 함수 스코프 내에 있는 지역 변수 name
이 출력된다. 이 지역 변수는 함수 내부에서만 유효하며 전역 변수와 이름이 같더라도 서로 영향을 주지 않는다. 또한, 함수 외부에서 name
을 출력하면 전역 변수 name
이 출력된다. 함수 내부에서 선언된 지역 변수와 전역 변수는 서로 독립적이기 때문에 함수 호출에 따라 전역 변수의 값이 변하지 않는다.
만약 A 개발자가 name
이라는 변수를 이미 선언했는데, 이를 모르고 있던 B 개발자가 중복 선언해서 값을 할당한다면?
var name = 'Heungmin';
console.log(name); // Heungmin
var name = 'Kangin';
console.log(name); // Kangin
이렇게 var
는 같은 이름으로 여러 번 선언되어도 런타임 시 에러가 발생하지 않고, 마지막 선언이 이전의 선언을 덮어쓰는 특성을 가지고 있다. 하지만 이러한 특성은 코드의 가독성과 유지보수를 어렵게 만들고, B 개발자는 의도치 않게 변경된 값을 받게 된다.
글로벌 오염
주로 변수의 스코프에 관련하여 사용되는 용어이다. 글로벌 오염은 전역 스코프에서 변수를 선언할 때 발생하는 문제를 나타낸다. 이런 문제는 특히 var
를 사용할 때 발생한다. var
의 함수 스코프 동작은 예상치 못한 변수 충돌이나 동작을 일으킬 수 있기 때문에 let
또는 const
를 사용하여 지역 스코프를 명시적으로 지정해서 전역 스코프에서 변수를 오염시키는 문제를 최소하하는 것이 좋다.
let
1. 블록 스코프를 가진다.
변수가 선언된 블록(일반적으로 중괄호 {}
로 둘러싸인 영역) 내에서만 유효하여, 블록 외부에서는 접근할 수 없다는 의미이다.
function example() {
let x = 10; // 함수 스코프를 가지는 변수 x
if (true) {
let y = 20; // 블록 스코프를 가지는 변수 y
console.log(x); // 10 (함수 스코프이므로 블록 내에서도 접근 가능)
console.log(y); // 20 (블록 스코프 내에서 선언된 변수이므로 접근 가능)
}
console.log(y); // ReferenceError: y is not defined (블록 외부에서 접근 불가)
}
example();
console.log(x); // ReferenceError: x is not defined (함수 외부에서 접근 불가)
함수 스코프를 가지는 변수 x
와 블록 스코프를 가지는 변수 y
가 선언되었다. 변수 x
는 example
함수 내에서만 유효하며, 변수 y
는 블록 내에서만 유효하다.
console.log(x);
: 함수 스코프를 가지는 변수x
에 접근하여 값을 출력했다. 함수 스코프이므로 블록 내에서도 접근 가능합니다.console.log(y);
: 블록 스코프를 가지는 변수y
에 접근하여 값을 출력했다. 블록 내에서 선언되었기 때문에 블록 내에서만 접근 가능하다.// console.log(y);
:y
는 블록 외부에서 접근했기 때문에 에러가 발생한다.// console.log(x);
:x
는 함수 외부에서 접근했기 때문에 에러가 발생한다.
2. 호이스팅이 일어나지만, 초기화되기 전에는 접근할 수 없다.
let
은 평가 단계에서 선언만 이루어지며, 이후 실행 단계에서 선언문을 만나면 값이 초기화된다.
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 10;
console.log(x); // 10
따라서 변수 x
는 호이스팅에 의해 선언은 끌어올려지지만, 초기화 전에 접근하기 때문에 ReferenceError
가 발생한다.
3. 중복 선언은 불가능하며, 재할당은 가능하다.
같은 스코프 내에서 let
으로 선언된 변수 이름을 중복해서 선언하면 문법 에러가 발생한다. 또한, let
변수는 재할당이 가능하다.
let name = 'Heungmin';
let name = 'Kangin'; // Uncaught SyntaxError: Identifier 'name' has already been declared
let league = 'EPL';
league = 'League 1';
console.log(league); // League 1
변수 name
은 같은 스코프 내에서 중복 선언되었으므로 문법 에러가 발생했다. 코드 천 줄 만 줄 짜다 보면 중복된 변수 만들 수 있을텐데 let
을 사용하면 방지할 수 있겠다. 그리고 재할당이 가능하므로 League 1
이라는 값이 출력된다.
const
1. 블록 스코프를 가지며, 선언과 동시에 초기화해야 한다.
const
는 let
과 마찬가지로 블록 스코프를 가지며, 호이스팅이 일어나지 않는 것처럼 동작한다. 그리고 선언과 동시에 초기화하지 않으면 문법 에러가 발생한다.
const x; // SyntaxError: Missing initializer in const declaration
2. 재할당이 불가능하다. 즉, 선언 후에 값이 변경될 수 없다.
const
는 재할당이 금지된다. const
로 선언된 변수에 원시 값을 할당한 경우 값을 변경할 수 없다. 이러한 특성을 이용해 상수를 선언하는 키워드로 사용하여 가독성과 유지보수성을 보장한다.
// 3.14의 의미를 알기 어렵기 때문에 가독성이 좋지 않음
if (round > 3.14) {
...
}
// 값의 의미를 명확히 기록하여 가독성이 향상됨
const PI = 3.14;
if (round > PI) {
...
}
// 재할당하면 타입 에러가 발생
PI = 3.14159; // TypeError: Assignment to constant variable.
3. 객체 내부의 속성이나 배열의 요소는 변경 가능하다.
const
는 객체와 배열에도 사용할 수 있다. 물론 이때도 재할당은 금지된다. 이는 const
로 선언된 변수의 타입이 객체나 배열인 경우, 객체나 배열에 대한 참조를 변경하지 못한다는 것을 의미한다. 하지만 이때 중요한 점, 객체의 프로퍼티나 배열의 요소는 보호되지 않는다. 즉, 재할당은 불가능하지만 할당된 객체나 배열의 내용(프로퍼티, 요소)은 변경할 수 있다.
const person = {
name: "John",
age: 30
};
console.log(person.name); // John
// 객체 내 속성 변경 가능
person.name = "Jane";
console.log(person.name); // Jane
// 객체 전체 재할당은 불가능
// person = { name: "Bob", age: 25 }; // TypeError: Assignment to constant variable.
const numbers = [1, 2, 3, 4];
console.log(numbers); // [1, 2, 3, 4]
// 배열 요소 변경 가능
numbers[0] = 5;
console.log(numbers); // [5, 2, 3, 4]
// 배열 전체 재할당은 불가능
// numbers = [1, 2, 3, 4, 5]; // TypeError: Assignment to constant variable.
객체 person
을 선언하고 초기화했다. const
는 객체 내부 속의 속성을 변경하는 것을 허용하는 특성에 따라 객체 내 속성 name
을 변경했다. 하지만, const
는 객체 전체를 다른 객체로 재할당하는 것은 허용하지 않기 때문에 에러가 발생했다.
배열도 마찬가지다. 배열 number
를 선언하고 초기화한 후에 배열 내 요소를 변경했다. 하지만, 배열 전체를 재할당하면 에러가 발생했다.
이러한 특성은 const
를 사용할 때 객체나 배열의 내부를 자유롭게 변경할 수 있으면서도, 변수가 다른 객체나 배열을 가리키지 못하도록 보장하는 역할을 한다.
정리
- 변수 선언에는 기본적으로
const
를 사용한다. 변경이 발생하지 않는(재할당이 필요 없는 상수) 원시 값과 객체, 배열에는const
를 사용한다. - 재할당이 필요한 경우에 한정해
let
을 사용한다. 이때 반드시 재할당이 필요한지 생각해 볼 필요가 있다. - ES6 이상 버전을 사용한다면
var
는 사용하지 않는 것이 좋다. var
의 함수 스코프 동작은 변수 충돌을 일으키기 때문에 전역 변수를 오염시키는 문제가 있다. 이로 인해 의도하지 않은 값으로 변경될 수 있다.var
가 가지고 있는 특징 중 호이스팅은 할당 이전에 언제나undefined
를 반환하기 때문에 에러를 발생시키지 않지만, 프로그램의 가독성을 떨어뜨리고 어디서 에러가 발생했는지 예측하기 어렵게 만든다.- 이러한 특징들을 고려하여 변수를 선택하면 코드의 가독성과 유지보수성을 향상시킬 수 있다.