데이터 타입의 종류
데이터 타입에는 기본형과 참조형이 있다. 기본형은 값이 담긴 주솟값을 복제하는 반면 참조형은 값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주솟값을 복제한다는 점에서 차이가 있다. 또한, 기본형은 불변성(immutability)을 띈다는 점을 기억해두자.
- 기본형(primitive type)
- 숫자(number)
- 문자열(string)
- 불리언(boolean)
- null
- undefined
- (ES6에서 추가된) 심볼(Symbol)
- 참조형(reference type)
- 함수(Function)
- 날짜(Date)
- 정규표현식(RegExp)
- (ES6에서 추가된) Map, WeakMap, Set, WeakSet
데이터 타입에 관한 배경지식
식별자와 변수의 개념
변수는 '변할 수 있는 무언가'로 여기서 '무언가'란 데이터를 말한다.
식별자는 어떤 데이터를 식별하는 데 사용하는 변수명을 의미한다.
메모리와 데이터
컴퓨터는 모든 데이터를 0 또는 1로 표현할 수 있는 비트(bit)로 구성되어 있다. 비트 단위로 위치를 확인하는 것은 매우 비효율적이므로 이를 묶어 다시 하나의 단위로 표현하게 되는데, 이 결과 바이트(byte)가 생겼다. 1바이트는 8개의 비트로 구성되어 있고 총 256(2^8)개의 값을 표현할 수 있다. 각 비트는 고유의 식별자를 지니고, 바이트 역시 이 비트의 식별자로 위치를 파악한다. 따라서 모든 데이터는 바이트 단위의 식별자, 즉 메모리 주솟값을 통해 서로 구분하고 연결한다.
과거 메모리 용량이 부족했던 시절에는..
C/C++, 자바 등의 정적 타입 언어는 메모리 낭비를 최소화하기 위해 데이터 타입별로 할당할 메모리 영역을 2바이트, 4바이트 등으로 나누어 정해놓았다. 예를 들어, 2바이트 크기의 정수형 타입(short)은 0을 포함해 -32768 ~ +32767의 숫자만 허용한다. 만약 이 이상의 숫자를 입력하면 오류가 나기 때문에 사용자가 직접 4바이트 크기의 정수형 타입(int)으로 형변환을 해야 하는 번거로운 작업이 필요했다. 반면, 메모리 용량이 과거보다 커진 상황에서 등장한 자바스크립트는 상대적으로 메모리 관리에 대해서 자유로워졌다. 숫자의 경우 정수형인지 부동소수형인지 구분하지 않고 8바이트를 확보하므로 개발자가 메모리를 신경쓰면서 형변환을 걱정해야 할 상황이 훨씬 덜 발생하게 되었다.
변수 선언과 데이터 할당
변수 선언
var a;
위 코드를 설명하면 '변할 수 있는 데이터를 만든다. 이 데이터의 식별자를 a로 한다'가 된다. 변할 수 있는 데이터이니 선언할 때는 undefined여도 결국에는 변경 가능한 데이터가 담긴 그릇인 셈이다. 이 명령을 바탕으로 메모리 영역에서 어떻게 작업을 수행하는지 살펴보자.
개략적으로 표현하자면, 명령을 받은 컴퓨터는 메모리에서 비어있는 공간 하나(@1002)를 확보한다. 이 공간의 이름을 a라고 지정한다. 여기까지가 변수가 선언되는 과정이다. 이후 사용자가 a에 접근하고자 하면 컴퓨터는 메모리에서 a라는 이름을 가진 주소를 검색해 해당 공간에 담긴 데이터를 반환한다.
데이터 할당
var a;
a = 'abc';
a라는 변수에 문자열 'abc'값을 할당하게 되면 해당 위치에 문자열을 저장하는 것이이라 생각할 수 있다. 하지만 실제론 데이터를 저장하기 위한 별도의 메모리 공간을 다시 확보해서 문자열 'abc'를 저장한 후, 그 주소를 변수 영역에 저장하는 방식으로 이뤄진다.
데이터 영역의 공간(@5003)에 문자열 'abc'를 저장한 후, 변수 영역에서 a식별자를 검색한다. 앞서 저장한 문자열의 주소(@5003)를 1002의 공간에 대입한다. 왜 변수 영역에 값을 직접 대입하지 않고 굳이 번거롭게 한 단계를 더 거치는 걸까? 이는 데이터 변환을 자유롭게 할 수 있게 함과 동시에 메모리를 더욱 효율적으로 관리하기 위해서다. 만약 미리 확보한 공간 내에서만 데이터 변환을 할 수 있다면 변환한 데이터를 다시 저장하기 위해서는 '확보된 공간을 변환된 데이터 크기에 맞게 늘리는 작업'이 선행돼야 한다. 이렇게 하면 컴퓨터가 처리해야 할 연산이 많아지므로 변수와 데이터를 별도의 공간에 나누어 저장하는 것이 최적의 방법이다.
가령, 문자열 'abc'의 마지막에 'def'가 추가되더라도 'abc'는 그대로 냅두고 새로운 공간(@5004)에 'abcdef'를 저장할 것이다.
기본형 데이터와 참조형 데이터
불변값
변수와 상수를 구분하는 성질은 '변경 가능성'이다. 이를 구분 짓는 변경 가능성의 대상은 변수 영역 메모리다. 반면 불변성 여부를 구분할 때의 변경 가능성의 대상은 데이터 영역 메모리다. 기본형 데이터인 숫자, 문자열, boolean, null, undefined, Symbol은 모두 불변값이다.
var a = 'abc';
a = a + 'def';
var b = 5;
var c = 5;
b = 7;
변수 a에 문자열 'abc'를 할당했다가 'def'를 추가하면 새로운 'abcdef'를 만들어 그 주소를 변수 a에 저장한다. 변수 b에 숫자 5를 할당하면 컴퓨터는 일단 데이터 영역에서 5를 찾고, 없으면 그제서야 데이터 공간을 하나 만들어 저장한다. 변수 c에도 같은 수인 5를 할당한다면, 이미 만들어 놓은 5를 가진 주소를 재활용한다. 이처럼 문자열 가밧도 한 번 만든 값을 바꿀 수 없고, 숫자 값도 다른 값으로 변경할 수 없다. 이것이 바로 불변값의 성질이며, 한 번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않는다.
가변값
기본형 데이터와 달리 참조형 데이터의 기본적인 성질은 가변값이다. 그러나 설정에 따라 변경 불가능한 경우도 있고(Object.definedProperty, Object.freeze) 아예 불변값으로 활용하는 방안도 있다.
var obj1 = {
a: 1,
b: 'bbb'
}
기본형 데이터와의 차이는 '객체의 변수 영역'이 별도로 존재한다는 점이다. 자세히 보면 객체가 별도로 할애한 영역은 변수 영역일 뿐 '데이터 영역'은 기존의 메모리 공간을 그대로 활용하고 있다. 그러나 '객체의 변수 영역'에는 다른 값을 얼마든지 대입할 수 있다. 이 부분 때문에 흔히 참조형 데이터는 가변값이라고 한다.
참조형 데이터의 프로퍼티에 다시 참조형 데이터를 할당하는 중첩 객체의 경우를 살펴보자.
var obj = {
x: 3,
arr: [3, 4, 5]
}
배열을 객체에 할당하게 되면 위와 같이 '배열의 변수 영역'이 생성되고, 내부 프로퍼티들을 저장하기 위한 별도의 주소들이 마련된다. 마찬가지로 이미 데이터 영역에 있는 값이라면(@5002) 그 주소값을 활용한다.
만약, 이 상태에서 obj.arr = 'str'이라는 재할당 명령을 내리게 되면 어떻게 될까? 데이터 영역에 @5006 공간을 만들어 'str'을 저장하고, 그 주소를 @7104에 저장할 것이다. (이름: arr, 값: @5006) 이렇게 된다면 @5003의 참조 카운트는 0이 된다. 참조 카운트가 0인 메모리 주소는 가비지 컬렉터(GC)의 수거 대상이 되어 메모리 사용량이 포화 상태에 임박할 때마다 자동으로 수거 대상들을 수거하게 된다. 이 과정에서 연쇄적으로 @8104~? 값도 참조 카운트가 0이 될 것이고, 따라서 '배열 변수@5003의 영역' 전체가 GC 수거 대상이 되어 함께 사라질 것이다.
변수 복사 비교
var a = 10;
var b = a;
var obj1 = {c: 10, d: 'ddd'};
var obj2 = obj1;
변수를 복사하는 과정은 기본형 데이터와 참조형 데이터 모두 같은 주소를 바라보게 되는 점에서 동일하다. 복사 과정은 동일하지만 데이터 할당 과정에서 차이가 있기 때문에 변수 복사 이후의 동작에 큰 차이가 발생한다.
b = 15;
obj2.c = 20;
a를 복사한 b의 값을 15로 변경하면 @5004에 15가 만들어지고, 1002는 해당 값을 저장한다.(이름:b, 값: @5004)
obj1을 복사한 obj2의 프로퍼티 값을 20으로 바꾸더라도 obj1과 obj2는 여전히 같은 객체를 바라보고 있게 된다. 이 결과가 바로 기본형과 참조형 데이터의 가장 큰 차이점이다. 대부분의 책에서 '기본형은 값을 복사하고 참조형은 주솟값을 복사한다'고 설명하지만, 엄밀히 따지면 자바스크립트의 모든 데이터 타입은 참조형 데이터이다.(심화 과정이므로 이해하기 다소 어려울 수 있다) 다만, 기본형은 주솟값을 복사하는 과정이 한 번만 이뤄지고, 참조형은 한 단계를 더 거치게 된다는 차이가 있다.
obj2 = {c: 20, d: 'ddd'}
또한, 위와 같이 객체 자체를 변경하게 되면 obj1과 obj2는 바라보는 주소가 서로 달라지게 된다. 즉, 참조형 데이터가 '가변값'이라고 설명할 때의 '가변'은 참조형 데이터 자체를 변경하는 경우가 아니라 그 내부 프로퍼티를 변경하는 경우에만 성립된다.
불변 객체
불변 객체를 만드는 간단한 방법
불변 객체는 매우 중요한 기초가 되는 개념이다. 데이터 자체를 변경하고자 하면 기본형 데이터와 마찬가지로 기존 데이터는 변하지 않아야 한다. 따라서 원본 객체가 변하지 않아야 하는 경우에 새로운 객체를 만드는 도구(immer라이브러리, spread operator, Object.assign메서드)를 활용한다면 객체 역시 불변성을 확보할 수 있다.
var user = {
name: 'Soyeon',
gender: 'female',
}
var changeName = function(user, newName) {
var newUser = user;
newUser.name = newName;
return newUser;
}
var user2 = changeName(user,'Jeon');
if(user1 !== user2) {
console.log('유저 정보가 변경되었습니다.');
}
console.log(user.name, user2.name); // Jeon Jeon
console.log(user === user2) // true
객체의 가변성으로 인해 원본 객체인 user의 이름까지 변경되어 '유저 정보가 변경되었습니다.' 라는 콘솔이 뜨지 않았다. 따라서 변경 전과 후에 서로 다른 객체를 바라보게 만드는 함수를 만든다.
var copyObject = function (target) {
var result = {};
for(var prop in target) {
result[prop] = target[prop];
}
return result;
}
객체에 프로퍼티가 많을수록, 변경해야 할 프로퍼티가 많을수록 사용자가 하드코딩으로 입력하는 번거로움이 생길 수 있다. 따라서 for in문법을 사용해 객체의 모든 프로퍼티들을 복사하는 함수를 만들고, 모두가 객체 내부 변경에 해당 함수를 사용하도록 합의한다. 그러나 모두가 규칙을 따르지 않을 수 있다. 따라서 이런 경우, immutable.js, baobab.js 등의 라이브러리를 사용해 시스템적으로 제약을 걸 수 있다.
얕은 복사와 깊은 복사
만들어둔 copyObject 함수는 바로 다래 단계의 값만 복사하는 얕은 복사(shallow copy)만 수행한다. 이 말은 중첩된 객체에서 참조형 데이터가 저장된 프로퍼티를 복사할 때 그 주솟값만 복사한다는 의미다. 따라서 해당 프로퍼티는 원본과 사본이 모두 동일한 참조형 데이터의 주소를 가리키게 되므로 사본을 바꾸면 원본도 바뀌고, 원본이 바뀌면 사본도 바뀐다.
var user = {
name: 'soyeon',
urls: {
portfolio: 'https://my-portfolio.com',
blog: 'https://blog.com',
facebook: 'https://facebook.com/abc'
}
};
var user2 = copyObject(user);
user2.name = 'jeon';
console.log(user.name === user2.name) // false
user.usrls.portfolio = 'https://portfolio2.com';
console.log(user.usrls.portfolio === user2.usrls.portfolio) // true
user객체에 직접 속한 프로퍼티에 대해서는 복사해서 완전히 새로운 데이터가 만들어진 반면, 한 단계 더 들어간 urls의 내부 프로퍼티들은 여전히 기존 데이터를 그대로 참조하고 있다.
user2.urls = copyObject(user.urls);
console.log(user.usrls.portfolio === user2.usrls.portfolio) // false
따라서, user2.urls에도 copyObject 함수를 사용해 불변 객체로 만들면 서로 다른 값을 만들 수 있다.
var copyObjectDeep = function(target) {
var result = {};
if(typeof target === 'object' && target !== null) {
for(var prop in target) {
result[prop] = copyObjectDeep(target[prop]);
}
} else {
result = target;
}
return result;
}
이를 바탕으로 copyObject함수를 깊은 복사 방식으로 고칠 수 있다.
이외에도 간단하게 깊은 복사를 처리할 수 있는 방법이 있는데, 바로 객체를 JSON 문법으로 표현된 문자열로 전환했다가 다시 JSON 객체로 바꾸는 것이다.
var copyObjViaJSON = function(target){
return JSON.parse(JSON.stringify(target));
};
var obj = {
a: 1,
b: {
c: null,
d: [1,2],
func1: function () { console.log(3) };
}
}
var obj2 = copyObjViaJSON(obj);
이 방법은 단순함에도 불구하고 잘 동작한다. 다만 메서드가 숨겨진 프로퍼티인 __proto__나 getter/setter 등과 같이 JSON으로 변경할 수 없는 프로퍼티들은 모두 무시한다. httpRequest로 받은 데이터를 저장한 객체를 복사할 때 등 순수한 정보만을 다룰 때 활용하기 좋은 방법이다.
undefined와 null
자바스크립트에는 '없음'을 나타내는 값 두 가지가 있는데, 바로 undefined와 null이다. 두 값의 의미는 미세하게 다르고, 사용 목적 또한 다르다.
undefined
- 사용자가 명시적으로 부여하는 경우
- 자바스크립트 엔진이 부여하는 경우
- 값을 대입하지 않은 변수, 즉 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때
- 객체 내부에 존재하지 않는 프로퍼티에 접근하려고 할 때
- return문이 없거나 호출되지 않는 함수의 실행 결과
var a;
console.log(a) // (1) undefined
var obj = {a: 1}
console.log(obj.b) // (2) undefined
var func = function() {};
var c = func();
console.log(c) // (3) undefined
그런데 1번에서 배열을 넣을 경우 조금 다른 동작을 보인다.
var arr1 = [];
arr1.length = 3;
console.log(arr1) // [empty x 3]
var arr2 = new Array(3);
console.log(arr2) // [empty x 3]
배열에 3개의 빈 요소를 확보했지만 각 요소에는 undefined조차 할당돼 있지 않다. 이처럼 '비어있는 요소'는 순회와 관련된 많은 배열 메서드들의 순회 대상에서 제외된다. 이는 배열만의 특별한 현상은 아니고 '배열도 객체'임을 생각해보면 당연한 현상이다. 즉, 값이 지정되지 않은 인덱스는 '아직은 존재하지 않는 프로퍼티'가 되므로 어떤 처리도 하지 않고 넘어가는 것이다. 반면에 undefined는 '비어있음'을 의미하지만 하나의 값으로 동작하기 때문에 고유의 키값(프로퍼티 이름)이 실존하게 되고, 따라서 순회의 대상이 될 수 있다.
이런 복잡함을 피하기 위해선 자바스크립트 엔진이 반환하는 undefined 외에 우리가 직접 undefined를 할당하지 않도록 하자.
var, let, const
1번에서 '값을 대입하지 않은 변수', 즉 데이터 영역의 메모리 주소를 지정하지 않은 식별자에는 자바스크립트가 직접 undefined를 할당한다. 한편 ES6에서 등장한 let, const는 undefined를 할당하지 않은 채로 초기화를 마치며, 이후 실제 변수가 평가되기 전까지는 해당 변수에 접근할 수 없다.
null
'비어있음'을 명시적으로 나타내고 싶을 때는 undefined가 아닌 null을 사용한다.
여기서 한 가지 주의할 점은 null의 타입이 object라는 점이다. 이는 자바스크립트 자체 버그로, 어떤 변수의 값이 null인지 여부를 판별하기 위해서는 typeof 대신 다른 방식으로 접근해야 한다.
var n = null;
console.log(typeof n); // object
console.log(n == undefined); // true
console.log(n == null); // true
console.log(n === undefined); // false
console.log(n === null); // true
동등 연산자(==)로 비교할 경우 null과 undefined가 서로 같다고 판단한다. 따라서 어떤 변수가 실제로 null인지 아니면 undefined인지확인하기 위해서는 동등 연산자가 아닌 일치 연산자(===)를 써서 정확히 판단해야 한다.
Reference
'개발 > 프론트엔드' 카테고리의 다른 글
[JavaScript] 코어 자바스크립트 week3 -this (0) | 2024.11.05 |
---|---|
[JavaScript] 코어 자바스크립트 week2 -실행 컨텍스트 (0) | 2024.10.28 |
[NextJS] GIF는 Image컴포넌트의 최적화가 지원되지 않는다?! (feat. Cloudinary) (0) | 2024.10.17 |
[CI/CD]vercel에 무료로 organization레포지토리 배포하기 (0) | 2024.09.26 |
[NextJS] NextJS와 TailwindCSS로 폰트 설정하기 (0) | 2024.09.18 |