[코어 자바스크립트] - 데이터 타입

이예슬·2022년 12월 11일
0

데이터 타입의 종류

자바스크립트 데이터 타입은 기본형(원시형, primitive type)과 참조형(reference type)이 있다. 할당 시 기본형은 값이 담긴 주솟값을 바로 복제, 참조형은 값이 담긴 주솟값들로 이루어진 묶음을 가리키는 주소값을 복제한다.

기본형

  • 숫자(number)
  • 문자열(string)
  • 불리언(boolean)
  • null
  • undefined
  • Symbol(ES6에서 추가)

참조형

  • 객체(object)
    • 배열(Array)
    • 함수(Function)
    • 날짜(Date)
    • 정규표현식(RegExp)
    • Map, WeakMap(ES6에서 추가)
    • Set, WeakSet(ES6에서 추가)

Symbol

  • 고유하고 변경 불가능한 원시값
  • 일반적으로 객체의 프로퍼티 키를 고유하게 설정함으로써 프로퍼티 키의 충돌을 방지하기 위해 사용된다.
  • 인자로 전달하는 문자열은 단지 설명문으로 디버깅 용도로만 사용 가능

Map과 WeakMap Set과 WeakSet

→ Map에서 객체를 키로 사용한 경우 Map이 메모리에 있는 한 객체도 메모리에 남으며 가비지 컬렉터의 대상이 되지 않는다.

→ WeakMap을 사용하면 키로 쓰인 객체가 가비지 컬렉션의 대상이 된다.(WeakMap의 키는 항상 객체이다)

→ WeakMap은 반복과 keys(), values(), entries() 메서드를 지원하지 않으며 키나 값 전체를 얻는 게 불가능하다.

→ WeakSet은 객체만 저장할 수 있다. WeakMap과 같이 Set 안의 객체가 도달 가능할 때만 메모리에서 유지된다.

https://ko.javascript.info/weakmap-weakset

💡 기본형은 불변성의 띈다.

데이터 타입에 관한 배경지식

메모리와 데이터

  • 컴퓨터는 모든 데이터를 0 또는 1로 바꿔 기억하며 0 또는 1로 표현할 수 있는 하나의 메모리 조각을 비트라고 한다.
  • 메모리는 많은 비트들로 구성돼 있으며 각 비트는 고유한 식별자를 통해 위치를 확인할 수 있다.
  • 1byte = 8bit

바이트 역시 비트와 같이 시작하는 비트의 식별자로 위치를 파악할 수 있다.

모든 데이터는 바이트 단위의 식별자 즉 메모리 주솟값을 통해 서로 구분하고 연결할 수 있다.

식별자와 변수

변수 : 변할 수 있는 무언가

식별자: 어떤 데이터를 식별하는 데 사용하는 이름, 변수명

변수 선언과 데이터 할당

선언 : 공간을 확보하고 변수명과 주소를 매칭시키는 과정

할당 : 해당 변수가 가리키는 주소의 공간에 데이터를 저장하는 과정

변수 선언

	var a; // 식별자를 a로 하는 변할 수 있는 데이터를 만든다. 

변수란 결국 변경 가능한 데이터가 담길 수 있는 공간 또는 그릇을 말한다.

데이터 할당에 따른 메모리 영역의 변화는 아래와 같은 순서로 이뤄진다.

주소10021003
데이터이름: a 값:
  1. 메모리에서 비어있는 공간 하나를 확보한다.
  2. 해당 공간의 이름(식별자)을 a라 지정한다.

이후 사용자가 a에 접근하고자 하면 컴퓨터는 메모리에서 a라는 이름을 가진 주소를 검색해 해당 공간에 담긴 데이터를 반환한다.

데이터 할당

var a; // 변수 a 선언
a = 'abc'; // 변수 a에 데이터 할당 

var a = 'abc'; //변수 선언과 할당을 한 문장으로 표현 

