Data types

김상연·2022년 11월 24일

JavaScript

목록 보기
6/19

1. Methods of primitives

원시타입의 메소드라니 어불성설인 것 같다. 그러나 자바스크립트는 가능하다!

예를들어,

const str = 'hello';
str[2]	//	'l'
str.toUpperCase()	//	'HELLO'

이렇게 보면 객체인 것도 같다. 그러나 원시타입 맞다. 반례로 객체라면 무언가 할당 할 수도 있겠지만 그것은 가능하지 않다.(strict 모드에서는 fatal error를 일으킬 정도다.)

자바스크립트가 이를 구현하는 방법은 다음과 같다.

  1. (원시타입)변수의 메소드가 호출 되는 순간 wrapper object가 생성된다. 이놈은 메소드를 호출했던 (원시타입)변수의 값을 가지며, 여러가지 유용한 메소드를 포함하고 있다.
  2. 메소드가 실행되고 결과 값을 리턴한다.
  3. 메소드 실행 후에 wrapper object는 사라진다. (원시타입)변수는 원시타입 인 채로 남는다.

이런 과정이 복잡해보이지만 최적화를 엄청 잘 해놔서 매우 적은 비용으로 호출 할 수 있단다.

예외적으로, undefined 또는 null 타입의 경우는 아무것도 없다. 가장 순수한(?) 원시 타입이라고 할 수 있다.

2. ParseInt(str) 과 Number(str)의 차이점은 유연함이다.

parseInt()가 더욱 유연하다. '0~9'가 아닌 놈을 만나기 직전까지 숫자로 변환 해 준다. 그러니까 통화 심볼이나 cm, px같은 단위가 붙은 걸 바꾸기 좋다.

물론 나는 그냥 numberize같은 함수 만들어 쓴다.

3. 배열; push / pop은 빠르고, shift / unshift는 느리다.

깊게 생각하지 않아도,

앞쪽을 건드리면 나머지 애들도 다~ 옮겨져야 한다. array의 근본은 순서에 있기 떄문이다. 배열 뒤집기나 정렬 알고리즘 같은 것을 짜 봤다면 손이 많이 가는걸 알 것이다. 그래서 느리다.

4. 배열; for of를 사용하고, for in은 사용 말자.

물론 가능은 하다. 기술적으로 배열은 object이기 때문에 for in도 사용 할 수는 있다.

그런데 for in은 모든 속성을 전부 참조한다. 때로 우리는 배열이 아닌데 배열 처럼 보이는 array-like, 즉 iterable한 요소를 다루게 되는데(특히 웹에서), 이놈들은 non-numeric한 속성과 메소드도 가지고 있다. for in 반복문에서는 이것들까지 참조된다.

또 (여전히 빠르지만) 10 ~ 100배의 성능 차이도 있다.

5. 시스템 심볼 : JS 작동 방식의 일면

배열 메소드 중에 concat이란게 있다. 메소드를 콜한 배열과 인자에 주어진 요소를 합친 새로운 배열을 리턴하는데, 인자로 배열이 올 경우 그 요소들이 모두 복사되어 합쳐진다. 그런데 array-like한 객체를 넣어준다면 어떨까?

const arrayLikeObject = {
	0: 'hi',
    1: 255,
    length: true
}

//	return [ ... , {0: 'hi', 1: 255, length: true}]

안된다. 그냥 오브젝트 자체가 요소로 들어온다.

그런데 아래와같은 심볼을 추가 해 주면 잘 된다. 것두 numeric한 속성만 잡아다가 복사 해 준다.

const arrayLikeObject = {
	0: 'hi',
    1: 255,
    [Symbol.isConcatSpreadable]: true,
    length: true,
    myNameIs: 'sangyeon'
}

//	return [ ..., 'hi', 255]

6. array.includes와 array.find

리턴 타입은 다르지만, 언뜻 같은 역할을 하는가 싶은 두 가지다. 그냥 별 생각 없이 습관적으로 객체를 찾을 땐 find를 썼던 것 같다.

find를 쓸 때면 참조가 다르면서 같은(=== 연산의 결과가 true인) 두 객체 없다는 점을 떠올려보면 좋겠다. 객체를 찾더라도 같은 참조를 찾는다면 당연히 includes 메소드를 써야겠고.

7. for of 루프 자세히 살펴보기

for of는 배열 메소드가 아니다. 정확히는 for (ele of iterableSomething) {}에서 iterableSomething의 iterator를 실행시키는 구문이다.

나는 가끔 for of구문을 사용하다가 아래와 같은 오류를 맞이한다.

땡땡땡은 iterable하지 않습니다.

어떤 요소가 iterable하다는 것은 무엇인가? 그 요소가 메소드로 built-in symbol인 Symbol.iterator를 가지고 있다는 것이다. iterator는 무엇인가? objSymbol.iterator를 실행한 결과다. 즉 iterator는 next 메소드를 가진 객체다.

