JavaScript :: var, const, let의 차이점

2015년, ES6에서 JavaScript를 보다 더 편리하게 사용할 수 있게 해주는 새로운 기능들이 많이 나왔다.
6년이 지난 2021년 현재, ES6은 이미 널리 사용되고 있어서 많은 JavaScript 개발자들이 ES6의 기법에 익숙해져 있는 상태다.

그 중,

  • "변수를 선언할 때, var를 사용하지 마라"
  • "constlet을 사용하라"

는 것이 정설로 퍼져있는데,
다시 한번 var, const, let의 차이점에 되짚고 넘어가보자 한다.

짧은 결론: 결국 무엇을 사용해야하는가

  • 기본적으로 const를 사용하자🙆🏻‍♀️
  • const를 사용하지 못하는 경우(재할당이 필요한 경우)에는 let을 사용하자.
  • var는 절대 사용하지 말자🙅🏻‍♀️

기초지식: 선언, 초기화, 할당?🤓

선언 (Declaration)

JavaScript에서 변수를 사용하기 위해서는 먼저 선언(Declaration)을 해줘야 한다.
변수 선언(Declaration)이란, 변수를 사용하기 위해 x라는 이름(식별자, Identifier)의 변수를 만들어주는 것.
var, let은 선언만 행할 수 있지만 const의 경우 선언만 행하는 것이 불가능하다.

var x;
let score;
const person; // SyntaxError: Missing initializer in const declaration

초기화 (Initialization)

변수 선언과 동시에, 값을 할당하는 것.
const는 선언과 동시에 초기화가 동시에 이루어져야만 한다.

var x = 0;
let score = 80;
const job = "developer";

할당 (Assignment)

대입, 저장이라고도 표현한다. 변수에 값을 할당하는 것.
또는 이미 값이 저장되어 있는 변수에 다른 값을 할당하는 것.
const는 상수, 변하지 않는 값이기 때문에 할당이 불가능하다.

var x = 0;
x = 1;

let score = 90;
score = 20;

const person = "John";
person = "Jason"; // TypeError: Assignment to constant variable.

1️⃣ 재선언 가능?

var: 재선언 가능🙆🏻‍♀️

var city = "Seoul";
console.log(city); // "Seoul"

var city = "Busan";
console.log(city); // "Busan"

var city = "Incheon";
console.log(city); // "Incheon

var는 기존에 선언된 변수가 있어도 재선언이 가능하기 때문에,
이미 선언해놓은 변수에 또다른 값을 재할당해버리는 예상치 못한 실수를 범하기 쉬우며 재선언에 의한 디버깅을 힘들게 한다.

const, let: 재선언 불가능🙅🏻‍♀️

const sports = "soccer";
const sports = "baseball"; // SyntaxError: Identifier 'sports' has already been declared

let score = 80;
let score = 100; // SyntaxError: Identifier 'score' has already been declared

constlet은 재선언이 불가능하다.
같은 변수명으로 또다시 재선언을 하게 되면 에러를 통해 그 변수는 이미 선언이 되어있음을 알려준다.
constlet은 재선언이 가능하다는 var의 위험을 사전에 차단시켜주고 있다는 것을 알 수 있다.


2️⃣ 재할당 가능?

var, let: 재할당 가능🙆🏻‍♀️

var city = "Seoul";
city = "Busan";
console.log(city); // "Busan"

let score = 50;
score = 100;
console.log(score); // 100

varlet은 기존에 할당된 값과는 또다른 새로운 값을 재할당시키는 것이 가능하다.

const: 재할당 불가능🙅🏻‍♀️

const sports = "soccer";
sports = "baseball"; // Uncaught TypeError: Assignment to constant variable.

constconstant, 상수 그 자체이므로 처음에 const 키워드를 사용하여 변수 선언 및 초기화를 하고 난 뒤에는 새로운 값을 재할당할 수 없다.


3️⃣ Scope?

var: Function scope

함수 내부에서 선언된 변수는 그 함수 내부에서만 참조가 가능하다. 이를 function scope라고 한다.

var는 function scope를 가지기 때문에,
var로 선언된 변수는 오로지 함수의 code block({})만을 scope로 인정한다.

var의 function scope를 증명하는 첫번째 예시를 보자.

function sayHello() {
  var greetings = "Hello";
  console.log(`sayHello says: ${greetings}`); // sayHello says: Hello
}

sayHello();
console.log(`I say: ${greetings}`); // ReferenceError: greetings is not defined

sayHello 함수 내부에서 var로 선언된 변수 greetings는,
함수 내부에서는 정상적으로 참조할 수 있지만 sayHello 함수 외부에서 변수 greetings를 참조하면 에러가 발생한다.

다음 두번째 예시 코드를 보자.