해당 위치에 문자열 ‘abc’를 직접 저장하지는 않으며 데이터를 저장하기 위한 별도의 메모리 공간을 다시 확보해서 문자열 ‘abc’를 저장하고 그 주소를 변수 영역에 저장한다.

변수영역주소10021003
데이터이름: a, 값:@5003
데이터 영역주소50035004
데이터‘abc’‘abcedf’
  1. 변수 영역에서 빈공간(@1003)을 확보한다.
  2. 확보한 공간의 식별자를 a로 지정한다.
  3. 데이터 영역의 빈공간(@5004)에 문자열 ‘abc’를 저장한다.
  4. 변수 영역에서 a라는 식별자를 검색한다.(@1003)
  5. 앞서 저장한 문자열의 주소(@5004)를 @1003의 공간에 대입한다.

한 번에 저장하지 않는 이유?

⇒ 데이터 변환을 자유롭게 + 메모리를 효율적으로 관리

  • 데이터 변환을 자유롭게 자바스크립트의 경우 숫자형 데이터는 64비트의 공간을 확보하지만 문자열은 특별히 정해진 규격이 없다. 이처럼 각각 필요한 메모리 용량이 가변적이므로 효율적인 데이터 변환을 처리하기 위해 변수와 데이터의 공간을 분리했다. 만약 변수 a의 값을 ‘abcdef’로 변경하기 위해서는 이미 저장된 @5003 메모리의 값에 ‘def’를 추가 하는 것이 아닌 새로운 공간에 ‘abcdef’라는 문자열을 새로 만들어 그 주소를 변수 공간에 연결한다. 이는 기존 문자열에 어떤 변환을 하든 상관없이 동일하게 이뤄진다.
  • 중복된 데이터에 대한 효율적인 처리 가능 이미 저장된 값의 경우 새로운 공간에 다시 중복하여 저장하는 것이 아닌 저장된 메모리의 주소를 가르킬 수 있도록 처리한다.

기본형 데이터와 참조형 데이터

불변값

불변성 여부를 구분할 때의 변경 가능성의 대상은 데이터 영역 메모리이다.

즉 불변값이란 메모리 영역에서의 변경이 불가능한 값을 의미한다.

기본형 데이터는 모두 불변값이다.

var a = 'abc' 
a = a + 'def'

위 코드에서 a를 보면 ‘abc’에서 ‘abcdef’로 변경된 것처럼 보이지만 이는 메모리의 값이 변경된 것이 아니라 식별자와 연결되어 있는 메모리가 변경된 것이다. ‘abc’와 ‘abcdef’는 완전히 별개의 데이터이다.

변경은 새로 만드는 동작으로 통해서만 이뤄지며 이것이 불변값의 성질이다. 한 번 만들어진 값은 가비지 컬렉팅을 당하지 않는 한 영원히 변하지 않는다.

가변값

참조형 데이터는 가변값이다.

(물론 설정에 따라 변경 불가능한 경우도 있고 아예 불변값으로 활용하는 방안도 있다)

참조형 데이터의 할당 과정은 아래와 같다.

var obj = { a: 1, b: 'bbb' } 
변수영역주소10021003
데이터이름: obj, 값:@5003
데이터 영역주소5003500450055006
데이터@7103 ~ ?1‘bbb’2
객체 @5003의 변수 영역주소71037104
데이터이름: a, 값: @5004이름: b, 값: @5005
  1. 컴퓨터는 우선 변수 영역의 빈 공간(@1002)을 확보하고 그 주소의 이름을 obj로 지정한다.
  2. 임의의 데이터 저장 공간(@5003)에 데이터를 저장하려고 보니 여러 개의 프로퍼티로 이뤄진 데이터 그룹이다. 이 그룹 내부의 프로퍼티들을 저장하기 위해 별도의 변수 영역을 마련하고 그 영역의 주소(@7103 ~ ?)를 @5001에 저장한다.
  3. @7103 및 @7104에 각각 a, b라는 프로퍼티 이름을 지정한다.
  4. 데이터 영역에서 숫자 1을 검색한다. 검색 결과가 없으므로 임의로 @5003에 저장하고 그 주소를 @7103에 저장한다. 문자열 ‘bbb’ 역시 임의로 @5004에 저장하고 이 주소를 @7104에 저장한다.

