JavaScript

var, let, const의 차이: var 사용을 지양해야 하는 이유

GoJay 2024. 10. 7. 18:55

JavaScript의 변수 선언자는 var, let, const 세 가지다. 그중 var는 JavaScript 초기부터 사용된 방식이고, letconst는 2015년 발표된 ES6를 통해 추가된 문법이다. 모든 기술이 그렇지만, 후에 나온 기술은 기존 기술의 한계를 보완하기 위해 등장하는 게 일반적이다. letconst 역시 var가 가진 한계를 극복하기 위해 추가됐고, 그렇다 보니 개발할 때 var가 아닌 letconst 사용이 권장된다.

흔히 varlet, const의 차이로 아래 세 가지가 얘기된다.

  • letconst는 한번 선언된 변수의 재선언이 불가능지만, var는 변수를 재선언 할 수 있다.
  • letconst는 블록 스코프가 적용되지만, var는 함수 스코프가 적용된다.
  • let은 실행 컨텍스트에서 호이스팅 될 시 값이 할당되지 않고, const는 실제 값의 선언과 할당이 같이 이루어지지만, var는 호이스팅 시 undefined가 할당된다.

각각이 실제 개발에서 어떤 의미를 가지는지 좀 더 자세히 살펴보자.

 

var로 선언된 변수는 재선언 될 수 있다.

var로 선언된 변수는 재선언이 가능하다(재선언 시에는 기존에 점유한 메모리 공간의 값을 변경하는 게 아니라 새로운 메모리 공간을 점유해서 값을 저장하고 변수에 참조된 메모리 주소를 새로운 메모리 주소로 변경하는 식으로 처리된다).

var myNum = 10; // 최초 선언 및 할당
console.log(myNum);

var myNum = 20; // 재선언 및 재할당
console.log(myNum);

같은 코드를 let으로 선언해보면 변수를 재선언한 지점에서 에러가 발생되는 게 확인된다. let은 같은 변수명으로 선언된 값이 재선언될 시 새로운 메모리 공간을 확보하지 않고 에러를 발생시킨다.

let myNum = 10; // 최초 선언 및 할당
console.log(myNum);

let myNum = 20; // Uncaught SyntaxError: Identifier 'myNum' has already been declared
console.log(myNum);

let은 재선언이 안되지만, 재할당은 가능하다. 재할당 시 점유하고 있는 메모리 주소는 동일하고, 해당 변수에 저장된 값이 변경된다(참고로, 원시 타입 데이터의 경우 스택 영역에 점유한 메모리 공간에 직접 값을 저장하고, 참조형 데이터는 힙 영역에 데이터를 저장한 후 해당 메모리 주소를 스택 영역에 참조하는 식으로 저장한다. 코어 자바스크립트 1장 내용 참고).

let myNum = 10; // 최초 선언 및 할당
console.log(myNum); // 10

myNum = 20; // 값만 재할당
console.log(myNum) // 20

const는 재선언과 재할당이 모두 불가능하다. let으로 선언 시 재할당에서 문제가 없었던 반면, const는 재할당이 불가하다는 에러 메시지가 확인된다(에러 메시지가 SyntaxError가 아니라 TypeError다. const라는 데이터 타입의 규칙을 지키지 않아서 발생한 에러인 것 같다).

const myNum = 10; // 최초 선언 및 할당
console.log(myNum) // 10

myNum = 20; // Uncaught TypeError: Assignment to constant variable.
console.log(myNum);

const는 재할당이 안된다는 특징 때문에, 반드시 최초 선언 시 할당이 함께 이루어져야 한다(let은 최초 선언 시 할당을 하지 않으면 undefined가 할당된다). 생각해보면 const라는 변수는 이후에 할당된 값을 변경할 수 없는데, 할당을 함께 하지 않는다는 게 논리적으로 말이 안되기 때문에 당연한 결과일 수 있다.

// Uncaught SyntaxError: Missing initializer in const declaration
let myNum1;
console.log(myNum1);

const myNum2;
console.log(myNum2);

 

위의 코드를 실행하면 myNum1에 해당하는 undefined가 먼저 나오고 그 다음 에러가 나는 게 아니라, 실행 시점에 바로 SyntaxError가 나온다. const myNum2;console.log(myNum1);보다 아래에 선언됐지만, 전역의 실행 컨텍스트가 시작할 때 변수가 호이스팅 되기 때문에, 해당 시점에 에러가 난거다. 아래처럼 변수 선언을 한 코드만 단독으로 실행하면 결과로 undefined가 나온다.

