자바스크립트 이론 정복하기 (1) variable
글을 쓴 이유
변수는 자바스크립트를 처음 접할 때 가장 먼저 배우는 개념이지만, 이론적으로 깊이 이해하진 않았다.
변수의 특성을 이해하지 못할 경우 생길 수 있는 치명적인 버그를 피하기 위해 개념들을 정리한다.
글의 목표
- 변수의 선언, 초기화, 재할당에 대해 이해한다.
- var을 사용하지 않는 이유에 대해 이해한다.
- let과 const의 차이에 대해 이해한다.
1. 자바스크립트는 컴퓨터 메모리에 어떻게 변수를 할당하는가
컴퓨터는 기본적으로 메모리와 CPU로 구성된다.
어떤 프로그램을 실행하면, 메모리 공간 내에는 변수가 할당되고 연산작업이 필요할 때 CPU가 작업한다.
예컨대,
var a = 2;
var b = 5;
var c = a * b
//$ c= 10
- 위 코드에서 변수 a,b를 선언하고 각각 2와 5를 할당하는 건 메모리에서 일어나는 일이다.
- 변수 c의 선언도 메모리의 영역이다. 반면, a * b는 CPU에서 작업되어 메모리로 보내져서 변수 c에 할당된다.
메모리 내 공간들은 일종의 방이다. 변수든 객체든 모두 각자의 방(메모리 공간)에 저장된다.
위 코드는 총 몇 개의 방이 필요할까?
흔히 말하길, 원시데이터 타입(문자열, 숫자열 등)은 변수 내에 값 그 자체로 저장되고 객체(array, object, map 등)는 별도의 메모리 공간에 저장되어서 변수는 주소만을 가진다고 한다.
하지만, 이는 오해의 여지가 있다.
변수와 데이터는 모두 각기 다른 방에 저장된다. 예컨대, 위 코드는 총 6개의 방이 필요하다.
(변수 a,b,c, 그리고 원시데이터 타입 2,5,10)
원시 데이터 타입이 있는 방은 말 그대로 해당 데이터를 바이트(byte)형식같은 이진(bynary) 형태로 저장한다. 반면, 변수의 방에는 해당 데이터의 위치를 가리키는 주소가 저장된다.
정리
모든 변수와 데이터는 각자 주소(address)와 값(value)로 메모리 공간에 저장된다.
2. 용어 정의: 선언, 초기화, 재할당
1) 선언
일반적인 프로그래밍 언어에서 선언은 변수가 저장하는 데이터 타입(string, int, float)을 명시하는 거다. 그런데, 자바스크립트는 동적 타입(dynamic typing) 언어라고 해서, 그런 거 없이 사용가능하다. 대신, 필요에 따라 var, const, let 중 하나를 선택해서 변수 타입을 선언해야 한다.
let a;
//$ let 타입의 변수를 선언
2) 초기화
변수를 선언할 때 처음으로 값을 할당하는 것이다. 초기화라는 말 때문에 헷갈리지만, 그냥 '최초 할당'이라 이해해도 무방하다. 대부분 프로그래밍 언어는 변수를 선언하고 초기화해야 사용가능하다.
let a=10;
//$ let 타입의 변수를 선언, 10으로 초기화
자바스크립트에서는 변수를 선언만하고 초기화하지 않을 시, undefined으로 자동 초기화된다.
let a;
//$ 변수 a가 undefined으로 자동 초기화
console.log("a:",a)
//$ 출력( a: undefined )
3) 재할당(할당)
변수에 새 데이터 값을 할당하는 거다. 기존 값을 대체하는 거니 재할당이라 불러야 맞겠지만 할당이라고도 부른다.
let a=10;
//$ 변수 a를 10으로 초기화
a = 5;
//$ a에 5를 재할당
console.log("a:",a)
//$ 출력(a: 5)
4) 메모리와의 관계
변수가 선언되면 메모리 공간 하나에 <변수의 이름>과 <변수가 참조하는 데이터의 주소>가 저장된다. 만약, 초기화를 명시했다면 해당 데이터는 별도의 메모리에 값으로 저장되고 변수 메모리에는 해당 데이터의 주소가 저장된다.
let a=10;
//$ 총 두개의 메모리 공간이 사용된다. 하나에는 10이라는 데이터가 저장된다.
//$ 다른 하나에는 변수 a가 저장된다. a에는 10의 주소가 저장된다.
변수에 다른 데이터를 재할당하면, 변수가 가지고 있는 주소 값이 재할당된 데이터로 바뀐다.
let a=10;
// a는 10 데이터의 메모리 주소를 저장한다.
a= 5;
// 빈 메모리에 5를 저장한다. a는 10의 메모리 주소를 버리고 5의 주소를 저장한다.
재할당의 정확한 원리는 주소값이 바뀐다는 건, 뒤에서 const를 이해하는 데 중요하니 기억하자.
변수를 선언하되 초기화하지 않았다면 undefined을 가진다. 이때 주의할 건 undefined은 메모리에 저장된 값이 아니다. 변수가 데이터 주소도 저장하지 않았을 때 상태를 표시한거다.
let a=;
//$ 총 한개의 메모리 공간이 사용된다. 변수 a가 할당된다.
//$ a에는 다른 데이터의 주소값이 없다. 자바스크립트는 이를 undefined으로 표시한다.
5) undefined과 null
자바스크립트에서 변수에 데이터가 없음을 표시하는 방법은 undefined 외에도 null이 있다.
그러면 null은 메모리에서 어떤 형태일까. 실험해보자.
let a = undefined;
let b = null;
console.log("a:", typeof a);
console.log("b:", typeof b);
//$ 출력(a: undefined)
//$ 출력(b: object)
undefined는 앞서 말했다시피 문자열이나 숫자열 등에 속하지 않는 "데이터가 빈 상태"이다. 그래서 고유의 데이터 타입을 가지고 있다. 그런데, null은 객체라고 표시된다. 그럼 null은 빈 객체의 주소를 변수에 저장한 걸까?
정답은 그렇지않다. 사실은 null도 undefined처럼 아무런 데이터 주소도 저장하지 않은 상태를 표시한거다.
null이 object라 표시되는 건 자바스크립트의 초기 버전부터 발생한 설계오류로, 현재까지도 구 코드의 호환성을 위해 수정되지 않았다. 그러면, 둘 다 변수에 주소값이 없는 빈 상태라는 점인데, 둘의 차이는 뭘까.
이는 개발자의 의도와 관련 있다. undefined은 개발자가 실수로 초기화를 안할 때 발생하지만, null은 개발자가 명시적으로 할당을 해줘야 생긴다. 따라서, 어떤 변수에 null이 할당되어 있으면,
"내가 모르고 비운게 아니라 일부러 그런거야!"
라고 이해하면 된다.
정리
선언 = 변수의 타입을 설정함.
초기화 = 선언할때 변수의 데이터 값을 처음 할당함. 초기화를 안하면 undefined으로 자동 초기화됨.
재할당 = 변수의 데이터 값을 다시 할당함.
변수와 데이터 값은 서로 별개의 메모리 공간에 저장됨. 변수는 데이터 값이 저장된 메모리의 주소를 저장함.
재할당은 기존 데이터의 주소를 버리고 새로운 데이터의 주소를 저장하는 것.
3. var, const, let
자바스크립트에서 변수 타입은 var, const, let 이렇게 세 가지가 존재하는데, 대개는 const와 let을 사용하고 var는 거의 사용하지 않는다. 그 이유는 var가 가진 문제점 때문이다.
1) var의 문제점1: 재선언
재선언이란, 이미 선언된 변수를 다시 선언하는 거다.
var a = 10;
var a = 5;
console.log(a);
//$ 출력( 5 )
var는 재선언을 해도 오류가 발생하지 않는다. 반면, const와 let은 재선언할 시 에러가 발생한다.
let a = 10;
let a = 5;
//$ 에러 발생( "Uncaught SyntaxError: Identifier 'a' has already been declared" )
console.log(a);
// 이 부분은 실행되지 않음
재선언이 가능하다는 건 좋지 않다. 왜냐하면, 변수에 다른 값을 할당할 때는 재할당을 하지, 재선언을 하진 않는다. 재선언을 하는 건 개발자의 실수가 대부분이다. 그래서 오류를 예방하고자 var을 권장하지 않는다.
2) var의 문제점2: 함수 스코프
스코프는 간단히 말하자면, 변수가 유효한 범위이다.
스코프는 전역 스코프, 블록 스코프, 함수 스코프로 구분된다.
var a = 1;
// 변수 a는 전역 스코프에 위치
if (true) {
var b = 2;
// 변수 b는 블록 스코프에 위치
}
function test() {
var c = 3;
// 변수 c는 함수 스코프에 위치
}
console.log(a);
//$ 출력 ( 1 )
console.log(b);
//$ 출력 ( 2 )
console.log(c);
//$ 에러 발생( Uncaught ReferenceError: c is not defined )
- 전역 스코프는 중괄호 안이 아닌 곳에서 선언된 변수나 함수들을 의미한다.
- 블록 스코프는 if처럼 중괄호 안에서 선언된 변수나 함수들을 의미한다.
- 함수 스코프는 블록스코프의 한 종류이면서, 함수의 중괄호 내에서 선언된 변수나 함수들을 의미한다.
스코프의 기본 원칙은 내부 스코프는 외부 스코프를 참조할 수 있는 반면 외부 스코프는 내부 스코프를 참조할 수 없다.
전역 스코프가 가장 외부에 위치한 개념이고 함수 스코프나 블록 스코프는 내부 스코프에 해당한다. 이처럼 스코프 간에 분리가 이뤄지면, 변수가 많아질 시에 실수로 재선언을 하거나 예기치 않은 동작을 예방할 수 있다.
그런데, var는 함수 스코프는 작동하지만 블록스코프는 작동하지 않는다. 위에서 보다시피, var는 함수 스코프에 위치한 변수 c는 외부에서 참조할 수 없지만, 블록 스코프에 위치한 변수 b는 외부에서 참조가능하다.
반면, const와 let은 블록 스코프에 위치한 변수나 함수를 외부에서 참조할 수 없어서 더 안정적이다.
3) var의 문제점3: 호이스팅
호이스팅은 간단히 말하자면, 변수나 함수가 코드의 제일 위로 끌어올려지는 거다. 이는 작성된 코드 자체를 변형시킨다기 보단, 자바스크립트 엔진이 코드를 읽고 실행하는 과정에서 자동으로 이뤄진다.
console.log(a);
//$ 출력 ( undefined )
var a =1
console.log(a);
//$ 출력 ( 1 )
코드는 기본적으로 위에서 아래로 실행된다. 따라서,
"console.log(a)"는 아직 변수 a가 선언되기 전이니 오류가 나야 한다. 하지만, undefined이 멀쩡하게 출력된다. 여기에는 호이스팅의 비밀이 숨겨져 있다. 자바스크립트 엔진은 우선 코드를 실행하기 전에 전체를 훑고, var 변수와 같은 특정 변수와 함수를 제일 위로 끌어 올려 선언시킨다.
즉, 위 코드는 실제로 아래처럼 작동한다.
// $ var a;
// 호이스팅에 의해 var a가 위로 끌어올려져 선언됨.
// 데이터 1은 끌어올려지지 않으므로 undefined
console.log(a);
// 호이스팅된 var a가 출력됨
var a =1
// 호이스팅된 a에 1이 재할당됨. 사실 상 아래와 같음
// a=1;
console.log(a);
//$ 출력 ( 1 )
대부분의 개발자는 변수를 선언하기도 전에 사용하지 않는다. 이런 경우는 대부분 실수로 일어난다. 하지만, 호이스팅은 개발자의 실수를 보이지 않게 만들어 더 큰 오류를 일으킬 수 있다.
4) const와 let의 차이
let은 초기화 없이 선언만 하고 사용가능하다. 또한, 재할당이 가능하다.
반면, const는 선언과 함께 초기화를 반드시 해야 한다. 또한, 재할당이 불가능하다.
let c;
// let 타입의 변수는 초기화 없이 선언만 할 수 있다.
// c = undefined
c = 10;
// c에 10을 재할당한다.
const a = 10;
// const 타입의 변수는 선언과 동시에 초기화를 해야 한다.
a = 5;
// const 타입의 변수에 재할당을 시도하면 에러가 발생한다.
// $ 에러 발생 ( Uncaught TypeError: Assignment to constant variable. )
결론은 둘은 용도에 따라 구분해서 사용하며 개발자의 의도를 표현한다.
만약, 어떤 변수가 const로 선언되어 있다면,
"이 변수의 데이터는 죽어도 바뀌지 않아!"
라는 거고,
let으로 선언되어 있다면, 뒤에 가서 할당된 데이터가 얼마든지 바뀔 수 있다는 의미다.
5) const에 할당된 객체의 변경
const 변수에는 재할당이 안되지만, 할당된 객체의 프로퍼티를 바꾸는 건 가능하다.
const a = [1, 2, 3, 4, 5];
a[3] = "change";
console.log(a);
// $ 출력 ( [1, 2, 3, 'change', 5] )
코딩을 처음 접하는 사람들이 여기서 많이 헷갈린다.
"const는 값을 바꿀 수 없다고 했는데, 왜 되는거지?" 라고 말이다.
그 이유는 const는 엄밀히 말해서 값을 못바꾸는게 아니라 <재할당>이 안되는 것이기 때문이다.
재할당이란, 변수에 저장된 데이터의 주소를 바꾸는 거다.
하지만, 객체의 프로퍼티를 바꾸는 건 데이터의 주소를 바꾸는 것과 관련 없다.
객체가 저장된 메모리의 내부 값만 바뀔 뿐, 변수가 저장한 주소 자체는 변함 없다.
이게 const 변수에서 객체 내부를 변경할 수 있는 이유다.
정리
변수의 타입에는 var, const, let이 있다.
var는 거의 사용되지 않는다. 그 이유는 var가 재선언이 가능하고, 호이스팅이 생기며, 함수 스코프를 가지기 때문이다.
let은 초기화 없이 선언이 가능하고 재할당이 가능하다.
const는 선언과 함께 초기화해야 하고 재할당이 불가능하다.
const에 재할당이 불가능해도 객체 내부의 프로퍼티를 변경하는 건 가능하다.