//	[Symbol.iterator]는 아래와 같은 형태의 함수를 가진다.

function() {
	return {
		next: function() {
        	return {done: Boolean, value: whatever}
        }
	}	
}

이때, next 메소드는 {done: Boolean, value: whatever}와 같이 두개의 속성을 가진 객체를 리턴한다.

처음으로 돌아가 for (ele of iterableSomething)구문을 실행한다는 것은 iterableSomething의 Symbol.iterator 메소드를 실행하여 iterator를 얻고, 이를 통해 next()반복 실행하는 것이다. 언제까지? next()done속성이 true인 객체를 리턴 할 때까지. 일례로 내장된 [Symbol.iterator]next메소드는 모든 요소를 한번씩 거치고 마지막에 {done: true}라는 값을 리턴함을 추측할 수 있다.

당연하게도 done속성과 함께 리턴되는 value속성의 값이 바로 for (ele of something)ele가 되는 것이다.

아마도 배열의 Symbol.iterator 메소드는 다음과 같이 생겼을 것이다.

const arr = [1, 2, 3, ];

arr[Symbol.iterator] = () => {
	let i = 0;
	return {
		next() {
			if(i < arr.length) { 
				return {done: false, value: arr[i++]}
			}
			return {done: true}
		}
	}
}

Symbol.iterator는 선언하거나 덮어 쓸 수 있기 때문에 때때로 실용적일 수 있는데, 예를들어 어떤 객체가 numeric한 속성을 가지진 않았지만 무언가의 목록이거나 그룹을 대표하는 속성을 가졌을 때 for of가 적절한 루프가 될 수 있다.

const range = {
	first: 1,
    last: 5,
    ...
}

range[Symbol.iterator] = function() {
    return {
    	current: this.first,
        last: this.last,
        next: function() {
        	if(this.last >= this.current) return {done: false, value: this.current++}
            else return {done: true}
        }
    }
}       

for (const ele of range) {
	console.log(ele)			//	1, 2, 3, 4, 5
}

여담으로 for await of로 http통신 등 비동기적인 작업을 할 때가 있다. 그런데 for of 도 똑같이 잘 된다. 추측컨데 await구문을 발견하면 [Symbol.iterator]대신 [Symbol.asyncIterator]를 사용하는것이 아닌가 싶다.

8. iterables와 array-likes는 서로 다르다.

iterables : [Symbol.iterator]를 메소드로 가진 객체

array-likes : index와 length를 가져 배열처럼 보이는 객체

9. Map 과 Set

이번에 공부하면서 처음 보는 개념이었다.

  • Map

Map은 객체의 일종이다. 특징은 key값으로 string 외에 무엇이든 올 수 있다는 것이다.

기본적인 사용은 다음과 같다.

선언과 할당)
const imMap = new Map([
	[100, 'everything'],
    [true, 'her_answer'],
    [{name: 'sangyeon'}, myInfo],
])
요소 추가)
imMap.set(key, value);
요소 확인)
imMap.get(key);

이 밖에 has(key), delete(key), clear(), size() 같은 메소드가 있으며, 이름에 걸맞는 기능들을 한다.

iterable하기 때문에 for of 루프를 사용 할 수 있는데, map.keys()key들을 순회하거나, map.values()로 값을 순회할 수 있고, map.entries()[key, value]쌍을 순회 할 수도 있다.


  • Set

Set 또한 객체의 일종이다. 그러나 array에 가깝게 생겼다. 특징은 key가 없고 모든 요소가 유일하다. 즉, 같은 것을 두번 넣어도 무시된다.

선언과 할당)
const imSet = new Set(['hi', {my_name: 'wawawa'}, false]);

이 밖에 add(value), delete(value), has(value), clear(), size 같은 메소드와 속성이 있으며, 이름에 걸맞는 기능들을 한다.

역시 iterable하기 때문에 for of구문을 사용할 수 있다.

여담으로, 어떤 배열이 해당 요소를 이미 가졌는지 확인 한 후 push해주는 경우가 있다. find, includes 등으로 직접 구현하기도 한다. 하지만 Set 타입의 데이터를 사용하는 것이 훨씬 성능이 좋다고 한다.

상상력을 발휘 해 보자면, Map같은 경우는 동일한 성향의 객체를 그룹으로 만들고, 각각에 서로다른 값을 부여해야 할 때 사용하면 좋을 것 같다. Set같은 경우야 특징에서 보여주듯 unique한 요소들을 가지되 collection 단위로서만 사용될 때 이용하면 좋겠다.