if (true) {
  var fruit = "grape";
}

console.log(fruit); // "grape";

함수가 아닌 영역(위 코드에서는 if)에서 var를 이용해 선언한 변수는 global variable(전역 변수)로 간주한다.
따라서 if문의 외부에서 변수 fruit을 참조한 결과, "grape"가 출력된다.

위에서도 언급했듯이 var함수의 code block({})만을 scope로 인정하기 때문이다.

const, let: Block scope

function scope를 가진 var와 달리, const, let는 block scope를 가진다.

if, switch, while, try/catch문 등은 block{}로 감싸져있는데,
block scope란, 각 block{}으로 감싸진 코드 범위를 말한다.

block scope 내에서 선언된 변수는 해당 block{} 내부에서만 유효하며, block{} 외부에서는 참조할 수 없는 지역 변수가 된다.

바로 예시 코드를 살펴보자.

function test() {
  // if문도 하나의 block{}이다.
  if (true) {
    // block{} 내부에 block scope를 가진 const, let로 변수를 정의
    const x = 1;
    let y = 2;
  }

  // 같은 함수 내에 있더라도, 변수가 정의됐던 block(if문) 외부에서는 해당 변수를 참조할 수 없다.
  console.log(x); // ReferenceError: x is not defined
  console.log(y); // ReferenceError: y is not defined
}

test();

const, let로 변수 x, y를 선언했다.
하지만 const, let은 block scope가 적용되기 때문에
같은 함수 내에 있어도 변수가 정의됐던 block(if문)의 외부에서는 해당 변수를 참조할 수 없다.

또다른 예시를 보자.

if (true) {
  const fruit = "grape";
  let sports = "soccer";
  console.log(fruit); // "grape"
  console.log(sports); // "soccer"
}

console.log(fruit); // ReferenceError: fruit is not defined
console.log(sports); // ReferenceError: sports is not defined

이 예시는 var: Function scope에서 사용했던 예시에서
varconst, let으로 변경해준 것이다.

var는 function scope를 가졌기 때문에 함수가 아닌 block은 무시되어
if문 외부에서도 변수를 참조할 수 있는 것에 반해,

const, let은 block scope를 가졌기 때문에 if문도 하나의 block으로 인정하여
block(if문) 내부에 선언한 변수는 block(if문) 외부에서는 사용할 수 없다는 것을 알 수 있다.


4️⃣ Hoisting?

  • 호이스팅을 변수 및 함수 선언이 물리적으로 작성한 코드의 상단으로 옮겨지는 것으로 가르치지만, 실제로는 그렇지 않습니다. 변수 및 함수 선언은 컴파일 단계에서 메모리에 저장되지만, 코드에서 입력한 위치와 정확히 일치한 곳에 있습니다.
  • JavaScript는 초기화가 아닌 선언만 끌어올립니다(hoist)
  • Hoisting - MDN

Hoisting이란, 단어 그대로 끌어올려지다(hoist)라는 의미를 가졌으며
scope 안의 어디서든 변수, 함수 선언을 하면 JavaScript 내부적으로 선언만을 끌어올려서 처리함으로써,
해당 scope 유효 범위의 최상단에 선언된 것과 동등해지는 현상이 발생되는 것을 말한다.

var: Hoisting이 발생한다.

아래 코드에서는 var를 이용하여 변수 fruit를 선언하기 전에, console.log로 변수 fruit를 참조하려고 하고 있다.
console에는 어떤 결과가 나타날까?

function findFruit() {
  console.log(fruit); // -> ????
  var fruit = "orange";
}
findFruit();

상식적으로 생각하면 변수 선언 전에 변수를 참조하게 됐을 때는 에러가 발생될 것만 같다.
하지만 실제로는 에러가 아니라 결과값으로 undefined가 나온다.

그 이유는, scope의 하단에 있는 fruit라는 변수의 선언 부분이 해당 scope의 최상단으로 hoisting되었기 때문이다.

var로 선언된 변수는 선언과 초기화가 동시에 이루어지면서 undefined를 값으로 가지는데,
var로 선언된 변수가 hoisting 되어 scope의 최상단에서 선언과 초기화가 동시에 이루어짐으로써 console.logfruit를 참조했을 때 undefined라는 결과값이 나타난다.

위의 코드를 풀어서 쓰면 아래와 같다.

function findFruit() {
  var fruit; // 변수 선언이 hoisting되어 scope 최상단에서 먼저 이루어짐
  console.log(fruit); // var는 선언, 초기화와 동시에 값으로 undefined를 가짐
  fruit = "orange"; // 값의 할당은 hoisting되지 않음
}
findFruit();

const, let: Hoisting이 발생하지만, TDZ가 존재한다.

