JavaScript 12분 읽기

var, let, const

지금까지 코드 설명을 var키워드를 이용하여 설명했습니다. 하지만 var를 사용하는 것은 문제를 발생시킬 소지가 있기 때문에 사용이 권장되지 않죠. 그렇다면 지금부터 무엇이 문제이고, 그것을 해결하기 위해선 무엇을 해야하는 지 알아 보도록 합시다.

지금까지 코드 설명을 var키워드를 이용하여 설명했습니다. 하지만 var를 사용하는 것은 문제를 발생시킬 소지가 있기 때문에 사용이 권장되지 않죠. 그렇다면 지금부터 무엇이 문제이고, 그것을 해결하기 위해선 무엇을 해야하는 지 알아 보도록 합시다.

변수의 생명 주기

본격적인 내용을 설명하기 전에 변수의 생명 주기에 대해 알려드리겠습니다. 변수가 생성되어 언제까지 유효한지가 변수의 생명 주기입니다. 변수는 생성된 후 프로그램이 끝나기 전에 소멸되기도 합니다.

전역 변수

전역 변수의 생명주기는 애플리케이션의 생명 주기와 같습니다. 이를 전역 객체의 생명 주기와 일치한다고 하는데요. 전역 객체는 또 뭔데 객체냐구요?

전역 객체는 코드가 실행되기도 전에 어떤 객체들 보다 먼저 생성되는 특별한 객체입니다. 브라우저의 경우 window가 전역 객체에 해당되죠. 크롬 브라우저의 콘솔창을 켜서 window를 입력하면 실제로 객체에 대한 정보가 나올 것입니다. 그것이 바로 전역 객체입니다.

window 객체

var로 전역 변수를 선언하게 되면 이 window의 객체의 프로퍼티가 됩니다. 전역 객체의 프로퍼티는 window.으로 프로퍼티 접근을 하지 않아도 됩니다. 반대로 프로퍼티 접근 방법으로 변수의 값을 얻을 수도 있죠.

var x = 1;
window.x; // 1

넘어가서 생명 주기에 대해 다시 말해보자면, 전역 변수는 이 전역 객체가 없어질 때까지 즉, 브라우저가 닫힐 때까지 유효하다는 것입니다.

지역 변수

지역 변수는 함수가 호출되면 생성되고 함수가 소멸하면 같이 소멸합니다.

function foo(){
  var x = 'local';
  console.log(x);
}
foo(); // 'local'
console.log(x); // ;ReferenceError: x is not defined

지역 변수는 함수가 호출되는 순간 함수를 평가하면서 생성됩니다. 함수를 호출하지 않으면 함수 내부의 변수 선언문이 실행되지 않기 때문이죠. 또 함수가 종료되면 소멸합니다.

이처럼 지역 변수의 생명주기는 함수의 생명 주기와 같다고 볼 수 있죠. 항상 같다고는 할 수 없습니다. 지역 변수가 함수보다 오래 살아있는 경우도 있을 수 있거든요. 이에 대해서는 클로저 포스트에 설명하겠습니다.

그렇다면 변수 호이스팅은 어떻게 되는 것일까요? 분명 런타임 이전에 변수 선언문은 상위로 끌어올려지는 것처럼 된다고 했는데, 이는 엄밀히 말해 전역 변수만 해당됩니다. 함수의 경우는 호출되는 순간 전역에서 했던 것처럼 함수 몸체 코드를 한 줄씩 실행하기 하기 전에 지역 변수를 호이스팅하게 됩니다. 호이스팅은 스코프 단위로 되는 것입니다.

만약 함수 선언문 내의 변수도 전체 런타임 전에 호이스팅이 된다고 했다면, 함수를 호출하지 않을 때 즉, 함수를 사용하지 않으면 그 변수가 차지하는 공간은 낭비됩니다. 또한, 함수 내부의 변수는 상위 스코프에서 사용할 수 없기 때문에, 함수를 호출하지 않는 한 그 변수에 접근할 이유가 없어 미리 공간을 만들어 둘 필요도 없는 것이죠.

전역 변수의 문제점