기본형 데이터와의 차이는 객체의 변수(프로퍼티) 영역이 별도로 존재한다는 점이다. 즉 참조형 데이터의 경우 객체는 어딘가에 따로 저장되어 있고 그 객체가 저장된 주소만을 참조한다. 데이터 영역에 저장된 값은 모두 불변 값이다. 하지만 변수에는 다른 값을 얼마든지 대입할 수 있다. 이 때문에 참조형 데이터는 가변값이라고 한다.

obj.a = 2;

객체 내부의 프로퍼티를 변경하게 될 경우 @1002에서 참조하고 있는 값은 변경사항이 없으며 @7103에서 참조하고 있는 주소만이 변하게 된다.

즉 새로운 객체가 만들어진 것이 아니라 기존의 객체 내부의 값만 변경된다.

중첩 객체의 프로퍼티 할당 과정은 아래와 같다.

var obj = { a: 1, arr: [1, 2, 3] } 
변수영역주소10021003
데이터이름: obj, 값:@5003
데이터 영역주소50035004500550065007
데이터@7103 ~ ?1@810323
객체 @5003의 변수 영역주소71037104
데이터이름: a, 값: @5004이름: arr, 값: @5005
배열 @5005의 변수 영역주소810381048105
데이터이름: 0, 값: @5004이름: 1, 값: @5006이름: 2, 값: @5007

위 상태에서 재할당을 하게 될 경우 새로 공간을 확보하여 값을 저장하고 변수영역에서 가르키는 주소를 변경하게 된다.

이 때 만약 메모리 주소의 참조 카운트가 0이 되면 메모리 주소는 가비지 컬렉터의 수거 대상이 된다.

변수 복사 비교

var a = 10; 
var b = a; 

var obj1 = { c: 10, d: 'ddd'};
var obj2 = obj1;
변수영역주소1001100210031004
데이터이름: a, 값:@5001이름: b, 값:@5001이름: obj1, 값:@5002이름: obj2, 값:@5002
데이터 영역주소5001500250035004
데이터10@7103 ~ ?‘ddd’
객체 @5001의 변수 영역주소7103710471057104
데이터이름: c, 값: @5001이름: d, 값: @5003

변수를 복사하는 과정은 기본형 데이터와 참조형 데이터 모두 같은 주소를 바라보게 되는 점에서 동일하다.

복사 과정은 동일하지만 데이터 할당 과정에서 차이가 있으므로 변수 복사 이후 동작에 큰 차이가 발생한다.

b = 15; 
obj2.c = 20; 
변수영역주소1001100210031004
데이터이름: a, 값:@5001이름: b, 값:@5004이름: obj1, 값:@5002이름: obj2, 값:@5002
데이터 영역주소50015002500350045005
데이터10@7103 ~ ?‘ddd’1520
객체 @5001의 변수 영역주소7103710471057104
데이터이름: c, 값: @5005이름: d, 값: @5003

기본형 데이터를 복사한 변수 b의 값을 변경하면 @1002의 값이 변경된다. 하지만 참조형 데이터를 복사한 변수 obj2의 프로퍼티 값을 변경하면 @1004의 값은 변경되지 않는다. 즉 변수 a, b 는 서로 다른 주소를 바라보게 되었으나 obj1, obj2는 여전히 같은 객체를 바라보고 있다.

단 내부 프로퍼티를 변경하는 것이 아닌 객체 자체를 변경하게 될 경우 새로운 객체를 할당함으로써 객체에 대한 변경임에도 값이 달라지게 된다.

