Javascript의 변수 선언: var, let, const의 차이점

Welcome file

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.loginsideTemp 변수를 출력하려 하면 정의되지 않은 변수를 출력하려 했기에 에러가 나게 됩니다.

불명확한 재정의

일단 스코프는 위와 같다고 기억만 하시고 이 섹션을 읽어보세요. 이 섹션에서 설명하는 내용도 매우 중요합니다.

일단 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와 달리 블록별로 스코프를 지니게 되는 것이죠.

이걸 위의 끔찍한 코드에 varlet으로 바꿔서 적용해볼까요?

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 블럭 내부에서 선언된 newMsg15번째 줄이 실행되는 스코프에 없습니다. 때문에 예외가 발생하게 되죠.

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 선언이 도대체 뭐가 다른지 헷갈려서 찾아본 뒤 작성한 포스트입니다. 부디 저와 같이 헤매던 분께 도움이 되길 바라며, 오늘도 모두 즐코하세요.


  1. 물론, var 없이 a = 123과 같이 할당할 수도 있지만 이는 논외로 하겠습니다. ↩︎

  2. 팀원간 소통 부재, 혹은 1인 개발 도중 어이쿠 까먹었네 등 ↩︎

  3. var varname; ↩︎

  4. = 대입 ↩︎

댓글

이 블로그의 인기 게시물

C# 남아도는 메모리에도 불구하고 OutOfMemoryException이 발생한다면?

USB를 뒤는 괜찮은데 앞에 꽂으면 인식이 힘들다?

MySQL 데이터 타입과 Java 데이터 타입 비교/매칭