1. 암묵적 결합

전역 변수는 어디서든 참조가 가능합니다. 모든 코드가 전역 변수를 참조하고 변경할 수 있는 암묵적 결합을 허용하는 것이죠. 코드가 길어지게 되면, 전역 변수는 가독성을 해칠 수 있고, 의도치 않게 값이 변할 수 있다는 것을 기억하세요.

2. 생명 주기

전역 변수는 전역 객체의 생명 주기와 같다고 했습니다. 그만큼 차지하는 공간도 오래 차지하는 것이죠. 또한 오래 살아있는만큼 의도치 않은 변경의 위험이 높습니다.

심지어 var의 경우는 중복 선언을 허용하기 때문에 이전에 선언한 변수를 깜빡하고 다른 값으로 다시 선언하는 경우도 발생할 수 있겠네요. 이것이 var키워드의 문제점 중 하나이기도 합니다.

3. 스코프 체인 상 종점

스코프 체인은 스코프 포스트에서 설명드렸습니다. 식별자를 찾을 때 스코프 체인을 이용해서 찾게 되는데, 만약 전역 변수로 선언된 식별자를 찾게 된다면, 가장 마지막에 있는 스코프에서 찾는 것이니 속도 차이가 있겠죠. 물론 그 차이가 크지는 않습니다만 분명 차이는 존재한다는 것은 알아두죠.

4. 네임스페이스 오염

자바스크립트는 하나의 전역 객체를 공유합니다. 파일이 분리되어 있어도 공유하는 것이죠. 즉 다른 파일에서 동일한 전역 변수가 있다면, 의도하지 않은 결과를 가져올 수 있겠네요.

전역 변수의 억제

변수의 스코프는 좁을수록 좋습니다. 굳이 전역 변수로 선언할 이유가 없다면 지역 변수를 사용하는 것이 좋겠죠.

1. 즉시 실행 함수

함수 포스트에서 설명드렸던 것처럼 즉시 실행 함수를 이용하면 전역 변수를 생성하지 않습니다.

(function (){
  var foo = 10;
})();

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

2. 모듈 패턴

모듈 패턴은 클래스를 모방해서 즉시 실행 함수를 이요해 모듈을 만드는 것입니다. 정보 은닉의 기능을 구현 한정적으로 구현할 수 있게 되죠.

var Counter = (function (){
  // private 변수 : 외부에서 접근할 수 없다. 변경하기 위해서는 함수가 반환하는 객체를 이용하는 방법뿐입니다.
  var num = 0;
  
  // 외부로 공개할 데이터나 메서드를 프로퍼티로 추가한 객체를 반환
  return {
    increase: function(){
      return num++; // num을 먼저 반환하고 증가합니다.
    },
    decrease: function(){
      return num--;
    }
  }
})();

위의 코드를 이해하기 위해서는 클로저를 알고 있어야 합니다.

3. ES6 모듈

ES6모듈을 사용하면 전역 변수를 사용할 수 없습니다. 대신 파일 자체적으로 모듈 스코프를 제공합니다. 모듈 내에서 var키워드로 선언한 변수는 더는 window의 프로퍼티가 아니게 되죠. 모듈 파일을 이용할 때는 .mjs확장자를 권장합니다. <script>태그에 type어트리뷰트를 module로 추가하면 됩니다.

<script type='module' src='lib.mjs'></script>
<script type='module' src='app.mjs'></script>

var, let, const

var의 문제

var키워드를 사용했을 때 문제점은 무엇일까요?

  • 변수의 중복 선언 허용
  • 함수 레벨 스코프
  • 변수 호이스팅

이 되겠네요. 변수를 중복 선언하여 의도치 않은 값의 변경이 있을 수 있고, 함수 레벨 스코프를 지원하기 때문에 함수가 아닌 코드블록의 변수들이 상위 스코프에 등록되기도 하겠죠. 또한, 변수 호이스팅이 이루어져 undefined로 초기화 되어 프로그램 흐름상 맞지 않고, 가독성을 떨어뜨릴 수 있죠.

let 키워드

