JavaScript - let, const 키워드와 var 키워드

Juhyeong Kim·2022년 1월 6일
1

JS

목록 보기
2/3

var 키워드 / let, const 키워드

ES5(ECMA Script 2009)까지는 var키워드를 사용해서 변수를 선언했지만, 문제가 많아 ES6(ECMA Script 2015)에 letconst가 만들어졌다고 알고 있었다. 그래서 var키워드를 사용하지 않고 letconst를 사용해왔지만, 사실 var키워드가 정확히 어떤 문제가 있고, letconst키워드가 어떻게 동작하는지 정확히 알지 못하고 있었다.

그래서 이번 스터디를 통해 배운 내용을 바탕으로 var키워드가 어떤 문제가 있고, letconst가 어떻게 동작하는지 이야기 해보려고 한다.

var 키워드의 문제점

1. 변수 중복 선언

var키워드로 선언한 변수는 중복으로 선언이 가능하다.

var x = 10;

var x = 1000;

console.log(x); // 1000;

위의 예시처럼, 같은 스코프에서 동일한 변수 이름을 선언해도 오류가 발생하지 않는다. 개발자가 실수로 같은 이름의 변수를 중복 선언을 한다면, 원치않는 사이드 이펙트가 발생할 수도 있다.

그래서 letconst키워드를 사용하면 변수 중복 선언 문제를 해결할 수 있다.

let x = 10;
const y = 20;

let x = 1000;
const y = 2000;

console.log(x); // SyntaxError: Identifier 'x' has already been declared
console.log(y); // SyntaxError: Identifier 'y' has already been declared

위의 예시처럼, letconst키워드를 사용해서 변수를 선언하게 되면, 중복으로 변수를 선언했을 때, SyntaxError문법에러가 발생하게 된다. 이 처럼, 개발자가 실수로 변수 중복 선언을 하게 되면 에러를 통해 확인이 가능하기 때문에 var키워드 대신 let, const키워드를 사용해야 한다.

2. 함수 레벨 스코프와 블록 레벨 스코프

- 함수 레벨 스코프

var키워드로 선언한 변수는 오로지 함수 코드 블록만 지역 스코프로 인정한다.

var name = 'Jay'; // 전역 스코프

if (true) { // 전역 스코프
  var name = 'James';
}

function local() { // 지역 스코프
  var name = 'JS';
  console.log(name); // JS
}

console.log(name) // James

위의 코드를 보면, if 코드 블록 안에서 선언한 변수가 if코드 블록의 지역 스코프로 등록되는 게 아니라, 전역 스코프에 등록이 되어 console.log(name)을 출력했을 때, name식별자의 값이 Jay에서 James로 바뀌게 된다. 이렇게 var키워드는 함수 코드 블록만 지역 스코프로 인정하기 떄문에, 함수 코드 블록을 제외한 나머지 코드 블록들은 전부 전역 스코프에 등록된다.

- 블록 레벨 스코프

let, const키워드로 선언한 변수는 모든 코드 블록을 지역 스코프로 인정한다.

var name = 'Jay'; // 전역 스코프

if (true) { // 지역 스코프
  var name = 'James';
  console.log(name); // James
}

function local() { // 지역 스코프
  var name = 'JS';
  console.log(name); // JS
}

console.log(name) // Jay

위의 코드를 보면, if코드 블록과 함수 코드 블록 모두 지역 스코프로 인정이 되기 때문에, 전역 스코프에 등록된 Jay가 출력된다.

이 처럼, var키워드는 함수 코드 블록만 지역 스코프로 인정하는데 반해, let, const키워드는 모든 코드 블록을 지역 스코프로 인정하기 때문에, 전역 변수를 남발해서 발생할 수 있는 문제점을 어느정도 해결할 수 있다.

3. 변수 호이스팅

console.log(firstName); // undefined
var firstName = 'Jay';

console.log(lastName); // ReferenceError: lastName is not defined
let lastName = 'Kim';

var키워드로 선언하면 호이스팅으로 인해 쓰레드가 선언문에 도달하기 전에 변수를 참조할 수 있지만, 프로그램의 흐름 상 맞지 않고, 가독성도 떨어트려 개발자가 실수 할 여지를 제공한다. let키워드를 사용하면 변수가 선언되기 전에 참조를 했을 때 ReferenceError에러가 발생하므로, 원치않는 사이드 이펙트를 방지할 수 있다.

위의 코드를 보면, var키워드로 선언한 변수는 호이스팅으로 인해 undefined가 출력이 되고, let키워드는 에러가 발생해서 호이스팅이 일어나지 않았다고 생각할 수도 있다. 하지만 호이스팅이 일어나지 않는 것 처럼 보일 뿐, let키워드도 호이스팅이 발생한다.

