데이터 타입의 종류
자바스크립트의 데이터 타입은 기본형 타입과 참조형 타입으로 나뉜다.
할당이나 연산 시에 기본형은 값이 담긴 주소값을 복제하고, 참조형은 값이 담긴 주소값들로 이루어진 묶음을 가리키는 주소값을 복제한다.
자바스크립트가 데이터를 처리하는 과정
메모리와 데이터
비트 Bit : 0 또는 1만 표현할 수 있는 하나의 메모리 조각
메모리는 매우 많은 비트들로 구성되어있는데 각 비트는 고유한 식별자를 통해 위치를 확인할 수 있다. 비트는 0과 1만 표현하기에 비트 단위로 위치를 확인하는 것은 비효율적이다. 이보다 비트를 몇 개씩 묶어 하나의 단위로 표현한다면 표현할 수 있는 값도 늘어나고 검색 시간을 줄일 수 있다. 반대로 비트를 너무 많이 한 단위로 묶으면 낭비되는 비트가 생기기도 한다. 적절하게 묶는 것이 중요하다고 생각했는데 이래서 등장한 것이 바이트(Byte= 8Bit) 이다. 모든 데이터는 바이트 단위의 식별자. 정확하게는 메모리 주소값을 통해 서로 구분하고 연결할 수 있다.
자바스크립트는 숫자형 데이터는 8바이트의 공간을 확보하고, 문자열은 특별히 정해진 규격이 없다.
식별자와 변수
프로그래밍 에서의 변수: 변할 수 있는 데이터
식별자: 이 변수를 식별하는 데 사용되는 이름. 즉 변수명이다.
변수 선언시 동작원리
자바스크립트에서 변수를 선언하면 어떤 동작이 일어날까?
var a;
변수를 선언하고 컴퓨터는 메모리에서 비어있는 공간 하나를 확보한다. 위 코드는 이 공간의 이름(식별자)을 a라고 지정한다는 뜻이다. 사용자가 a에 접근하고자 하면 컴퓨터는 메모리에서 a라는 이름을 가진 주소를 검색해 해당 공간에 담긴 데이터를 반환한다.
주소 | 1004 |
데이터 | 이름 : a 값 : |
아직 값을 할당하지 않았기 때문에 값 영역은 비어있다.
데이터 할당
데이터를 할당하는 과정은 어떻게 일어날까?
var a;
a = 'abc'
변수 선언할 때 저장되었던 데이터의 값에 'abc'이 바로 할당될 것 같지만 실제로는 그렇지 않다. 데이터를 저장하기 위한 별도의 메모리 공간을 다시 확보해서 값을 저장하고, 그 주소를 가리키는 방식으로 저장된다.
주소 | 1003 | 2003 |
데이터 | 이름: a 값 : 2003 |
'abc' |
그렇다면 왜 값을 직접 대입하지 않고 주소를 가리키는 방식을 사용하는 걸까? 이는 데이터 변환을 자유롭게 할 수 있게 함과 동시에 메모리를 더욱 효율적으로 관리하기 위해서이다.
영어는 1글자마다 1바이트, 한글은 2바이트로 각각 필요한 메모리 용량이 가변적이며 전체 글자 수 역시 가변적이다. 만약 데이터를 변환한 후 다시 저장한다면 변환된 데이터 크기에 맞게 확보한 공간을 늘려야 하는 상황이 생길 수 있다. 그래서 변수와 데이터의 메모리 공간을 따로 분리해 놓는 것이다. 이러면 확보한 공간을 더 늘리는 작업 없이 저장되어 있는 데이터의 주소만 가리키면 되기에 작업이 수월하고 재활용 할 수도 있다.
또 문자열은 어떤 변환을 가하든 무조건 새로운 값을 만든다. 만약 'abc'의 마지막에 'def'를 추가하려고 하면 컴퓨터는 저장된 'abc'를 변경하는 것이 아닌 새로운 'abcdef' 를 새로 만들어서 저장하고 가리키는 주소를 바꾼다. 여기서 데이터는 자신의 주소를 저장하는 변수가 하나도 없게 되면(아무도 자신을 가리키지 않을 때) 가비지 컬렉터에 의해 제거되기 때문에 새로 만드는 이런 방식에 대한 걱정은 없어도 된다.
주소 | 1003 |
데이터 | 이름 : a 값 : 2003 -> 2004 |
주소 | 2003 | 2004 |
데이터 | 'abc' // 가비지 컬렉터의 제거 대상 | 'abcdef' |
불변값과 가변값
불변값
기본형 데이터 타입은 모두 불변값이다. 여기서 불변값이라고 하면 값을 바꿀 수 없는 상수라고 오해할 수 있다. 상수와 불변은 다른 개념이다. 변수와 상수에 대한 개념은 저장한 변수가 가리키는 주소를 바꿀 수 있는지(재할당을 할 수 있는지)의 개념이고, 불변은 데이터가 바뀔 수 있느냐의 개념이다. 위에서 문자열 'abc'에 'def'를 추가하는 예시를 들었는데, 이것이 바로 불변의 개념이다. 데이터가 바뀌는 것이 아닌 새로 만들어진 것이기 때문이다.
가변값
참조형 데이터는 가변값일까? 기본적인 성질은 가변값인 경우가 많지만 불변값으로 활용하는 방안도 있다. 지금까지 기본형 데이터 타입을 변수로 할당하는 과정을 정리했는데 참조형 데이터 타입을 변수에 할당하는 과정부터 알아보자.
var obj1 = {
a: 1,
b: 'bbb'
}
주소 | 1001 | 1002 | 1003 | 1004 |
데이터 | 이름 : obj1 값: 5001 |
주소 | 5001 | 5002 | 5003 | 5004 |
데이터 | 7001 ~ 7002 | 1 | 'bbb' |
주소 | 7001 | 7002 | 7003 | 7004 |
데이터 | 이름: a 값: 5003 |
이름: b 값: 5004 |
기본형 데이터와는 다르게 객체 프로퍼티를 변수로 저장하는 영역이 존재한다. 프로퍼티 영역에 다른 값을 얼마든지 대입할 수 있기 때문에 흔히 참조형 데이터는 가변값이라고 한다.
예를들어 참조형 데이터 타입의 프로퍼티를 재할당 해보자.
obj1.a = 2;
계속 가리키는 곳을 따라서 데이터들의 공간에서 2를 찾게된다. 하지만 2가 없기에 새로운 2를 만들고 a 변수(프로퍼티 공간의)가 가리키는 주소를 바꾼다. 이 때, 변수 obj1이 가리키는 주소(5001)는 변하지 않는다.
객체 안 객체가 있을 때도 위와 같은 과정을 한번 더 거치는 것이다. 객체 안 객체도 참조카운트(참조하는 변수의 개수)가 0이 되면 가비지 컬렉터의 대상이 될 수 있으며, 그 내부 요소들 또한 연쇄적으로 제거된다.
변수 복사 비교
기본형 타입과 참조형 타입의 복사를 비교해보자.
var a = 10;
var b = a;
var obj1 = { c:10, d: 'ddd'};
var obj2 = obj1;
기본형 타입과 참조형 타입을 선언 및 할당하면 아래 테이블과 같은 모습이다.
주소 | 1001 | 1002 | 1003 | 1004 |
데이터 | 이름 : a 값 : 5001 |
이름 : b 값 : 5001 |
이름 : obj1 값 : 5002 |
이름 : obj2 값 : 5002 |
주소 | 5001 | 5002 | 5003 | 5004 |
데이터 | 10 | 7103 ~ 7104 | 'ddd' | |
주소 | 7103 | 7104 | 7105 | 7106 |
데이터 | 이름 : c 값 : 5001 |
이름 : d 값 : 5003 |
변수 복사 이후 값을 변경했을 때 각 타입의 동작을 따라가보자.
b = 15;
obj2.c = 20;
먼저 기본형 타입 b 를 15로 변경 하게되면 a변수와 b변수가 가리키는 주소 (값) 이 달라진다.
주소 | 1001 | 1002 | 1003 | 1004 |
데이터 | 이름 : a 값 : 5001 |
이름 : b 값 : 5004 |
이름 : obj1 값 : 5002 |
이름 : obj2 값 : 5002 |
주소 | 5001 | 5002 | 5003 | 5004 |
데이터 | 10 | 7103 ~ 7104 | 'ddd' | 15 |
주소 | 7103 | 7104 | 7105 | 7106 |
데이터 | 이름 : c 값 : 5001 |
이름 : d 값 : 5003 |
참조형 타입 obj2의 프로퍼티를 변경해보자.
주소 | 1001 | 1002 | 1003 | 1004 | 1005 |
데이터 | 이름 : a 값 : 5001 |
이름 : b 값 : 5004 |
이름 : obj1 값 : 5002 |
이름 : obj2 값 : 5002 |
|
주소 | 5001 | 5002 | 5003 | 5004 | 5005 |
데이터 | 10 | 7103 ~ 7104 | 'ddd' | 15 | 20 |
주소 | 7103 | 7104 | 7105 | 7106 | |
데이터 | 이름 : c 값 : 5005 |
이름 : d 값 : 5003 |
obj1과 obj2의 실제 값은 달라지지 않고, 같은 객체를 바라보고 있다. 이러한 동작 때문에 복사 이후에 프로퍼티를 변경하면 두 객체 모두 값이 바뀌는 것이다.
아래는 두 변수를 코드로 비교해서 콘솔에 찍어본 모습이다. (두 변수를 비교할 때 값이 같은지 아닌지가 비교를 결정한다)
a === b // false
obj1 === obj2 // true
이번에는 객체 자체를 변경해보자.
var a = 10;
var b = a;
var obj1 = {c:10, d: 'ddd'};
var obj2 = obj1;
b= 15;
obj2 = {c:20, d: 'ddd'}
내부 프로퍼티를 바꾸는 것이 아닌 새로운 객체를 만들고, obj2의 값을 바꾸는 것이기 때문에 obj1과 obj2는 서로 다른 객체가 되어버리는데, 그림으로 표현해보면
주소 | 1001 | 1002 | 1003 | 1004 | 1005 | |
데이터 | 이름 : a 값 : 5001 |
이름 : b 값 : 5004 |
이름 : obj1 값 : 5002 |
이름 : obj2 값 : 5006 |
||
주소 | 5001 | 5002 | 5003 | 5004 | 5005 | 5006 |
데이터 | 10 | 7103 ~ 7104 | 'ddd' | 15 | 20 | 8204~8205 |
주소 | 7103 | 7104 | 8204 | 8205 | ||
데이터 | 이름 : c 값 : 5005 |
이름 : d 값 : 5003 |
이름 : c 값 : 5005 |
이름 : 'd' 값 : 5003 |
obj1 === obj2 // false
불변 객체를 만드는 방법
위 동작을 보면 참조형 데이터는 가변값이라고 할 때 내부의 프로퍼티를 변경할 때만 성립한다고 볼 수 있다. 내부 프로퍼티 변경이 아닌 새로운 데이터를 할당하면 기존 데이터는 변하지 않는다. 객체의 변경이 필요할 때, 기존 객체는 변하지 않아야 하는 경우에 불변 객체가 필요하다.
기존 객체를 유지하려면 새로만든 객체와 서로 다른 객체를 바라보게 만들면 된다. 원시적인 방법으로는 새로운 객체를 할당해주는 방법이 있다.
// 같은 객체를 바라보는 경우. 원본 객체에 변경이 일어날 수 있음
let obj1 = {a: 1}
let obj2 = obj1
obj2.b=2
console.log(obj1,obj2) // {a:1, b:2} , {a:1, b:2}
// 객체를 재할당해주는 경우. 원본 객체를 지킬 수 있음
let obj1 = {a: 1}
let obj2 = {a: 1}
obj2.b=2
console.log(obj1, obj2) // {a: 1} , {a: 1, b: 2}
하지만 새로운 객체를 할당해주는 이 방법은 객체안의 프로퍼티가 많아질수록 변경해야할 것이 많아지고, 계속해서 하드코딩을 해야한다. 그래서 반복문으로 기존의 정보를 모두 복사하여 새로운 객체를 만드는 코드를 짤 수 있다.
// 원본 객체를 복사하는 함수
const copyObject = (target) => {
const result = {};
for(let prop in target) {
result[prop] = target[prop];
}
return result
}
이러한 방법을 얕은 복사라고 한다. 깊은 복사와 비교해보면서 두 방법의 차이, 얕은 복사의 한계 등을 정리해보자.
얕은 복사와 깊은 복사
얕은 복사는 위에서 copyObject 함수와 같이 바로 아래 단계의 값만 복사하는 방법이고, 깊은 복사는 그 내부의 모든 값들을 모두 복사하는 방법이다. copyObject는 객체 내부 프로퍼티에 또 객체가 있는 경우에 문제가 발생한다. 아래 코드에서 obj1의 b 프로퍼티는 참조형 데이터이기 때문에 현재 copyObject로는 b의 객체를 그대로 참조하게 되기에, 복사한 obj2의 b를 변경했을 때 obj1도 바뀌게 된다. 얕은 복사의 한계이다. 깊은 복사는 이 과정에서 참조형 데이터가 있을 때마다 재귀적으로 내부의 모든 값에 대해서 복사를 수행하는 것이다.
let obj1 = {
a: 1,
b: {
c:2
}
}
let obj2 = copyObject(obj1)
obj2.e = 4
obj2.b.d = 3
console.log(obj1,obj2) // {... b: {c:2, d:3}} , {... b: {c:2, d:3}, e:4}
copyObject처럼 직접 순회하는 코드를 짜지 않고도 얕은복사와 깊은 복사를 하는 방법이 있다.
얕은 복사를 하는 방법
- Object.assign({}, obj)
- { ...obj }
- arr.slice()
- [...arr]
깊은 복사를 하는 방법
- JSON.parse(JSON.stringfy(obj))
undefined 와 null
undefined는 사용자가 명시적으로 지정할 수도 있지만 값이 존재하지 않을 때 자바스크립트 엔진이 자동으로 부여하는 경우도 있다. 사용자가 명시적으로 붙이는 경우는 드물기 때문에 엔진이 자동으로 undefined를 부여하는 경우에 대해서 알아보자.
- 값을 대입하지 않은 변수, 즉 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때
- 객체 내부의 존재하지 않는 프로퍼티에 접근하려고 할 때
- return 문이 없거나 호출되지 않는 함수의 실행 결과
let a;
let obj = {
a: '1'
}
let func = function() {}
console.log(a); // undefined
console.log(obj.b); // undefined
console.log(func()); // undefined
1번 경우에 배열인 상황에서는 조금 특이한 동작을 한다.
let arr1 = [];
arr1.length = 2;
console.log(arr1); // [empty X 2]
let arr2 = new Array(2);
console.log(arr2) // [empty X 2]
배열인 상황에서는 "undefined"가 아닌 비어있는 요소 "empty" 가 출력된다. 비어있는 요소는 배열을 순회할 때 순회 대상에서 제외되는 특징이 있다. 배열도 객체이기 때문에 존재하지 않는 프로퍼티에 대해서는 순회할 수 없는 것을 생각하면 자연스럽다. 실제로는 인덱스에 값을 지정하는 시점에 메모리에서 빈 공간을 확보하고 인덱스를 이름으로 지정하고 데이터의 주소값을 지정하는 동작을 한다.
let arr1 = [];
arr1.length = 2;
arr1[1] = 2;
arr1.forEach((v)=>console.log(v)) // 2
let arr2 = [];
arr2.length = 2;
arr2[0] = undefined;
arr2[1] = 2;
arr2.forEach((v)=>console.log(v)) // undefined,2
위의 결과를 비교해보면 배열에서 사용자가 직접 undefined를 부여하는 경우에는 인덱스에 값을 지정하여 프로퍼티가 존재하고, 이는 배열에서 순회의 대상이 된다. 반면에 자바스크립트 엔진이 반환해주는 undefined는 프로퍼티가 존재하지 않음을 의미한다. 그리고 이렇게 사용자가 명시적으로 undefined를 부여하는 경우에는 사실 "비어있음"을 의미하는 null을 사용한다.
정리하면 undefined는 어떤 변수에 값이 존재하지 않을 경우(직접 부여하는 경우가 아닌 자바스크립트 엔진이 반환해주는)를 의미하고, null은 사용자가 명시적으로 없음을 표현하기 위해 대입한 값이다.
'JavaScript' 카테고리의 다른 글
AJAX (0) | 2025.07.26 |
---|---|
자바스크립트 이벤트루프 (1) | 2025.07.26 |
자바스크립트에서의 this (0) | 2025.07.19 |
자바스크립트의 렉시컬 스코프(Lexical Scope) (0) | 2025.07.19 |
var, let, const 비교 (0) | 2025.07.11 |