var와 다르게 let키워드는 어떨까요?

  • 변수의 중복 선언을 허용하지 않음
  • 블록 레벨 스코프
  • 변수 호이스팅이 발생하지 않는 것처럼 동작
  • 전역 객체의 프로퍼티로 등록되지 않음

let키워드를 사용해서 변수를 중복 선언하면 에러가 발생합니다. 이미 선언이 되었다고 오류 문구를 보여주죠.

let foo = 1;
let foo = 2; // SyntaxError: Identifier 'foo' has already been declared

let키워드는 블록 레벨 스코프입니다. 함수가 아닌 코드 블록을 스코프로 인정하는 것이죠.

let foo = 1;
{
  let foo = 2;
  let bar = 3;
}
console.log(foo); // 1
console.log(bar); // ReferenceError: bar is not defined

블록문 안에 선언한 변수는 상위 스코프에서 접근할 수 없게 됩니다.

호이스팅을 볼까요? var키워드의 경우, 선언과 초기화단계가 런타임 이전에 이루어집니다. 여기서 초기화는 undefined로 할당하는 것을 말하죠.

console.log(foo); // undefined
var foo = 1;

하지만 let의 경우, 선언 단계와 초기화 단계가 따로 이루어집니다. 호이스팅이 되어 선언 단계는 실행되지만, 초기화는 하지 않고, 변수 선언문에 도달했을 때 실행하게 되는 것입니다. 즉, 스코프 시작 지점부터 선언문까지 let변수로 선언한 변수는 참조할 수 없습니다.

console.log(foo); // ReferenceError: foo is not defined
let foo = 1;

변수를 참조할 수 없는 구간을 일시적 사각 지대, TDZ(Temporal Dead Zone)라고 합니다.

전역 객체와는 무슨 상관이 있을까요? 전역 스코프에서 선언한 변수는 모두 전역 변수라고 합니다. var로 선언한 경우 window객체의 프로퍼티가 됩니다. 하지만 let의 경우 전역 변수임에도 전역 객체 window의 프로퍼티로 등록되지 않습니다. 대신, 보이지 않는 개념적인 블록에 존재하게 됩니다.

var vx = 'var';
let lx = 'let';
function foo(){
  console.log(vx); // 'var'
  console.log(lx); // 'let'
}
foo(); 
console.log(window.vx); // 'var'
console.log(window.lx); // undefined
console.log(lx); // 'let'

전역 변수이기 때문에 하위 스코프인 foo함수에서 접근할 수 있지만, vx의 경우 window객체의 프로퍼티로 등록이 되어 있지만, lx의 경우는 등록되지 않아 프로퍼티 접근 방식으로 값을 가지고 오지 못하죠. 하지만 변수를 사용하듯이 참조하면 값을 가져옵니다.

const

const는 어떤 특징을 가지고 있을까요? let키워드와는 대부분 동일하지만 다른 점이 있습니다.

  • 선언과 동시에 초기화
  • 재할당 금지
  • 상수

const로 선언한 변수는 반드시 초기화를 같이 해주어야 합니다.

const foo = 1; // 1

const bar; // SyntaxError: Missing initializer in const declaration

밑의 오류를 보면, const 선언에서 초기화가 없다고 설명이 나와있습니다. 첫 줄의 코드처럼 반드시 초기화를 같이 해주어야 합니다.

단, const변수로 선언했을 때 값을 재할당할 수 없다는 것을 명심하세요.

const foo = 1;
foo = 2; // TypeError: Assignment to constant variable.

오류를 보면 상수에 할당하고 있다는 오류네요. 재할당이 안됩니다.

상수는 무엇일까요? 상수는 재할당이 금지된 변수를 말합니다. const로 선언한 변수가 해당되는 것이죠. 단, 재할당이 불가한 것이지 값의 변경이 불가한 것은 아닙니다. 즉, 객체의 프로퍼티 변경은 가능한 것이죠.

const person ={
  name: 'kim'
}

person.name = 'lee';
console.log(person); // { name: 'lee' }

위 코드를 보면 const키워드를 사용해서 객체를 선언했지만, 변경이 가능하다는 것을 알 수 있죠.