내 개인 프로젝트 수준이 아니라.. 좀더 큰 규모, 복잡한 계층구조의 데이터를 다룰 때가 되면 많이 쓰지 않을까 싶다. 당분간은... 안녕..!!

10. WeakMap and WeakSet

둘은 MapSet에서 기능을 추가, 제거한 버전이라고 보면 된다. WeakMapkey로, WeakSetvalue로 오직 객체만을 사용할 수 있게 된다. 원시타입 사용은 불가능하다.

  • 얻는 것
    key또는 value에 할당된 객체가 garbage collected 되면 WeakMap이나 WeakSet에서도 자동으로 삭제된다. 일반적인 객체나 배열, Map이나 Set에서는 그렇지 않다.
예시)
let someUser = {
	name: 'kim',
    age: 30
}

const userArray = [someUser];

someUser = null;

//	{name: 'kim', age: 30} 이라는 객체 데이터는 여전히 userArray[0]에 저장 된 참조값으로 참조할 수 있다.
//	만약 userArray가 Array가 아닌 WeakMap 또는 WeakSet 타입이었다면 이 객체는 자동으로 삭제되었을 것이다.
  • 잃는 것
    둘 모두 iterable하지 않게 된다. 즉, 모든 속성을 꺼내볼 수 없게 되고 (특정 요소가 있는지는 확인 가능 has(key or value) ) 당연히 size메소드 또는 속성도 잃어버린다. 이는 어찌보면 당연한 것이 garbage collecting이 알고리즘에 따라 다양한 에 이루어지는 만큼 약속된 결과를 보장하지 못하기 때문이다.

이런 것은 캐쉬 메모리의 데이터 타입으로 적절하다. 재사용 될 경우를 대비하여 어떤 객체에 대한 연산 값을 보조적으로 저장 해 두는 경우 말이다. 그런데 캐쉬 메모리의 참조 값이 garbage colleted 된다면? 즉, 재사용 될 일이 사라진다면? 캐쉬 메모리에서도 다시 사용될 가능성이 없어진다. 이럴 때 알아서 삭제 해 주면 좋으니까 사용되는 것이다.

좋은 설명을 마지막으로 덧붙인다.

WeakMap and WeakSet are used as “secondary” data structures in addition to the “primary” object storage. Once the object is removed from the primary storage, if it is only found as the key of WeakMap or in a WeakSet, it will be cleaned up automatically.

11. 객체에서 map, filter같은 메소드 사용하기

기본적으로 map 또는 filter 같은 메소드가 object에는 없다. 이 말은 다르게 표현하면 iterable하지 않다는 것이다. 실제로 객체에 Symbol.iterator을 참조해보면 undefined라고 나온다.

당연히도 해결 방법은 객체를 iterable하게 만들어주면 된다.Symbol.iterator를 직접 작성하는 것도 재미있는 방법이지만, 그보다는 Object.entriesObject.fromEntries를 사용하면 간단하다.

const prices = {
	apple: 10,
    banana: 20,
    melon: 50
}

const doublePrices = Object.fromEntries(
	Object.entries(prices).map(ele => [ele[0], ele[1] *2])
)

사실 나는 util 모듈을 알고 난 뒤로 전혀 사용하지 않던 메소드였다. 나는 console.log로 찍어보면 [object object] 로 문자열 형 변환되는 객체의 속을 들여다 볼 때 사용해왔던 것이다...

map 이나 reduce 를 적극 활용하게 된 뒤로 객체는 정보를 저장하고, 배열이 데이터를 가공하는 역할이라는 편견이 생겼었는데, 이 파트를 읽고 나니 편견일 뿐이었던 것 같다.

12. Destructuring assignment, 구조분해 할당

전에 팀프로젝트에서 다른분이 사용하셔서 뭔지 대충(우측 배열에 있는 것이 차례대로 할당된다.)은 알았는데 이번에 정확히 알게 되었다.

  • Array destructuring

우변에는 iterable 을 두기만 하면 된다. 왜냐면 구조분해 할당은 결국 for of 루프를 사용하기 때문이다.

그래서 이렇게도 가능하다.

const [first, second, third] = 'abc'	//	각각 'a', 'b', 'c'

const [one, two, three] = new Set([1, 2, 30])	//	각각 1, 2, 30

좌변에 일반 변수 말고 객체도 올 수 있다.

const user = {};

[user.name, user.age] = 'Sangyeon 30'.split(' ');

for of 루프도 좀 더 편하게 사용 할 수 있다.

const user = {
	name: 'sangyeon',
    age: 30
}

for (const [key, value] of Object.entries(user)) {
	console.log('key is: ', key, ', value is : ', value)
}

//	key is : name , value is : sangyeon
//	key is : age , value is : 30

잘 알려진 트릭인데, 두 변수의 값을 바꿔치기에 좋다.

let big = 1000;
let small = 10;

[big, small] = [small, big];