let myNum1; // 할당 없이 선언만 함
console.log(myNum1) // undefined

var는 한번 선언된 변수의 재선언이 가능하다는 점 때문에 의도치 않은 동작을 야기한다. 위의 예시들처럼 몇 줄 안되는 코드에서는 같은 변수가 중복되게 선언된 걸 쉽게 확인 가능해서 이슈가 덜 하지만, 수천-수만 줄의 코드로 이루어진 복잡한 프로그램에선 큰 문제를 만들어낼 수 있다.

예를 들어, 앞에서 특정 변수명이 선언돼서 값으로 활용되고 있다는 걸 모르는 채로 뒤에서 변수를 새로 선언하고 값을 할당했다고 해보자. 그러면 전혀 다른 목적을 한 두 변수가 코드 내에서 혼용될 수 있고, 의도치 않은 동작이 만들어질 수 있다. let으로 변수를 선언한다면 재선언 시 에러가 나기 때문에 해당 문제가 빠르게 파악되지만, var는 재선언 시 에러가 발생하지 않기 때문에 문제의 원인을 찾는 게 굉장히 어려울 수 있다. var는 쓸데 없이 친절해서 쉽게 에러를 발생시키지 않기 때문에, 문제를 쉽게 찾아서 해결하고 싶다면 let이나 const를 쓰자.

 

var는 함수 스코프가 적용된다.

var는 함수 스코프가 적용되며, 함수 내의 블록 스코프 영역을 가지는 곳에서 선언된 변수가 값을 공유한다. 아래 예시를 살펴보자.

function myFunc() {
  var i = 10; // 함수 스코프에서의 변수 선언

  for (var i = 1; i < 4; i++) {
    console.log(i) // 1, 2, 3이 차례대로 출력
  };

  console.log(i); // 4
};

myFunc()

for 반복문 내에서 활용된 i라는 변수가 반복문 블록 내에서만 적용되는 게 아닌, 함수 스코프 내에 계속 영향을 주고 있다. 사실 같은 변수명을 쓰는 것 자체가 문제이기 때문에 좋은 예시가 아닐 수 있고, 실제 개발을 할 땐 절대 의도하지 않을 코드이긴 하다. 하지만, 사람 일은 모르기 때문에, var 변수를 쓸 때 충분히 발생할 수 있다는 점을 유념해야 한다.

위의 코드를 let으로 선언하면 반복문 밖에서 실행된 console.log(i)은 함수 스코프에 등록된 i 값인 10을 출력한다.

function myFunc() {
  let i = 10; // 함수 스코프에서 i 변수 선언과 할당

  for (let i = 1; i < 4; i++) {
    console.log(i)
  }; // for 반복문이 끝나면서 블록 스코프 내에 선언된 변수인 i는 Garbage Collecting의 대상이 됨

  console.log(i); // 함수 스코프에서 정의된 i 값인 10이 출력됨
};

myFunc()

블록 스코프 내에서 사용한 값을 의도적으로 블록 스코프 외부에서 사용하기 위해 var를 사용할 수도 있지만, 해당 방식으로 사용할 경우 변수가 선언된 위치와 사용되는 위치에 괴리가 생기기 때문에 이후에 코드를 읽을 때 헷갈릴 수 있다. 가능하면 정해진 블록 내에서 선언된 변수는 해당 블록 내에서만 활용하도록 하고, 그게 아니라 블록 외부(함수 스코프 영역)에 선언된 변수의 값을 다루고 싶은 거라면 블록 스코프 내에선 할당만 새로하는 걸로 하자(아래는 함수 스코프에 선언된 변수 값을 블록 스코프 내에서 다루는 예시).

// 정수 1부터 10까지의 합 계산하기
function myFunc() {
  let myNum = 0; // 함수 스코프 영역에 변수 선언 및 값 할당

  for (i = 1; i <= 10; i++) {
    myNum += i; // 스코프 체이닝을 통해 상위 스코프에 정의된 myNum에 접근해서 값을 변경
  };

  console.log(myNum);
};

myFunc()

 

var는 호이스팅 시 undefined가 자동 할당된다

아래는 var가 호이스팅 될 시 예상치 못한 동작을 하게 되는 대표적인 예시다.

function myFunc() {
  console.log(myNum); // undefined
  var myNum = 10;
}

myFunc();

콘솔에 출력하는 명령보다 변수의 선언이 나중에 돼있기 때문에, 직관적으론 에러가 나야 맞을 것 같다. 하지만, var로 선언된 변수는 호이스팅되면서 초기화와 undefined 할당이 같이 이루어지기 때문에, 실제론 아래와 같은 방식으로 처리되는 셈이다.