즉 참조형 데이터가 가변값이라고 할 때 가변은 참조형 데이터 자체를 변경할 경우가 아니라 그 내부의 프로퍼티를 변경할 때만 성립한다.

불변 객체

값으로 전달받은 객체에 변경을 가하더라도 원본 객첸느 변하지 않아야 할 때 불변 객체가 필요하다.

불변 객체를 만드는 간단한 방법

앞서 봤듯이 참조형 데이터의 가변은 데이터 자체가 아닌 내부 프로퍼티를 변경할 때만 성립한다. 데이터 자체를 변경하고자 하면 기본형 데이터와 마찬가지로 기본 데이터는 변하지 않는다. 그렇다면 내부 프로퍼티를 변경할 필요가 있을 때마다 매번 새로운 객체를 만들어 재할당하기로 규칙을 정하거나 자동으로 객체를 만드는 도구(immutable.js, immer.js, immutability-helper)를 활용한다면 객체 역시 불변성을 확보할 수 있을 것이다.

기존 정보를 복사해서 새로운 객체를 반환하는 함수(얕은 복사)

var copyObject = function (target) {
	var result = {} 
	for(var prop in target) {
		result[prop] = target[prop]; 
	} 
	return result; 
}; 

얕은 복사와 깊은 복사

얕은 복사

  • 바로 아래 단계의 값만 복사하는 방법 (중첩된 객체에서 프로퍼티 복사시 주소값만 복사)
  • 사본을 바꾸면 원본도 바뀐다.
  • Array.prototype.slice()
  • Object.assign()
  • spread 연산자

깊은 복사

  • 내부의 모든 값들을 전부 찾아 복사하는 방법
  • 사본을 바꿔도 원본이 변경되지 않는다.
  • JSON.parse & JSON.stringify
  • 재귀함수
  • Lodash cloneDeep

Array.prototype.slice()

slice() 는 원본을 대체하지 않습니다. 원본 배열에서 요소의 얕은 복사본을 반환합니다.

slice()는 중첩 객체의 깊은 복사를 수행할 수 없다.

**Object.assign()**

Object.assign()은 속성의 값을 복사하기 때문에 깊은 복사를 수행하려면 다른 방법을 사용해야 한다.

Object.assign(새로운 객체, 복사할 객체)

const object = {
  a: "a",
  number: {
    one: 1,
    two: 2,
  },
};
 
const copy = Object.assign({}, object);
 
copy.number.one = 3;
 
console.log(object === copy); // false
console.log(object.number.one  === copy.number.one); // true

재귀함수

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;  
}

깊은 복사를 하기 위해서는 target이 객체인 경우 내부 프로퍼티들을 순회하며 copyObjectDeep 함수를 재귀적으로 호출하고 객체가 아닌 경우 target을 그대로 지정하게끔 해야 한다. 해당 함수를 사용해 객체를 복사한 경우 원본과 사본이 서로 완전히 다른 객체를 참조하게 되어 어느 쪽의 프로퍼티를 변경하더라도 다른 쪽에 영향을 주지 않는다.

JSON.parse & JSON.stringify

JSON.stringify() 는 객체를 json 문자열로 변환하는데 이 과정에서 원본 객체와의 참조가 모두 끊어진다. 객체를 json 문자열로 변환 후 JSON.parse()를 이용해 다시 객체로 변환한다. 간단하지만 다른 방법에 비해 느리며 함수나 숨겨진 프로퍼티와 같은 JSON으로 변경할 수 없는 프로퍼티는 모두 무시된다. 따라서 순수한 정보만 다를 때 활용하기 좋다.

const object = {
  a: "a",
  number: {
    one: 1,
    two: 2,
  },
};

const copy = JSON.parse(JSON.stringify(object));

console.log(object === copy); // false
console.log(object.number.one === copy.number.one); // false
console.log(object.arr === copy.arr); // false

Lodash

→ A modern JavaScript utility library delivering modularity, performance & extras.

