Javascript의 변수 선언: var, let, const의 차이점
ES6 등장 이전의 var 변수 선언
ES6 (ECMA Script 6/ECMA Script 2015) 등장 이전, 자바스크립트에서 변수를 선언하려면 var
를 사용해야 했습니다.1 문제는, var
를 사용해 선언한 변수에 적용되는 스코프였습니다.
var 선언의 스코프
예를 들어보겠습니다. function A() { }
가 있고, temp
라는 변수가 해당 함수 외부에 선언되었다고 가정하겠습니다.
var temp = 'temp value';
function A() {
// code
}
위 예제 코드에서 함수 A
외부에 선언된 temp
변수는 전역 스코프입니다. 즉, 해당 변수는 어디서든 접근이 가능하다는 얘기입니다.
그럼 다음과 같은 코드는 어떻게 될까요?
var temp = 'temp value';
function A() {
var insideTemp = 'inside value';
}
console.log(insideTemp);
이 경우에는 temp
변수는 글로벌 스코프로, insideTemp
변수는 A
함수 내부 스코프로 정의되었기 때문에 console.log
로 insideTemp
변수를 출력하려 하면 정의되지 않은 변수를 출력하려 했기에 에러가 나게 됩니다.
불명확한 재정의
일단 스코프는 위와 같다고 기억만 하시고 이 섹션을 읽어보세요. 이 섹션에서 설명하는 내용도 매우 중요합니다.
일단 var
로 정의된 변수는, 언제든지 어떻게든지 어느 스코프에서든지 재정의가 가능합니다. 이게 무슨 소리냐고요? 다음과 같은 행동이 아무런 에러 없이 가능하다는 얘기입니다.
var a = 1;
console.log(a); // > 1
var a = 'abc';
console.log(a); // > abc
a = {}
console.log(a); // > {}
그럼 이게 왜 문제인가? 하는 분도 분명 계실겁니다. 이렇게 짧은 코드에서는 그 문제점이 크게 부각되지 않기 때문입니다. 몸으로 겪어보는 것이 가장 뼈아프게 와닿겠지만, 글로 짧게나마 왜 이게 문제인지 설명하자면 다음과 같은 한 줄로 나타낼 수 있습니다.
도대체 내가 어디서 뭘 잘못 코딩했는지 모르겠다
이게 무슨 소릴까요? 앞서 제가 이렇게 짧은 코드에서는 와닿지 않는다고 설명드렸습니다. 그럼 긴 코드, 수천, 수만, 수십만 줄짜리 코드에서는 어떻게 될까요?
만약 어떤 사이트의 제일 처음 로드되는 main.js
라는 파일에서 현재 접속자의 상태를 나타내는 currentStatus
변수를 글로벌 스코프에 할당했다고 가정해봅시다. 이 변수는 언제나 현재 접속자의 상태를 나타내기 위해서만 사용되어야 합니다. 또한, 사용 가능한 값은 문자열입니다. (connected
, disconnected
, …)
그러나, 어떠한 구조적 문제2로 인해 저 구석 어딘가에 처박혀있는 unknown.js
라는 파일에서 currentStatus = 1
이라고 변경했다고 칩시다. 이제 우리는 프로그램이 알 수 없는 이유로 오동작을 하는 것을 사람이 눈치채기 전까지 무엇이 잘못됐는지 알 수 없습니다. 코드는 아무런 문제 없이 동작하고, 아무런 에러도 나타나지 않으며, 심지어 어디서 잘못됐는지조차 찾기가 힘들기 때문이죠.
이게 뭔 소리야?
이게 무슨 소린가 하는 분도 분명 계실겁니다. 이러한 초보적인 실수를 하는 개발자는 초보밖에 없을 거라고요.
그런데 글쎄요… 과연 그럴까요?
var helloMsg = '안녕하세요!';
var someCount = 10;
if (someCount > 5)
{
var helloMsg = '많이도 방문하셨네요! 돌아오셔서 반갑습니다!';
// 위 helloMsg를 사용하는 작업...
}
console.log(helloMsg);
무슨 결과가 나올 것 같나요? 6번째 줄에서 재정의 불가 에러? 앞서 말씀드렸듯, var
는 어디서든 재정의가 가능하기에 그런 거 없습니다. 당연히 많이도 방문하셨네요! 돌아오셔서 반갑습니다!
문자열이 콘솔에 출력되게 됩니다.
이제 좀 문제점이 보이기 시작하시나요? 만약 저 변수명이 helloMsg
가 아니라 루프 등에서 자주 사용되는 i
, j
, k
, 혹은 count
등이라고 생각해보세요. 이건 초보적인 실수니 어쩌니 하는 레벨의 문제가 아니게 됩니다. 여기에 스코프 문제까지 합쳐진다면? 그야말로 지옥의 코드가 탄생하는 것이죠.
var 선언의 Hoisting (끌어올리기)
var
선언에 적용되는 hoisting을 설명하겠습니다. 이는 작성한 코드가 실제로 실행될 때 어떻게 변하는지를 나타냅니다.
console.log(msg); // => undefined
var msg = 'Hello!';
분명 msg
변수는 console.log
이후에 선언되었는데도 정의되지 않았다는 예외가 발생하지 않고 undefined
가 출력됩니다. 이는 msg
변수 선언이 끌어올려졌기(hoisted) 때문입니다. 위 코드는 실행시 다음과 같이 변환됩니다.
var msg;
console.log(msg);
var msg = 'Hello!';
var
선언은 변수 선언시 아무것도 대입하지 않을 경우3 해당 변수를 undefined
로 초기화하기 때문에, 실질적으로는 1번째 줄에서 var msg = undefined;
로 실행한 것과 같은 결과가 실행되고, 이후 두 번째 줄에서 msg
의 내용인 undefined
가 출력되며, 마지막으로 msg
변수에 'Hello!'
가 할당되는 것입니다.
##ES6의 등장!
위와 같은 문제점을 해결하기 위해, ES6에서 let
, const
선언이 추가되었습니다.
let
이미 let
선언은 이제 var
보다 더 많이 쓰이고 있죠. let
은 블록 스코프에서 정의됩니다. 함수 또는 전역 스코프였던 var
와 달리 블록별로 스코프를 지니게 되는 것이죠.
이걸 위의 끔찍한 코드에 var
만 let
으로 바꿔서 적용해볼까요?
let helloMsg = '안녕하세요!';
let someCount = 10;
if (someCount > 5)
{
let helloMsg = '많이도 방문하셨네요! 돌아오셔서 반갑습니다!';
// 위 helloMsg를 사용하는 작업...
}
console.log(helloMsg);
결과는? Uncaught SyntaxError: Identifier 'helloMsg' has already been declared
예외가 발생하게 됩니다.
언제든 재정의 가능한 var
와 달리, let
은 블록 스코프라는 차이 외에도, 이미 동일하거나 더 상위의 스코프에서 정의된 변수명을 재정의할 수 없기 때문입니다.
그럼 살짝 예제를 비틀어볼까요?
let helloMsg = '안녕하세요!';
let someCount = 10;
if (someCount > 5)
{
let newMsg = '많이도 방문하셨네요! 돌아오셔서 반갑습니다!';
console.log(newMsg);
console.log(helloMsg);
}
console.log(helloMsg);
console.log(newMsg);
결과는 다음과 같습니다.
많이도 방문하셨네요! 돌아오셔서 반갑습니다!
안녕하세요!
안녕하세요!
Uncaught ReferenceError: newMsg is not defined
8번째 줄, console.log(newMsg);
에서는 if (someCount > 5) { }
블럭 안의 스코프에서 정의된 newMsg
변수의 값이 정상적으로 출력됩니다. (많이도 방문...
)
10번째 줄, console.log(helloMsg);
역시 해당 블록보다 상위의 스코프에서 정의된 helloMsg
변수의 값이 정상 출력됩니다. (안녕하세요!
)
13번째 줄 역시 동일한 스코프에서 정의된 변수의 값을 출력하기에 문제가 없죠.
그러나 15번째 줄의 console.log(newMsg);
부분에서는 문제가 생깁니다. let
은 블록 스코프이기 때문에 if 블럭
내부에서 선언된 newMsg
는 15번째 줄이 실행되는 스코프에 없습니다. 때문에 예외가 발생하게 되죠.
let의 재정의 및 업데이트
let
은 위에서 나타났듯 재정의는 불가능하지만, 업데이트는 가능합니다. 즉, 다음과 같은 코드가 허용됩니다.
let a = 0;
a = 'abc';
console.log(a); // > abc
if (true) {
a = 10.5;
}
console.log(a); // > 10.5
또한, 서로 다른 스코프라면 동일한 이름의 변수를 선언하고 독립적으로 사용할 수 있습니다. 이는 let
으로 선언된 변수는 독립된 스코프에서 각각 선언되기 때문입니다.
let scope = 'Global scope';
if (true) {
let scope = 'Block scope';
console.log(scope); // > Block scope
}
console.log(scope); // > Global scope
let 선언의 Hoisting
let
선언 역시 hoisting이 존재합니다. 그러나 let
선언은 선언시 아무것도 대입하지 않으면 변수를 초기화하지 않기 때문에, var
와 달리 다음 코드에서 선언되지 않은 변수를 호출하려 한다는 예외가 발생하게 됩니다.
console.log(msg);
let msg = 'abc';
위 코드의 실행 결과는 Uncaught ReferenceError: Cannot access 'msg' before initialization
입니다. 실행시 코드는 다음과 같이 변경됩니다.
let msg;
console.log(msg);
let msg = 'abc';
다시 말씀드리지만, var
선언은 대입 없이 선언시 해당 변수를 undefined
로 초기화하지만 let
은 초기화하지 않습니다. 때문에 레퍼런스 에러가 발생하는 것이죠.
const
그렇다면 const
선언은 뭘까요? const
선언은 let
선언과 비슷한 면도 있지만 다른 면도 있습니다. 비슷한 면은 몰라도 다른 면은 너무 당연한 얘기죠? 애초에 다른 선언이니까요.
const 선언의 스코프
우선 비슷한 면입니다. const
선언 역시 블록 스코프입니다. let
의 스코프와 동일하다고 생각하시면 됩니다.
const 선언의 재정의 및 업데이트
이제 다른 면이 나옵니다. const
선언은 재정의가 불가능할 뿐 아니라 업데이트조차 불가능합니다. 즉, 해당 스코프에서 한 번 선언된 변수는 그대로 쭉 유지된다는 소립니다.
const msg = 'abc';
msg = 'def'; // > Uncaught TypeError: Assignment to constant variable.
const anotherMsg = 'abc';
const anotherMsg = 'def'; // > Uncaught SyntaxError: Identifier 'anotherMsg' has already been declared
물론, 여타 언어와 같이 오브젝트의 프로퍼티는 변경할 수 있습니다. *변수에 할당된 오브젝트 레퍼런스 변경4은 불가능하지만요.
const obj = { A: 'this is A', B: 1234};
obj.A = 'this is A???';
obj.B = 4567;
console.log(obj); // > { A: 'this is A???', B: 4567 }
obj = { A: 'asdf', B: 4848 }; // > Uncaught TypeError: Assignment to constant variable.
const obj = { A: 1234, B: 4567 }; // > Uncaught SyntaxError: Identifier 'obj' has already been declared
const 선언의 Hoisting
const
선언 역시 Hoisting되지만, let
과 같이 초기화하지 않습니다. 그러나, 수동으로 초기화하지 않을 수는 없습니다.
즉, 다음과 같은 코드는 에러가 발생합니다.
const asdf; // > Uncaught SyntaxError: Missing initializer in const declaration
마치며…
웹 개발 중에 var
, let
, const
선언이 도대체 뭐가 다른지 헷갈려서 찾아본 뒤 작성한 포스트입니다. 부디 저와 같이 헤매던 분께 도움이 되길 바라며, 오늘도 모두 즐코하세요.
댓글
댓글 쓰기