const, let 또한 hoisting이 발생한다.
var처럼 const, let으로 변수를 선언해도 scope의 최상단으로 변수의 선언이 hoisting 되기는 하지만,
선언하기 전에 먼저 constlet으로 선언된 변수를 참조하려고 하면
var와 달리 ReferenceError가 발생된다.

constlet에는 TDZ(Temporal Dead Zone)가 존재하기 때문이다.
TDZ란, 변수의 선언 단계와 초기화 단계의 사이에 존재하는 공간으로, 초기화되지 않은 변수에 access하는 것을 허용하지 않는 공간을 말한다.

이게 무슨 말일까?

console.log(animal); // ReferenceError: Cannot access 'animal' before initialization
const animal = "cat";

2행의 코드인 const animal = "cat"에서 hoisting이 일어났다.
hoisting이 발생됐다는 증거는, Cannot access 'animal' before initialization라는 에러가 발생했다는 점에 있다.
hoisting이 발생되지 않았다면, animal is not defined와 같은 에러가 발생했을 것이다.

먼저 animal이라는 const 변수의 선언은 hoisting에 의해 최상단으로 끌어올려졌다.
하지만, const의 경우 선언만 하는 것이 불가능하고 선언과 동시에 초기화를 행해줘야하기 때문에 ReferenceError: Cannot access 'animal' before initialization라는 에러가 발생한 것이다.
즉, 초기화를 행하지 않은 상태의 const로 선언된 변수가 hoisting 되면서 TDZ에 빠지게 되어 나타난 에러인 것이다.

앞서 var의 hoisting 부분에서 언급했듯, var는 선언과 초기화가 동시에 이루어지면서 undefined를 값으로 가지는 반면,
constlet은 그렇지 않다.

TDZ는 초기화를 행하지 않은 변수의 참조를 허용하지 않는 공간이다.
var와는 달리 constlet은 선언과 초기화가 동시에 이루어지지 않기 때문에, constlet으로 선언된 변수가 hoisting 되어 버리면 TDZ에 빠져버리게 되는 것이다.


5️⃣ window 전역 객체의 property의 여부?

var: window 전역 객체의 property이다🙆🏻‍♀️

함수 외부에서 var로 선언된 변수는 global object(전역 객체. window object)의 property가 되어, global variable(전역 변수)가 된다.
따라서 프로그램 어디서든 access할 수 있게 된다.

var apple = "red";
console.log(window.apple) // "red";

var로 선언된 변수 apple는 전역 객체인 window의 property로 할당된다.

const, let: window 전역 객체의 property가 아니다🙅🏻‍♀️

let apple = "red";
console.log(window.apple) // undefined

var와는 달리, const, let으로 선언된 변수는 window의 property로 할당되지 않았음을 알 수 있다.


차이점 정리✏️

var

  • 재선언 가능🙆🏻‍♀️
  • 재할당 가능🙆🏻‍♀️
  • Function scope
  • Hoisting이 발생한다.
  • window 전역 객체의 property이다🙆🏻‍♀️

let

  • 재선언 불가능🙅🏻‍♀️
  • 재할당 가능🙆🏻‍♀️
  • Block scope
  • Hoisting이 발생하지만, TDZ가 존재한다.
  • window 전역 객체의 property가 아니다🙅🏻‍♀️

const

  • 재선언 불가능🙅🏻‍♀️
  • 재할당 불가능🙅🏻‍♀️
  • Block scope
  • Hoisting이 발생하지만, TDZ가 존재한다.
  • window 전역 객체의 property가 아니다🙅🏻‍♀️

그래서 왜 var를 사용하면 안되는건데?🤷‍♀️

  • 재선언을 가능하게 하기 때문에, 같은 이름의 변수를 여러 번 생성 가능하게 한다.
  • 재선언, 재할당을 가능하게 하기 때문에 의도치 않게 변수와 값을 바꿔버릴 가능성이 크다. → 에러의 추적과 디버깅을 어렵게 함.
  • Function scope이기 때문에, 함수 안이라면 어디서든지 참조가 가능해버리며, 함수 내의 다른 block(if, switch, try/catch 등)에 선언된 값도 참조해버리기 때문에 예상치 못한 값을 도출해버린다.
  • var는 어느 위치에 변수를 선언하더라도 제일 최상단으로 hoisting 되어버린다.
  • ...등등

다시 한번 결론🤓

  • 기본적으로 const를 사용하자🙆🏻‍♀️
  • const를 사용하지 못하는 경우(재할당이 필요한 경우)에는 let을 사용하자.
  • var는 절대 사용하지 말자🙅🏻‍♀️

잘못된 정보가 있다면 마구마구 지적 부탁드립니다🙇‍♀️

좋은 웹페이지 즐겨찾기