아래의 예시를 자세히 살펴보자.

let name = 'Jay'; // 전역변수

{ // 코드 블록
  console.log(name); // ReferenceError
  let name = "james"; // 지역변수
}

1번 - 런타임 전에 자바스크립트 엔진에 의해서 전역 스코프에 name식별자가 등록된다.

2번 - let name = 'Jay'를 보면 name식별자에 'Jay'가 바로 할당되는 것 처럼 보이지만, name식별자가 undefined로 초기화가 되고나서 'Jay'를 할당하게 된다.

let name;
name = 'Jay';

즉, 위의 코드처럼 두 개의 코드를 하나로 작성했기 때문에 'Jay'가 바로 할당되는 것 처럼 보이지만, undefined초기화가 일어난 다음에 값이 할당된다는 것을 꼭 기억해야 한다.

3번 - { }코드 블록에 도달하게 되면, 1번에서 런타임 전에 코드를 평가한 것 처럼, 코드 블록 안에 있는 코드를 실행하기 전에 코드 블록 안에 있는 코드를 평가하게 된다. 코드 블록 안에 let name = 'james'라는 변수 선언문이 있기 때문에, 코드 블록의 지역 스코프에 name 식별자가 등록된다. 위에서 설명했던 것 처럼, let키워드는 블록 레벨 스코프이기 때문에 지역 스코프를 가질 수 있다.

4번 - console.log(name)을 보고, 현재 스코프인 지역 스코프에서 먼저 name 식별자가 있는지 확인한다. 3번에서 코드 블록을 평가했을 때 지역 스코프에 name식별자가 등록되었기 때문에 name식별자의 값을 가져오려고 하지만, 아직 undefined초기화가 되기 전이라서 ReferenceError에러가 발생하게 된다.

5번 - 이 때 지역 스코프에 선언되어 있던 name 식별자의 값을 undefined로 초기화 시켜주고, james값을 할당해주게 된다.

{ }코드 블록이 평가되어 지역 스코프에 식별자가 등록(3번)된 이후 부터, 식별자가 변수 선언문을 만나 undefined초기화가 되기 전까지(5번)를 TDZ(Temporal Dead Zone), 일시적 사각지대라고 한다.

만약 let키워드가 호이스팅이 발생하지 않았다면, console.log(name)을 했을 때, 지역 스코프에서는 찾지 못해 전역 스코프에 있는 name식별자의 Jay값을 가져왔겠지만, 쓰레드가 { }코드 블록에 도달했을 때, 코드 블록이 평가되어 지역 스코프에 name식별자가 등록되었기 때문에 호이스팅이 발생했다고 말할 수 있다. 그래서 지역 스코프에 있는 name식별자를 가져오려고 했지만, 아직 초기화가 되기 전인 TDZ(일시적 사각지대)에 있기 때문에 ReferenceError가 발생하게 된다.

지금까지 내용을 바탕으로 정리를 한 번 해보자.

var키워드를 사용하면 런타임 전에 선언 단계(스코프에 식별자 등록)와 초기화 단계(undefined)가 동시에 일어난다. 즉, 호이스팅으로 인해 var키워드는 쓰레드가 선언문에 도달하기 전에 변수를 불러와도 에러가 발생하지 않고 undefined를 가져와 코드의 흐름을 읽기 어렵게 만든다.

하지만 let,const키워드는 선언 단계와 초기화 단계가 분리되어 일어나기 때문에, 쓰레드가 선언문에 도달하기 전에 변수를 불러오면 초기화가 되기 전인 TDZ에 있으므로 ReferfencError에러가 발생해 호이스팅이 발생하지 않는 것 처럼 보이고, 그로 인해 원치않는 사이드 이펙트를 방지할 수 있다. 호이스팅이 발생하지 않는 것 처럼 보이지만, 결국에 호이스팅은 발생한다는 사실을 기억해야 한다.

let 키워드와 const 키워드

let 키워드

위에서 let키워드가 어떻게 동작하는지 살펴봤다.

  • 선언 단계(스코프에 식별자 등록)
  • (TDZ)
  • 초기화 단계(undefined)
  • 할당 단계(값을 식별자에 할당)

위의 순서대로 let키워드를 통해 변수가 할당된다. let키워드로 등록한 변수는 재할당이 가능하다.

let name = 'Jay';
console.log(name); // Jay

name = 'James';
console.log(name); // James

name = 100;
console.log(name); // 100