function myFunc() {
  var myNum = undefined;
  console.log(myNum);
  myNum = 10;
}

myFunc();

에러가 나는 것으로 기대되는 상황에서 에러가 나지 않기 때문에, 해당 방식으로 돌아가는 코드는 의도치 않은 동작이 나오더라도 어디서 문제가 되는 건지 찾기가 힘들다. 해당 상황에서 let을 사용하면 직관적인 논리의 흐름과 일치되게 동작하도록 만들 수 있다(let은 호이스팅 시 선언만 되고 초기화는 진행되지 않는다).

function myFunc() {
  console.log(myNum); // Uncaught ReferenceError: Cannot access 'myNum' before initialization at myFunc
  let myNum = 10;
}

myFunc();

에러 메시지를 보면 myNum이 초기화되지 않았다는 걸 확인할 수 있다. let으로 선언된 변수는 실행 컨텍스트 내에서 호이스팅 되지만 초기화(값의 할당)는 코드의 실행 순서(런타임)에 맞게 처리된다. 이때, let이 선언만 이뤄졌을 때 자동으로 undefined가 할당되는 것과 달리, 호이스팅에서의 '선언'시엔 '초기화(값 할당)'가 같이 되지 않는다는 점이 특이하다. 호이스팅이 아니라 let myNum; 과 같이 선언이 됐을 땐 선언과 함께 초기화되면서 undefined로 값이 할당되는거고, 호이스팅 됐을 땐 선언만 되고 초기화-할당이 되지 않는다는 걸 잘 기억하자.

constlet과 마찬가지로 호이스팅 될 때 선언만 진행된다. 그래서, 예시와 같이 변수의 사용이 값의 할당보다 먼저 이뤄졌을 때 마찬가지로 '초기화되지 않았다'라는 에러 메시지가 확인된다.

function myFunc() {
  console.log(myNum); // Uncaught ReferenceError: Cannot access 'myNum' before initialization at myFunc
  const myNum = 10;
}

myFunc();

코드의 논리적인 흐름을 봤을 때 변수의 선언과 할당보다 사용이 더 먼저 됐다면 어색한 게 맞고, 이럴 땐 에러가 발생되는 게 자연스럽다. 하지만, var를 사용하면 선언과 할당을 하기 전에 변수가 사용되더라도 값이 undefined로 할당되어있기 때문에 에러는 발생하지 않고, 대신 의도하지 않은 동작이 만들어진다. 이러한 '의도하지 않은 동작'은 많은 경우 개발에서 어려움을 불러일으키기 때문에, 가급적 사전에 해당 상황을 예방하는 게 필요하다.

 

결론

많은 경우에 var를 사용하면 의도하지 않은대로 코드가 동작함에도 불구하고 에러가 발생하지 않는다. 차라리 에러가 나면 어디서 왜 문제가 발생됐는지 알 수 있기 때문에 문제를 해결하기 수월한데, 에러 없이 의도치 않은 동작이 만들어지면 더 슬픈 상황이 벌어진다. 그러니, 가급적 var를 사용하지 말고 letconst를 사용하자. 이게 결론이다.

추가적으로, 여러 내용을 알아가는 도중에 'var는 변수가 선언-할당될 때 전역에서는 전역 객체, 실행 컨텍스트에서는 변수 객체의 속성(프로퍼티)과 값(밸류)으로 들어간다'는 내용을 확인했다. 아래는 브라우저에서 전역에 선언된 var 변수가 전역 객체인 window 객체의 속성과 값으로 등록된 예시다.

크롬 브라우저 개발자 도구 콘솔에서 var로 선언한 변수 A가 window 객체의 프로퍼티로 정의된 걸 확인함

반면, let이랑 const는 전역 상태에서 선언되도 전역 객체의 속성과 값으로 등록되지 않고, 특정 실행 컨텍스트에서도 변수 객체가 아닌, 렉시컬 환경(Lexical Environment)에 있는 환경 레코드(Environment Record)에 등록된다고 한다. 아무래도 이 차이가 varlet, const의 차이를 이해하는 데 중요한 부분이지 않을까 생각이 들었다. 아직 실행 컨텍스트에 대한 개념이 제대로 잡히지 않아서 이해가 부족한데, 이 부분에 대해서도 추가로 공부해보고 포스트를 남겨봐야겠다.

이제 개발을 시작하는 단계여서 오류가 많을 수 있습니다. 혹시 잘못된 내용 있다면 댓글로 알려주시면 감사하겠습니다 🙏