.cloneDeep.clone 을 재귀적으로 수행해주는 함수

💡 hasOwnProperty를 활용해 프로토타입 체이닝을 통해 상속된 프로퍼티를 복사하지 않게끔 할 수도 있다.

hasOwnProperty() : 해당 객체에 특정 프로퍼티가 존재하면 true, 그렇지 않으면 false 를 반환한다.

단, 프로토타입 체인은 확인하지 않고, 해당 객체가 스스로 정의한 프로퍼티만을 판단한다.

undefined와 null

undefined와 null은 자바스크립트에서 없음을 나타내는 두 가지 값이지만 의미와 목적이 다르다.

undefined

undefined는 사용자가 명시적으로 지정할 수도 있지만 값이 존재하지 않을 겨우 자바스크립트 엔진이 자동으로 부여하는 경우도 있다.

자바스크립트 엔진은 사용자가 응당 어떤 값을 지정할 것이라고 예상되는 상황임에도 실제로는 그렇게 하지 않았을 때 undefined를 반환한다.

  1. 값을 대입하지 않은 변수, 즉 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때
  2. 객체 내부의 존재하지 않는 프로퍼티에 접근하려고 할 때
  3. return 문이 없거나 호출되지 않는 함수의 실행 결과
let arr1 = [undefined, 1]
let arr2 = []
arr2[1] = 1

arr1.forEach((v, i) => console.log(v, i)) // undefined 0 / 1 1 
arr2.forEach((v, i) => console.log(v, i)) // 1 1 

arr1.map((v, i) => {return v + i}) // [NaN, 2] 
arr2.map((v, i) => {return v + i}) // [empty, 2]

arr1.filter((v) => {return !v}) //[undefined]
arr2.filter((v) => {return !v}) //[]

arr1.reduce((p, c, i) => {return p + c + i}, '') // undefined011
arr2.reduce((p, c, i) => {return p + c + i}, '') // 11

비어있는 요소와 undefined를 할당한 요소는 출력 결과부터 다르다.

비어있는 요소는 순회와 관련된 많은 배열 메서드들의 순회 대상에서 제외된다. 이는 배열도 객체이므로 존재하지 않는 프로퍼티에 대해서는 순회할 수 없기 때문이다. 배열은 객체와 마찬가지로 특정 인덱스에 값을 지정할 때 비로소 빈 공간을 확보하고 인덱스를 이름으로 지정하고 데이터의 주소값을 저장한다.

즉 값이 지정되지 않은 인덱스는 아직은 존재하지 않는 프로퍼티에 지나지 않는다.

사용자가 명시적으로 부여한 경우의 undefined는 그 자체로 값이다. 비어있음을 의미하긴 하지만 하나의 값으로 동작하기 때문에 이때의 프로퍼티나 배열의 요소는 고유의 키값이 실존하게 되고 순회의 대상이 될 수 있다.

사용자가 아무것도 하지 않은 채로 접근했을 때 자바스크립트 엔진이 반환하는 undefined는 해당 프로퍼티 내지 배열의 키값 자체가 존재하지 않음을 의미한다.

값으로써 어딘가에 할당된 undefined는 실존하는 데이터인 반면, 자바스크립트 엔진이 반환해주는 undeined는 문자 그대로 값이 없음을 나타내는 것이다.

null

이처럼 복잡한 경우를 피하기 위해 null을 사용한다.

null은 식별되지 않은 것을 표현하며 즉 변수가 아무런 객체를 가리키지 않음을 표현한다.

비어있음을 명시적으로 나타내고 싶을 때는 undefined가 아닌 null을 쓰면 된다.

💡typeof null 은 object이다.

따라서 어떤 변수의 값이 null인지 여부를 판별하기 위해서는 typeof를 사용하면 안된다.

let 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

<코어 자바스크립트> 정재남, 위키북스(2019)

profile
꾸준히 열심히!

0개의 댓글