위의 코드처럼, let키워드로 선언하고 Jay값으로 할당된 변수 namelet키워드로 선언되었기 때문에 다른 값으로 수정이 가능하다. 정확히 말하자면, let키워드로 선언한 name식별자는 다른 값으로 재할당이 가능하다. 재할당이라고 하면 위에 그림에 있는 할당 단계를 계속 반복할 수 있다는 말과 같다.

반면에 const키워드는 재할당이 불가능하다.

const 키워드

let키워드와 다르게 const키워드는 변하지 않는 값, 즉 상수를 선언하기 위해 사용된다. 그렇게 때문에 const키워드로 선언된 변수는 다른 값으로 재할당을 하게 되면 에러가 발생한다. 그리고 반드시 선언과 동시에 값을 할당해줘야 한다.

  1. 반드시 선언과 동시에 할당
const age; // SyntaxError: Missing initializer in const declaration

const age = 100;
  1. 재할당 불가능
const age = 100;

age = 10; // TypeError: Assignment to constant variable.

이 처럼, const키워드를 사용하면 재할당이 불가능하기 때문에 상태 유지와 가독성, 유지보수의 편의를 위해 많이 사용된다.

그럼 객체는 변할 수 있는 값이니까 let키워드를 사용해서 선언하면 될까?

혹시 이런 생각을 하고 있다면, 아직 제대로 이해하지 못한 것이다.(제가 그랬습니다..)

let name = 'Jay';

name = 'James';

const age = 100;
age = 1000; // TypeError:

const person = {
  name: 'Juhyeong',
  age: 100,
};

person.age = 1000;

위의 코드를 통해 letconst키워드에 어떤 차이가 있는지 자세히 알아보자.

그림에서는 런타임 이전에 스코프에 식별자를 등록하는 단계(선언 단계)와 초기화 단계를 생략했다.

1번 - 런타임 전에 let키워드로 등록 된 name식별자를 undefined로 초기화 후 메모리에 있는 'Jay'값을 변수에 할당해줬다.

2번 - name식별자는 let키워드로 선언되었기 때문에 재할당이 가능하다. 그래서 name = 'James'를 보고 다른 메모리 공간에 James값을 변수에 재할당해줬다.

3번 - 런타임 전에 const키워드로 등록 된 age식별자를 undefined로 초기화 후 메모리에 있는 100값을 변수에 할당해줬다. 바로 다음에 age = 1000코드를 보면, age변수는 const키워드로 선언되었기 때문에 값을 재할당하지 못한다. 그래서 그림처럼 재할당할 수 없고 TypeError에러가 발생하게 된다.

4번 - 런타임 전에 const키워드로 등록 된 person식별자를 undefined로 초기화 후, 이번엔 객체이기 때문에 스택 메모리가 아닌 힙 메모리에 객체값을 저장하고, 객체값에 해당하는 힙 메모리 주소가 스택 메모리 값으로 저장된다. 결국 person변수는 힙 메모리 주소를 값으로 가지고 있다.

5번 - const로 선언한 person식별자가 가리키는 힙 메모리 주소를 통해 객체의 age프로퍼티 값을 1000으로 수정한다. 이 때 person변수의 스택 메모리 값이 바뀌는 게 아니기 때문에 오류가 발생하지 않는다.
만약 person = 'animal'처럼 스택 메모리의 값을 바꾸게 된다면, 재할당이 발생하기 때문에 const키워드로 선언된 person변수는 재할당을 할 수 없다. 그러므로 3번처럼 TypeError에러가 발생하게 된다.

다시 질문으로 돌아가 보자.

그럼 객체는 변할 수 있는 값이니까 let키워드를 사용해서 선언하면 될까?

위의 질문에 대한 대답을 하면, 객체라서 let키워드로 등록하는 게 아니라, 변수에 재할당이 필요하다면 let키워드를 사용해서 변수를 선언하는 것이다. 만약 재할당이 필요없는 변수라면 const키워드로 선언하면 된다. 결국에 원시타입이던 객체타입이던 상관없이, 재할당이 필요한 경우에만 let키워드로 선언하면 된다.

그럼 객체는 값이 변할 수 있는데 왜 굳이 const키워드를 사용할까?

왜냐하면 const키워드로 선언하게 되면 객체의 프로퍼티가 추가/수정/삭제될 수는 있어도 재할당을 할 수 없기 때문에, 객체라는 자료형은 변하지 않는다. 그래서 해당 변수가 객체타입에서 다른 타입으로 변할 이유가 없다면 const키워드를 사용해서 변수를 선언하면 된다.

결론적으로, 변수를 선언할 때 const키워드를 주로 사용하고, 재할당이 필요한 경우에만 let키워드를 사용하면 된다. var키워드는 특별한 이유가 있는 게 아닌 이상 사용을 지양해야 한다.

0개의 댓글