좌변에 기본값을 할당 할 수 있다. 이 값에는 함수도 올 수 있는데, 할당 값이 없을 때 실행된다.

const [first, second = console.log('There is nothing to assign.')] = ['first'];

//	이 구문이 실행되면 콘솔에 'There is ...'가 동일한 실행 컨텍스트에서 찍힌다. 
  • Object destructuring

먼저 기본기는 이렇다.

const obj = {
	height: 100,
    weight: 50,
    length: 200
}

const {length, height} = obj;

//	const height에 100, const lenght에 200이 할당 된다. key를 찾기 때문에 순서는 상관없다.

const {lenght: L = 10} = obj;

//	이렇게 콜론으로 다른 이름을 준다. const L에 200이 할당 된다.
//	= 연산자로 기본값을 줄 수 있고, 배열 구조분해 할당 처럼 함수 실행도 가능하다. 

이런 경우 에러가 발생한다.

let title, width, height;

{title, width, height} = {title: "Menu", width: 200, height: 100};	//	Error occured!

왜냐면 { } 이놈을 코드블럭으로 인식하기 때문이다. 코드블럭에 뭔가 할당 할 수는 없기 때문에 = 가 붙는 순간 에러가 난다. 이 경우 전체 코드에 ( )괄호로 감싸주면 된다. 타입스크립트 서버에 친절하게 에러가 바로 뜬다.

let title, width, height;

({title, width, height} = {title: "Menu", width: 200, height: 100});	//	Now all good :>

객체는 속성으로 다른 객체나 배열을 가질 수도 있다. 당연히 nested destructuring assigment도 가능하다.

let options = {
  size: {
    width: 100,
    height: 200
  },
  items: ["Cake", "Donut"],
  extra: true
};

// destructuring assignment split in multiple lines for clarity
let {
  size: {
    width,
    height
  },
  items: [item1, item2],
  title = "Menu"
} = options;

alert(title);  // Menu
alert(width);  // 100
alert(height); // 200
alert(item1);  // Cake
alert(item2);  // Donut

13. Date 객체 생각 해 볼 점

  • Date.now()는 현재 시간(timestamp)을 빠르게 리턴해준다. 간편하고, 객체가 생성되지 않으니 빠르다.
  • Dates 객체의 차를 통해 시간 차이를 알 수 있다. 숫자로 변환 할 때 timestamp가 되기 때문이다.

14. JSON

다들 알다시피 JSON이란 객체를 전달하기 쉽게 format하는 형식을 말한다. Javascript는 JSON.stringify(object)JSON.parse(formatted-string) 두가지 메소드를 제공한다.

여기까지는 익숙했지만 내겐 새로운 내용들도 있었다. JSON 포맷을 많이 사용해보며 경험적으로는 알고 됐어도 이론으로 확인한 것은 처음이었다.

  • 함수나 심볼, 값이 undefined인 것들은 무시된다. 단, null은 무시되지 않는다.
  • circular reference(순환 참조) 요소를 포함한 객체를 JSON formating 시도하면 무시되는 것이 아니라 오류를 이르킨다.

또 하나 중요한 것은,

  • 각 메소드에 두새개의 인자가 더 올 수 있다.
JSON.stringify(value[, replacer, space])

JSON.parse(str, [reviver])

replacer 는 배열 또는 함수가 올 수 있다. 배열이 주어지는 경우엔 해당 배열의 원소들을 key 값으로 가진 것 외에는 무시된다. 주의할 점은 nested 요소들의 key 값 또한 모두 제시되어야 한다는 점이다.

그래서 아래와 같이 함수를 넣어주는게 더 유연하다.

const obj = {
	numbers: [2, 23, 99, 12, 441, 7],
    someSecret: 'I am your father.'
}

JSON.stringify(obj, function replacer(key, value) {
	return (key === 'someSecret') ? undefined : value
})

//	"{"numbers":[...]}", someSecret의 값은 undefined라서 무시된다.

이 때 replacer함수 는 neseted 요소에도 모두 작용한다.

space는 데이터 수정은 하지 않고 단지 입맛에 따라 보기 편하게 해 줄 뿐이다. 숫자를 주면 indentation을 조절하며, 문자열을 줄 수도 있다.

reviverreplacer 와 같은 역할을 하지만 정 반대의 흐름이라고 보면 된다. 역시 모든 nested 요소에 작용한다.

이 밖에도 객체에 toJSON 메소드를 정의해서 해당 객체가 JSON.stringify 메소드의 인자로 주어질 때 어떻게 변환 될지 미리 정의 해 둘 수도 있다. 정해진 변환 형식이 다양하고 중요하다면, class 속성에서부터 들어가면 편리할 것 같다.

profile
리눅스와 컴퓨터 프로그래밍

0개의 댓글