자바스크립트의 변수를 간단히 알아봅니다.

변수(variable)

컴퓨터는 메모리를 통해 데이터를 기억하고, CPU를 통해 연산합니다.

메모리는 데이터를 저장할 수 있는 memory cell의 집합체입니다. cell 하나당 1 byte(= 8 bit)이며 1 byte 단위로 데이터를 저장하거나 읽습니다. 각 cell은 고유의 memory address를 가집니다.

컴퓨터가 10 + 20이라는 연산의 결과값 30을 메모리에 저장했다고 하면, 이 30이라는 값에 접근할 수 있는 방법은 30이 저장된 cell의 주소값을 통해 직접 접근하는 것입니다. 하지만, 주소를 통해 직접 값에 접근하는 것은 매우 위험한 일입니다. 따라서 JS는 개발자의 직접적인 메모리 제어를 허용하지 않습니다.

그렇기 때문에 변수(variable)이라는 개념이 필요합니다. 변수는 하나의 값을 저장하기 위해 확보한 메모리 공간 그 자체 혹은 그 메모리 공간을 식별하기 위해 붙이는 이름을 말합니다. 즉, 값의 위치를 가리키는 상징적인 이름입니다. 인터프리터가 알아서 메모리 공간의 주소로 치환해 실행합니다.

식별자(identifier)

변수 이름을 식별자라고도 부르는데, 어떤 값을 구별해 식별할 수 있는 고유한 이름을 말합니다.

var result = 10 + 20;
var result = 10 + 20;

여기에서 식별자 result에는 값 30이 저장돼 있는 메모리 주소를 기억합니다. 이를 값이 저장돼 있는 메모리 주소와 매핑 관계를 맺는다고 말하며, 이 매핑 정보 또한 메모리에 저장됩니다.

변수 이름 뿐만 아니라 함수, 클래스 등의 이름도 식별자입니다.

변수의 선언(variable declaration)

변수 선언은 값을 저장하기 위한 메모리 공간을 확보(allocate)하고 변수 이름과 확보된 메모리 공간의 주소를 연결(name binding)해 값을 저장할 수 있게 준비하는 것을 말합니다. 이 때 확보된 공간은 해제(release)되기 전까지는 누구도 사용할 수 없도록 보호됩니다.

JS에서 변수를 선언할 때 사용할 수 있는 키워드는 var, let, const 등이 있습니다.

추후 정리할 여러 이유들로 인해 최근에는 var를 거의 쓰지 않지만, 종종 옛날에 작성된 코드들에서 사용됩니다. ES6에서 let, const 키워드가 추가됐다는 점을 알아둡시다.

var shiwoo;
var shiwoo;

변수를 선언만 하고 값을 할당하지 않으면 JS 엔진은 undefined라는 값을 암묵적으로 할당해 초기화하기 때문에 쓰레기 값이 확보된 메모리 공간에 있을 염려가 없습니다.

undefined는 primitive value중 하나입니다.

선언문의 실행 시점과 호이스팅(hoisting)

console.log(name);
var name = "shiwoo";
console.log(name);
var name = "shiwoo";

변수 선언문보다 변수를 참조하는 코드가 더 앞에 있다면 어떻게 될까요? JS 코드는 인터프리터 언어기 때문에 한 줄씩 순차적으로 실행되므로 위 코드에서는 undefined가 출력될 것입니다.

여기서 중요한 점은 참조 에러(ReferenceError)가 발생하지 않는다는 것입니다. JS는 소스코드를 순차적으로 실행하기 전에 코드를 평가하며 실행하기 위한 준비를 합니다. 이 때, 모든 선언문을 코드에서 찾아 먼저 실행합니다. 그리고 평가 과정이 끝나면 선언문을 제외한 코드를 한 줄씩 실행합니다.

즉, 선언문이 runtime이 아닌 그 이전 단계인 평가 단계에서 먼저 실행되기 때문에 ReferenceError가 발생하지 않습니다. 따라서 선언문이 어디에 있든 있기만 하다면 언제든지 변수, 함수, 클래스 등을 참조할 수 있습니다. 이런 특징을 호이스팅이라고 부릅니다.

값의 할당(assignment)

변수에 값을 할당할 때는 대입 연산자 =를 사용합니다.

변수의 선언과 할당은 한 문으로 축약해 표현할 수 있지만, 주의할 점은 실제 실행될때는 변수의 선언과 할당 시점이 다르다는 것입니다.

console.log(name) // undefined
var name = "박시우"
console.log(name) // 박시우
console.log(name) // undefined
var name = "박시우"
console.log(name) // 박시우

runtime 전에 name이 선언되므로 코드는 ReferenceError 없이 잘 실행됩니다.

그렇다면 여기에서 두 console.log는 각각 어떤 내용을 출력할까요?

console.log(name)
name = "박시우"
var name
console.log(name)
console.log(name)
name = "박시우"
var name
console.log(name)

이는 아래 순서로 실행된다고 생각하면 쉽게 예측할 수 있습니다.

var name
console.log(name) // undefined
name = "박시우"
console.log(name) // 박시우
var name
console.log(name) // undefined
name = "박시우"
console.log(name) // 박시우

값의 재할당(reassignment)

let이나 var 키워드로 선언된 변수의 값은 재할당 될 수 있습니다.

var x = 10
x = 20
var x = 10
x = 20

위 코드가 실행되면, x 값은 undefined가 할당되고, 다음으로 10이 먼저 재할당됐다가 20이 재할당됩니다. 이 때 재할당 되는 값은 먼저 할당된 값이 저장돼 있던 메모리 공간에 덮어쓰는 것이 아니라, 새로운 메모리 공간을 확보하고 거기에 새로 할당된 값을 저장합니다. 매핑 정보가 사라진 undefined10은 가비지 콜렉터에 의해 자동으로 해제됩니다. (언제 해제될지는 예측할 수 없습니다.)

garbage collector가 있는 언어를 managed language라고 부릅니다. C언어는 대표적인 unmanaged language이며 malloc(), free()같은 저수준 메모리 제어 기능을 제공합니다. 덕분에 개발자의 역량에 따라 최적의 성능을 확보할 수 있지만 치명적 오류를 일으킬 가능성도 늘어납니다. 반면 managed language는 알아서 메모리를 관리해주니 생산성은 높지만 성능 면에서 손실을 감수할 수밖에 없습니다.

식별자 naming convention

식별자를 naming할때는 주석을 달지 않고도 변수가 무엇인지 설명할 수 있는지를 잘 생각하며 정하면 좋습니다. 자주 쓰이는 컨벤션은 아래의 네가지가 있습니다.

// camelCase
var myName

// snake_case
var my_name

// PascalCase
var MyName

// typeHungarianCase
var strMyName // 타입과 식별자를 함께 사용
var $element = document.getElementById("element") // DOM 노드 앞에 $
var observable$ = fromEvent(document, "click") // RxJS Observable
// camelCase
var myName

// snake_case
var my_name

// PascalCase
var MyName

// typeHungarianCase
var strMyName // 타입과 식별자를 함께 사용
var $element = document.getElementById("element") // DOM 노드 앞에 $
var observable$ = fromEvent(document, "click") // RxJS Observable

일반적으로 변수나 함수의 이름에는 camelCase를, 생성자 함수나 클래스 이름에는 PascalCase를 사용합니다.