5. 자료구조와 자료형(3)

5.7 맵과 셋


요약

맵은 키가 있는 값이 저장된 컬렉션입니다.

주요 메서드와 프로퍼티:

  • new Map([iterable]) – 맵을 만듭니다. [key,value]쌍이 있는 iterable(예: 배열)을 선택적으로 넘길 수 있는데, 이때 넘긴 이터러블 객체는 맵 초기화에 사용됩니다.
  • map.set(key, value) – 키를 이용해 값을 저장합니다.
  • map.get(key) – 키에 해당하는 값을 반환합니다. key가 존재하지 않으면 undefined를 반환합니다.
  • map.has(key) – 키가 있으면 true, 없으면 false를 반환합니다.
  • map.delete(key) – 키에 해당하는 값을 삭제합니다.
  • map.clear() – 맵 안의 모든 요소를 제거합니다.
  • map.size – 요소의 개수를 반환합니다.

일반적인 객체와의 차이점:

  • 키의 타입에 제약이 없습니다. 객체도 키가 될 수 있습니다.
  • size 프로퍼티 등의 유용한 메서드나 프로퍼티가 있습니다.

셋은 중복이 없는 값을 저장할 때 쓰이는 컬렉션입니다.

주요 메서드와 프로퍼티:

  • new Set([iterable]) – 셋을 만듭니다. iterable 객체를 선택적으로 전달받을 수 있는데(대개 배열을 전달받음), 이터러블 객체 안의 요소는 셋을 초기화하는데 쓰입니다.
  • set.add(value) – 값을 추가하고 셋 자신을 반환합니다. 셋 내에 이미 value가 있는 경우 아무런 작업을 하지 않습니다.
  • set.delete(value) – 값을 제거합니다. 호출 시점에 셋 내에 값이 있어서 제거에 성공하면 true, 아니면 false를 반환합니다.
  • set.has(value) – 셋 내에 값이 존재하면 true, 아니면 false를 반환합니다.
  • set.clear() – 셋을 비웁니다.
  • set.size – 셋에 몇 개의 값이 있는지 세줍니다.

맵과 셋에 반복 작업을 할 땐, 해당 컬렉션에 요소나 값을 추가한 순서대로 반복 작업이 수행됩니다. 따라서 이 두 컬렉션은 정렬이 되어있지 않다고 할 수 없습니다. 그렇지만 컬렉션 내 요소나 값을 재 정렬하거나 (배열에서 인덱스를 이용해 요소를 가져오는 것처럼) 숫자를 이용해 특정 요소나 값을 가지고 오는 것은 불가능합니다.

  • 맵(Map)은 키가 있는 데이터를 저장한다는 점에서 객체와 유사하지만, 키에 다양한 자료형을 허용한다는 점에서 차이
  • 맵은 객체와 달리 키를 문자형으로 변환하지 않으며, 키엔 자료형 제약이 없음
  • new Map() – 맵을 만듭니다.
  • map.set(key, value)key를 이용해 value를 저장합니다.
  • map.get(key)key에 해당하는 값을 반환합니다. key가 존재하지 않으면 undefined를 반환합니다.
  • map.has(key)key가 존재하면 true, 존재하지 않으면 false를 반환합니다.
  • map.delete(key)key에 해당하는 값을 삭제합니다.
  • map.clear() – 맵 안의 모든 요소를 제거합니다.
  • map.size – 요소의 개수를 반환합니다.
let map = new Map();

map.set('1', 'str1');   // 문자형 키
map.set(1, 'num1');     // 숫자형 키
map.set(true, 'bool1'); // 불린형 키

// 객체는 키를 문자형으로 변환한다는 걸 기억하고 계신가요?
// 맵은 키의 타입을 변환시키지 않고 그대로 유지합니다. 따라서 아래의 코드는 출력되는 값이 다릅니다.
alert( map.get(1)   ); // 'num1'
alert( map.get('1') ); // 'str1'

alert( map.size ); // 3

map[key]는 Map을 쓰는 바른 방법이 아닙니다.

  • map[key] = 2로 값을 설정하는 것 같이 map[key]를 사용할 수 있긴 합니다. 하지만 이 방법은 map을 일반 객체처럼 취급하게 됩니다. 따라서 여러 제약이 생기게 되죠.
  • map을 사용할 땐 map전용 메서드 set, get 등을 사용해야만 합니다.
  • 맵은 키로 객체를 허용합니다.
let john = { name: "John" };
// 고객의 가게 방문 횟수를 세본다고 가정해 봅시다.
let visitsCountMap = new Map();
// john을 맵의 키로 사용하겠습니다.
visitsCountMap.set(john, 123);
alert( visitsCountMap.get(john) ); // 123
  • 객체에 객체형 키를 사용하면 문자형으로 변환시켜 [object Object]가 됨
  • map.set을 호출할 때마다 맵 자신이 반환되기 때문에 map.set을 '체이닝(chaining)'할 수 있습니다.
    ex) map.set('1', 'str1').set(1, 'num1').set(true, 'bool1');

맵의 요소에 반복 작업하기

  • map.keys() – 각 요소의 키를 모은 반복 가능한(iterable, 이터러블) 객체를 반환합니다.
  • map.values() – 각 요소의 값을 모은 이터러블 객체를 반환합니다.
  • map.entries() – 요소의 [키, 값]을 한 쌍으로 하는 이터러블 객체를 반환합니다. 이 이터러블 객체는 for..of반복문의 기초로 쓰입니다.
  • forEach – 맵은 배열과 유사하게 내장 메서드 forEach도 지원합니다.
let recipeMap = new Map([
  ['cucumber', 500],
  ['tomatoes', 350],
  ['onion',    50]
]);

// 키(vegetable)를 대상으로 순회합니다.
for (let vegetable of recipeMap.keys()) {
  alert(vegetable); // cucumber, tomatoes, onion
}

// 값(amount)을 대상으로 순회합니다.
for (let amount of recipeMap.values()) {
  alert(amount); // 500, 350, 50
}

// [키, 값] 쌍을 대상으로 순회합니다.
for (let entry of recipeMap) { // recipeMap.entries()와 동일합니다.
  alert(entry); // cucumber,500 ...
}

맵은 삽입 순서를 기억합니다.
맵은 값이 삽입된 순서대로 순회를 실시합니다. 객체가 프로퍼티 순서를 기억하지 못하는 것과는 다릅니다.

Object.entries: 객체를 맵으로 바꾸기

  • 각 요소가 [키, 값] 쌍인 배열을 맵으로 바꾸기
let map = new Map([
  ['1',  'str1'],
  [1,    'num1'],
  [true, 'bool1']
]);

alert( map.get('1') ); // str1
  • 평범한 객체를 가지고 맵을 만들기
    Object.entries(obj): 객체의 키-값 쌍을 요소([key, value])로 가지는 배열을 반환
let obj = {
  name: "John",
  age: 30
};
let map = new Map(Object.entries(obj));
alert( map.get('name') ); // John

Object.fromEntries: 맵을 객체로 바꾸기

  • Object.fromEntries: 각 요소가 [키, 값] 쌍인 배열을 객체로 바꿔줌
let prices = Object.fromEntries([
  ['banana', 1],
  ['orange', 2],
  ['meat', 4]
]);
// now prices = { banana: 1, orange: 2, meat: 4 }
alert(prices.orange); // 2
  • 맵을 객체로 바꾸기
let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);
//let obj = Object.fromEntries(map.entries()); 
let obj = Object.fromEntries(map); // .entries()를 생략가능
// obj = { banana: 1, orange: 2, meat: 4 }
alert(obj.orange); // 2

  • 셋(Set)은 중복을 허용하지 않는 값을 모아놓은 특별한 컬렉션이며 키가 없는 값이 저장됨
  • new Set(iterable) – 셋을 만듭니다. 이터러블 객체를 전달받으면(대개 배열을 전달받음) 그 안의 값을 복사해 셋에 넣어줍니다.
  • set.add(value) – 값을 추가하고 셋 자신을 반환합니다.
  • set.delete(value) – 값을 제거합니다. 호출 시점에 셋 내에 값이 있어서 제거에 성공하면 true, 아니면 false를 반환합니다.
  • set.has(value) – 셋 내에 값이 존재하면 true, 아니면 false를 반환합니다.
  • set.clear() – 셋을 비웁니다.
  • set.size – 셋에 몇 개의 값이 있는지 세줍니다.
let set = new Set();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
// 어떤 고객(john, mary)은 여러 번 방문할 수 있습니다.
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);
// 셋에는 유일무이한 값만 저장됩니다.
alert( set.size ); // 3
for (let user of set) {
  alert(user.name); // // John, Pete, Mary 순으로 출력됩니다.
}

셋의 값에 반복 작업하기

for..offorEach를 사용하면 셋의 값을 대상으로 반복 작업을 수행할 수 있음

let set = new Set(["oranges", "apples", "bananas"]);
for (let value of set) alert(value);
// forEach를 사용해도 동일하게 동작합니다.
set.forEach((value, valueAgain, set) => {
  alert(value);
});
  • set.keys() – 셋 내의 모든 값을 포함하는 이터러블 객체를 반환합니다.
  • set.values()set.keys와 동일한 작업을 합니다. 맵과의 호환성을 위해 만들어진 메서드입니다.
  • set.entries() – 셋 내의 각 값을 이용해 만든 [value, value] 배열을 포함하는 이터러블 객체를 반환합니다. 맵과의 호환성을 위해 만들어졌습니다.


5.8 위크맵과 위크셋


요약
위크맵은 맵과 유사한 컬렉션입니다. 위크맵을 구성하는 요소의 키는 오직 객체만 가능합니다. 키로 사용된 객체가 메모리에서 삭제되면 이에 대응하는 값 역시 삭제됩니다.

위크셋은 셋과 유사한 컬렉션입니다. 위크셋엔 객체만 저장할 수 있습니다. 위크셋에 저장된 객체가 도달 불가능한 상태가 되면 해당 객체는 메모리에서 삭제됩니다.

두 자료구조 모두 구성 요소 전체를 대상으로 하는 메서드를 지원하지 않습니다. 구성 요소 하나를 대상으로 하는 메서드만 지원합니다.

객체엔 ‘주요’ 자료를, 위크맵과 위크셋엔 ‘부수적인’ 자료를 저장하는 형태로 위크맵과 위크셋을 활용할 수 있습니다. 객체가 메모리에서 삭제되면, (그리고 오로지 위크맵과 위크셋의 키만 해당 객체를 참조하고 있다면) 위크맵이나 위크셋에 저장된 연관 자료들 역시 메모리에서 자동으로 삭제됩니다.

  • 자료구조를 구성하는 요소는 자신이 속한 자료구조가 메모리에 남아있는 동안 대개 도달 가능한 값으로 취급되어 메모리에서 삭제되지 않음
  • 객체의 프로퍼티나 배열의 요소, 맵이나 셋을 구성하는 요소들이 이에 해당함
  • 맵에서 객체를 키로 사용한 경우 역시, 맵이 메모리에 있는 한 객체도 메모리에 남고 가비지 컬렉터의 대상이 되지 않음

위크맵

  • 위크맵의 키가 반드시 객체여야 함
    (원시값은 위크맵의 키가 될 수 없음)

  • 위크맵의 키로 사용된 객체를 참조하는 것이 아무것도 없다면 해당 객체는 메모리와 위크맵에서 자동으로 삭제됨

  • 위크맵은 반복 작업과 keys(), values(), entries() 메서드를 지원하지 않음

  • weakMap.get(key)

  • weakMap.set(key, value)

  • weakMap.delete(key)

  • weakMap.has(key)

유스 케이스: 추가 데이터

  • 위크맵은 부차적인 데이터를 저장할 곳이 필요할 때 그 진가를 발휘함
  • 서드파티 라이브러리와 같은 외부 코드에 ‘속한’ 객체를 가지고 작업을 해야 한다고 가정할 경우
  • 이 객체에 데이터를 추가해줘야 하는데, 추가해 줄 데이터는 객체가 살아있는 동안에만 유효한 상황
  • 위크맵에 원하는 데이터를 저장하고, 이때 키는 객체를 사용하면 객체가 가비지 컬렉션의 대상이 될 때, 데이터도 함께 사라지게 됨
weakMap.set(john, "비밀문서");
// john이 사망하면, 비밀문서는 자동으로 파기됩니다.

예시 :

// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // 위크맵에 사용자의 방문 횟수를 저장함
// 사용자가 방문하면 방문 횟수를 늘려줍니다.
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

// 📁 main.js
let john = { name: "John" };
countUser(john); // John의 방문 횟수를 증가시킵니다.
// John의 방문 횟수를 셀 필요가 없어지면 아래와 같이 john을 null로 덮어씁니다.
john = null;

유스 케이스: 캐싱

  • 위크맵은 캐싱(caching)이 필요할 때 유용함

  • 캐싱은 시간이 오래 걸리는 작업의 결과를 저장해서 연산 시간과 비용을 절약해주는 기법

  • 동일한 함수를 여러 번 호출해야 할 때, 최초 호출 시 반환된 값을 어딘가에 저장해 놓았다가 그다음엔 함수를 호출하는 대신 저장된 값을 사용하는 게 캐싱의 실례임

  • 예시: Map 활용

// 📁 cache.js
let cache = new Map();
// 연산을 수행하고 그 결과를 맵에 저장합니다.
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* 연산 수행 */ obj;
    cache.set(obj, result);
  }
  return cache.get(obj);
}

// 함수 process()를 호출해봅시다.

// 📁 main.js
let obj = {/* ... 객체 ... */};
let result1 = process(obj); // 함수를 호출합니다.
// 동일한 함수를 두 번째 호출할 땐,
let result2 = process(obj); // 연산을 수행할 필요 없이 맵에 저장된 결과를 가져오면 됩니다.
// 객체가 쓸모없어지면 아래와 같이 null로 덮어씁니다.
obj = null;
alert(cache.size); // 1 (엇! 그런데 객체가 여전히 cache에 남아있네요. 메모리가 낭비되고 있습니다.)
  • 예시: WeakMap 활용
// 📁 cache.js
let cache = new WeakMap();
// 연산을 수행하고 그 결과를 위크맵에 저장합니다.
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* 연산 수행 */ obj;
    cache.set(obj, result);
  }
  return cache.get(obj);
}

// 📁 main.js
let obj = {/* ... 객체 ... */};
let result1 = process(obj);
let result2 = process(obj);
// 객체가 쓸모없어지면 아래와 같이 null로 덮어씁니다.
obj = null;
// 이 예시에선 맵을 사용한 예시처럼 cache.size를 사용할 수 없습니다.
// 하지만 obj가 가비지 컬렉션의 대상이 되므로, 캐싱된 데이터 역시 메모리에서 삭제될 겁니다.
// 삭제가 진행되면 cache엔 그 어떤 요소도 남아있지 않을겁니다.

위크셋

  • 위크셋은 셋과 유사한데, 객체만 저장할 수 있고, 원시값은 저장할 수 없음
  • 셋 안의 객체는 도달 가능할 때만 메모리에서 유지됨
  • add, has, delete를 사용할 수 있고, size, keys()나 반복 작업 관련 메서드는 사용할 수 없음
let visitedSet = new WeakSet();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

visitedSet.add(john); // John이 사이트를 방문합니다.
visitedSet.add(pete); // 이어서 Pete가 사이트를 방문합니다.
visitedSet.add(john); // 이어서 John이 다시 사이트를 방문합니다.
// visitedSet엔 두 명의 사용자가 저장될 겁니다.

// John의 방문 여부를 확인해보겠습니다.
alert(visitedSet.has(john)); // true
// Mary의 방문 여부를 확인해보겠습니다.
alert(visitedSet.has(mary)); // false
john = null;

// visitedSet에서 john을 나타내는 객체가 자동으로 삭제됩니다.

위크맵과 위크셋의 가장 큰 단점은 반복 작업이 불가능하다는 점입니다. 위크맵이나 위크셋에 저장된 자료를 한 번에 얻는 게 불가능하죠. 이런 단점은 불편함을 초래하는 것 같아 보이지만, 위크맵과 위크셋을 이용해 할 수 있는 주요 작업을 방해하진 않습니다. 위크맵과 위크셋은 객체와 함께 ‘추가’ 데이터를 저장하는 용도로 쓸 수 있습니다.



5.9 Object.keys, values, entries


  • keys(), values(), entries()를 사용할 수 있는 자료구조 Map, Set, Array

Object.keys, values, entries

  • Object.keys(obj) – 객체의 키만 담은 배열을 반환
  • Object.values(obj) – 객체의 값만 담은 배열을 반환
  • Object.entries(obj) – [키, 값] 쌍을 담은 배열을 반환
구분Map, Set, Array(전용 메서드)객체(일반 메서드)
호출 문법map.keys()Object.keys(obj)(obj.keys() 아님)
반환 값iterable 객체‘진짜’ 배열
  1. obj.keys()가 아닌 Object.keys(obj)를 호출
    이렇게 문법이 다른 이유는 유연성 때문입니다. 아시다시피 자바스크립트에선 복잡한 자료구조 전체가 객체에 기반합니다. 그러다 보니 객체 data에 자체적으로 data.values()라는 메서드를 구현해 사용하는 경우가 있을 수 있습니다. 이렇게 커스텀 메서드를 구현한 상태라도 Object.values(data)같은 형태로 메서드를 호출할 수 있으면 커스텀 메서드와 내장 메서드 둘 다를 사용할 수 있습니다.

  2. 메서드 Object.*를 호출하면 iterable 객체가 아닌 객체의 한 종류인 배열을 반환함
    ‘진짜’ 배열을 반환하는 이유는 하위 호환성 때문입니다.

Object.keys, values, entries는 심볼형 프로퍼티를 무시함

객체 변환하기

#객체에 map 사용하기 객체에 전용 메서드 사용하기

  • 객체에 map, filter 같은 배열 전용 메서드를 사용할 수 없지만 Object.entriesObject.fromEntries를 순차적으로 적용하면 객체에도 배열 전용 메서드 사용할 수 있음
  1. Object.entries(obj)를 사용해 객체의 키-값 쌍이 요소인 배열을 얻습니다.
  2. 1.에서 만든 배열에 map 등의 배열 전용 메서드를 적용
  3. 2.에서 반환된 배열에 Object.fromEntries(array)를 적용해 배열을 다시 객체로 되돌림
let prices = {
  banana: 1,
  orange: 2,
  meat: 4,
};

let doublePrices = Object.fromEntries(
  // 객체를 배열로 변환해서 배열 전용 메서드인 map을 적용하고 fromEntries를 사용해 배열을 다시 객체로 되돌립니다.
  Object.entries(prices).map(([key, value]) => [key, value * 2])
);

alert(doublePrices.meat); // 8


5.10 구조 분해 할당


요약

  • 구조 분해 할당을 사용하면 객체나 배열을 변수로 연결할 수 있습니다.

  • 배열 분해하기:
    let [item1 = default, item2, ...rest] = array
    array의 첫 번째 요소는 item1에, 두 번째 요소는 변수 item2에 할당되고, 이어지는 나머지 요소들은 배열 rest 저장됩니다.

  • 객체 분해하기:
    let {prop : varName = default, ...rest} = object
    object의 프로퍼티 prop의 값은 변수 varName에 할당되는데, objectprop이 없으면 defaultvarName에 할당됩니다.
    연결할 변수가 없는 나머지 프로퍼티들은 객체 rest에 복사됩니다.

  • 할당 연산자 좌측의 패턴과 우측의 구조가 같으면 중첩 배열이나 객체가 있는 복잡한 구조에서도 원하는 데이터를 뽑아낼 수 있습니다.

  • 구조 분해 할당(destructuring assignment): 객체나 배열을 변수로 '분해’할 수 있게 해주는 특별한 문법

배열 분해하기

// 이름과 성을 요소로 가진 배열
let arr = ["Bora", "Lee"]

// let firstName = arr[0];
// let surname = arr[1];
// 구조 분해 할당을 이용
let [firstName, surname] = arr;

alert(firstName); // Bora
alert(surname);  // Lee

'분해(destructuring)'는 '파괴(destructive)'를 의미하지 않습니다.

쉼표를 사용하여 요소 무시하기

// 두 번째 요소는 필요하지 않음
let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert( title ); // Consul

할당 연산자 우측엔 모든 이터러블이 올 수 있습니다.

let [a, b, c] = "abc"; // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3]);

할당 연산자 좌측엔 뭐든지 올 수 있습니다.

let user = {};
[user.name, user.surname] = "Bora Lee".split(' ');
alert(user.name); // Bora

.entries()로 반복하기

// 객체에 활용
let user = {
  name: "John",
  age: 30
};
for (let [key, value] of Object.entries(user)) {
  alert(`${key}:${value}`); // name:John, age:30이 차례대로 출력
}
// map에 활용
let user = new Map();
user.set("name", "John");
user.set("age", "30");
for (let [key, value] of user) {
  alert(`${key}:${value}`); // name:John, then age:30
}

변수 교환 트릭

let guest = "Jane";
let admin = "Pete";
// 변수 guest엔 Pete, 변수 admin엔 Jane이 저장되도록 값을 교환함
[guest, admin] = [admin, guest];
alert(`${guest} ${admin}`); // Pete Jane(값 교환이 성공적으로 이뤄졌습니다!

'…'로 나머지 요소 가져오기

  • 배열 앞쪽에 위치한 값 몇 개만 필요하고 그 이후 이어지는 나머지 값들은 한데 모아서 저장하고 싶을 때, 점 세 개 ...를 붙인 매개변수 하나를 추가하면 ‘나머지(rest)’ 요소를 가져올 수 있음
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert(name1); // Julius
alert(name2); // Caesar
// `rest`는 배열입니다.
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2
  • rest 대신에 다른 이름을 사용해도 되는데, 변수 앞의 점 세 개(...)와 변수가 가장 마지막에 위치해야 함

기본값

  • 할당하고자 하는 변수의 개수가 분해하고자 하는 배열의 길이보다 클 경우, 할당할 값이 없으면 undefined로 취급됨
  • =을 이용하면 할당할 값이 없을 때 기본으로 할당해 줄 값인 '기본값(default value)'을 설정 가능
// 기본값
let [name = "Guest", surname = "Anonymous"] = ["Julius"];
alert(name);    // Julius (배열에서 받아온 값)
alert(surname); // Anonymous (기본값)

// name의 prompt만 실행됨
let [surname = prompt('성을 입력하세요.'), name = prompt('이름을 입력하세요.')] = ["김"];

alert(surname); // 김 (배열에서 받아온 값)
alert(name);    // prompt에서 받아온 값

객체 분해하기

  • let {var1, var2} = {var1:…, var2:…}
  • 할당 연산자 우측엔 분해하고자 하는 객체를, 좌측엔 상응하는 객체 프로퍼티의 '패턴’을 넣음
let options = {
  title: "Menu",
  width: 100,
  height: 200
};
let {title, width, height} = options;
alert(title);  // Menu
alert(width);  // 100
alert(height); // 200
  • 순서는 중요하지 않음
// let {...} 안의 순서가 바뀌어도 동일하게 동작함
let {height, width, title} = {
  title: "Menu", 
  height: 200, 
  width: 100 
}
  • 분해하려는 객체의 프로퍼티와 변수의 연결을 원하는 대로 조정할 수도 있음
  • 분해하려는 객체의 프로퍼티: 목표 변수
let options = {
  title: "Menu",
  width: 100,
  height: 200
};

// { 객체 프로퍼티: 목표 변수 }
let {width: w, height: h, title} = options;

// width -> w
// height -> h
// title -> title

alert(title);  // Menu
alert(w);      // 100
alert(h);      // 200
  • 기본값 적용
let options = {
  title: "Menu"
};

let {width = 100, height = 200, title} = options;

alert(title);  // Menu
alert(width);  // 100
alert(height); // 200
  • 콜론과 할당 연산자를 동시에 사용 가능
let options = {
  title: "Menu"
};

let {width: w = 100, height: h = 200, title} = options;

alert(title);  // Menu
alert(w);      // 100
alert(h);      // 200
  • 프로퍼티가 많은 복잡한 객체에서 원하는 정보만 뽑아옴
let options = {
  title: "Menu",
  width: 100,
  height: 200
};

// title만 변수로 뽑아내기
let { title } = options;

alert(title); // Menu

나머지 패턴 ‘…’

  • (IE에서는 바벨이 필수)
let options = {
  title: "Menu",
  height: 200,
  width: 100
};

// title = 이름이 title인 프로퍼티
// rest = 나머지 프로퍼티들
let {title, ...rest} = options;

// title엔 "Menu", rest엔 {height: 200, width: 100}이 할당됩니다.
alert(rest.height);  // 200
alert(rest.width);   // 100

let 없이 사용하기

let title, width, height;
// 에러가 발생하지 않습니다.
({title, width, height} = {title: "Menu", width: 200, height: 100});
alert( title ); // Menu

중첩 구조 분해

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

// 코드를 여러 줄에 걸쳐 작성해 의도하는 바를 명확히 드러냄
let {
  size: { // size는 여기,
    width,
    height
  },
  items: [item1, item2], // items는 여기에 할당함
  title = "Menu" // 분해하려는 객체에 title 프로퍼티가 없으므로 기본값을 사용함
} = options;

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

똑똑한 함수 매개변수

  • 함수 매개변수 문법은 구조 분해 할당 문법과 동일함
function({
  incomingProperty: varName = defaultValue
  ...
})
  • 예시
let options = {
  title: "My menu",
  items: ["Item1", "Item2"]
};

function showMenu({
  title = "Untitled",
  width: w = 100,  // width는 w에,
  height: h = 200, // height는 h에,
  items: [item1, item2] // items의 첫 번째 요소는 item1에, 두 번째 요소는 item2에 할당함
}) {
  alert( `${title} ${w} ${h}` ); // My Menu 100 200
  alert( item1 ); // Item1
  alert( item2 ); // Item2
}

showMenu(options);
  • 🚨 함수 매개변수를 구조 분해할 땐, 반드시 인수가 전달된다고 가정되고 사용된다는 점에 유의
  • 모든 인수에 기본값을 할당해 주려면 빈 객체를 명시적으로 전달해야 함
showMenu({}); // 모든 인수에 기본값이 할당됩니다.
showMenu(); // 에러가 발생할 수 있습니다.
  • 예방책 예시
function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
  alert( `${title} ${width} ${height}` );
}
showMenu(); // Menu 100 200


5.11 Date 객체와 날짜


요약

  • 자바스크립트에선 Date 객체를 사용해 날짜와 시간을 나타냅니다. Date 객체엔 ‘날짜만’ 혹은 ‘시간만’ 저장하는 것은 불가능하고, 항상 날짜와 시간이 함께 저장됩니다.
  • 월은 0부터 시작합니다(0은 1월을 나타냅니다).
  • 요일은 getDay()를 사용하면 얻을 수 있는데, 요일 역시 0부터 시작합니다(0은 일요일을 나타냅니다).
  • 범위를 넘어가는 구성요소를 설정하려 할 때 Date 자동 고침이 활성화됩니다. 이를 이용하면 월/일/시간을 쉽게 날짜에 추가하거나 뺄 수 있습니다.
  • 날짜끼리 빼는 것도 가능한데, 이때 두 날짜의 밀리초 차이가 반환됩니다. 이게 가능한 이유는 Date 가 숫자형으로 바뀔 때 타임스탬프가 반환되기 때문입니다.
  • Date.now()를 사용하면 현재 시각의 타임스탬프를 빠르게 구할 수 있습니다.
  • 자바스크립트의 타임스탬프는 초가 아닌 밀리초 기준이라는 점을 항상 유의하시기 바랍니다.
  • 간혹 밀리초보다 더 정확한 시간 측정이 필요할 때가 있습니다. 자바스크립트는 마이크로초(1/1,000,000초)를 지원하진 않지만 대다수의 호스트 환경은 마이크로초를 지원합니다. 브라우저 환경의 메서드 performance.now()는 페이지 로딩에 걸리는 밀리초를 반환해주는데, 반환되는 숫자는 소수점 아래 세 자리까지 지원합니다.
  • Date 객체를 활용하면 생성 및 수정 시간을 저장하거나 시간을 측정할 수 있고, 현재 날짜를 출력하는 용도 등으로도 활용할 수 있음

객체 생성하기

  • Date 객체 생성:new Date()

  • new Date(): 인수 없이 호출하면 현재 날짜와 시간이 저장된 Date 객체가 반환됩니다.

  • new Date(milliseconds): UTC 기준(UTC+0) 1970년 1월 1일 0시 0분 0초에서 milliseconds 밀리초(1/1000 초) 후의 시점이 저장된 Date 객체가 반환됨

let now = new Date();
alert( now ); // 현재 날짜 및 시간이 출력됨

// 1970년 1월 1일 0시 0분 0초(UTC+0)를 나타내는 객체
let Jan01_1970 = new Date(0);
alert( Jan01_1970 );

// 1970년 1월 1일의 24시간 후는 1970년 1월 2일(UTC+0)임
let Jan02_1970 = new Date(24 * 3600 * 1000);
alert( Jan02_1970 );
  • 타임스탬프(timestamp): 1970년의 첫날을 기준으로 흘러간 밀리초를 나타내는 정수

  • 1970년 1월 1일 이전 날짜에 해당하는 타임스탬프 값은 음수

  • new Date(datestring)
    인수가 하나인데, 문자열이라면 해당 문자열은 자동으로 구문 분석(parsed)됨

  • new Date(year, month, date, hours, minutes, seconds, ms)
    주어진 인수를 조합해 만들 수 있는 날짜가 저장된 객체가 반환됩니다(지역 시간대 기준, 첫 번째와 두 번째 인수만 필수값)
    - year는 반드시 네 자리 숫자여야 합니다. 2013은 괜찮고 98은 괜찮지 않습니다.
    - month🚨0(1월)부터 11(12월) 사이의 숫자 여야 합니다.
    - date는 일을 나타내는데, 값이 없는 경우엔 1일로 처리됩니다.
    - hours/minutes/seconds/ms에 값이 없는 경우엔 0으로 처리됩니다.

  • 최소 정밀도는 1밀리초(1/1000초)


날짜 구성요소 얻기

  • getFullYear()
    연도(네 자릿수)를 반환합니다.

  • getMonth()
    월을 반환합니다(0 이상 11 이하)

  • getDate()
    일을 반환합니다(1 이상 31 이하)

  • getHours(), getMinutes(), getSeconds(), getMilliseconds()
    시, 분, 초, 밀리초를 반환합니다.

  • getDay()
    일요일을 나타내는 0부터 토요일을 나타내는 6까지의 숫자 중 하나를 반환합니다.

  • 위의 메서드 모두는 현지 시간 기준 날짜 구성요소를 반환합니다.
    위 메서드 이름에 있는 ‘get’ 다음에 'UTC’를 붙여주면 표준시(UTC+0) 기준의 날짜 구성 요소를 반환해주는 메서드 getUTCFullYear(), getUTCMonth(), getUTCDay()를 만들 수 있습니다.

getYear()가 아닌 getFullYear()를 사용하세요.
getYear()는 두 자릿수 연도를 반환하는 경우가 있기 때문에 절대 사용해선 안 됩니다.

  • getTime()
    주어진 일시와 1970년 1월 1일 00시 00분 00초 사이의 간격(밀리초 단위)인 타임스탬프를 반환합니다.
  • getTimezoneOffset()
    현지 시간과 표준 시간의 차이(분)를 반환합니다.

날짜 구성요소 설정하기

  • setFullYear(year, [month], [date])
  • setMonth(month, [date])
  • setDate(date)
  • setHours(hour, [min], [sec], [ms])
  • setMinutes(min, [sec], [ms])
  • setSeconds(sec, [ms])
  • setMilliseconds(ms)
  • setTime(milliseconds) (1970년 1월 1일 00:00:00 UTC부터 밀리초 이후를 나타내는 날짜를 설정)
let today = new Date();
today.setHours(0);
alert(today); // 날짜는 변경되지 않고 시만 0으로 변경됩니다.
today.setHours(0, 0, 0, 0);
alert(today); // 날짜는 변경되지 않고 시, 분, 초가 모두 변경됩니다(00시 00분 00초).

자동 고침

  • Date 객체엔 자동 고침(autocorrection) 이라는 유용한 기능이 있어, 범위를 벗어나는 값을 설정하려고 하면 값이 자동으로 수정됨
let date = new Date(2013, 0, 32); // 2013년 1월 32일은 없습니다.
alert(date); // 2013년 2월 1일이 출력됩니다.
  • 예시: 지금부터 70초 후의 날짜
let date = new Date();
date.setSeconds(date.getSeconds() + 70);
alert( date ); // 70초 후의 날짜가 출력됩니다.
  • 예시: 날짜 구성요소에 0 이나 음수 설정
let date = new Date(2016, 0, 1); // 2016년 1월 1일
date.setDate(0); // 일의 최솟값은 1이므로 0을 입력하면 전 달의 마지막 날을 설정한 것과 같은 효과를 봅니다.
alert( date ); // 31 Dec 2015

Date 객체를 숫자로 변경해 시간차 측정하기

  • Date 객체를 숫자형으로 변경하면 타임스탬프(date.getTime()을 호출 시 반환되는 값)가 됨
let date = new Date();
alert(+date); // 타임스탬프(date.getTime()를 호출한 것과 동일함)
  • 예시: 스톱워치
let start = new Date(); // 측정 시작
// 원하는 작업을 수행
for (let i = 0; i < 100000; i++) {
  let doSomething = i * i * i;
}
let end = new Date(); // 측정 종료
alert( `반복문을 모두 도는데 ${end - start} 밀리초가 걸렸습니다.` );

Date.now()

  • Date.now()new Date().getTime()과 의미론적으로 동일하지만 중간에 Date 객체를 만들지 않는다는 점이 다릅니다. 따라서 new Date().getTime()를 사용하는 것보다 빠르고 가비지 컬렉터의 일을 덜어준다는 장점이 있습니다.
let start = Date.now(); // 1970년 1월 1일부터 현재까지의 밀리초

// 원하는 작업을 수행
for (let i = 0; i < 100000; i++) {
  let doSomething = i * i * i;
}

let end = Date.now(); // done

alert( `반복문을 모두 도는데 ${end - start} 밀리초가 걸렸습니다.` ); // Date 객체가 아닌 숫자끼리 차감함

벤치마크 테스트Date.parse와 문자열

  • '벤치마크 테스트’는 비교 대상을 두고 성능을 비교하여 시험하고 평가할 때 쓰입니다.
  • CPU를 많이 잡아먹는 함수의 신뢰할만한 벤치마크(평가 기준)를 구하려면 상당한 주의가 필요합니다.
  • 좀 더 신뢰할만한 벤치마크 테스트를 만들려면 benchmark를 번갈아 가면서 여러 번 돌려야 합니다.

Date.parse와 문자열

  • 메서드 Date.parse(str)를 사용하면 문자열에서 날짜를 읽어올 수 있습니다.
  • 단, 문자열의 형식은 YYYY-MM-DDTHH:mm:ss.sssZ처럼 생겨야 합니다.
  • YYYY-MM-DD – 날짜(연-월-일)
  • "T" – 구분 기호로 쓰임
  • HH:mm:ss.sss – 시:분:초.밀리초
  • 'Z'(옵션) – +-hh:mm 형식의 시간대를 나타냄. Z 한 글자인 경우엔 UTC+0을 나타냄
  • YYYY-MM-DD, YYYY-MM, YYYY같이 더 짧은 문자열 형식도 가능합니다.
  • 위 조건을 만족하는 문자열을 대상으로 Date.parse(str)를 호출하면 문자열과 대응하는 날짜의 타임스탬프가 반환됩니다. 문자열의 형식이 조건에 맞지 않은 경우엔 NaN이 반환됩니다.


5.12 JSON과 메서드


요약

  • JSON은 독자적인 표준을 가진 데이터 형식으로, 대부분의 언어엔 JSON을 쉽게 다룰 수 있게 해주는 라이브러리가 있습니다.
  • JSON은 일반 객체, 배열, 문자열, 숫자, 불린값, null을 지원합니다.
  • JSON.stringify를 사용하면 원하는 값을 JSON으로 직렬화 할 수 있고, JSON.parse를 사용하면 JSON을 본래 값으로 역 직렬화 할 수 있습니다.
  • 위 두 메서드에 함수를 인수로 넘겨주면 원하는 값만 읽거나 쓰는 게 가능합니다.
  • JSON.stringify는 객체에 toJSON 메서드가 있으면 이를 자동으로 호출해줍니다.

JSON.stringify

  • JSON.stringify – 객체를 JSON으로 바꿔줍니다.
  • JSON.parse – JSON을 객체로 바꿔줍니다.
  • 이렇게 변경된 문자열은 JSON으로 인코딩된(JSON-encoded), 직렬화 처리된(serialized), 문자열로 변환된(stringified), 결집된(marshalled) 객체라고 부릅니다. 객체는 이렇게 문자열로 변환된 후에야 비로소 네트워크를 통해 전송하거나 저장소에 저장할 수 있습니다.
  • 문자열은 큰따옴표로 감싸야 합니다. JSON에선 작은따옴표나 백틱을 사용할 수 없습니다('John'이 "John"으로 변경된 것을 통해 이를 확인할 수 있습니다).
  • 객체 프로퍼티 이름은 큰따옴표로 감싸야 합니다(age:30이 "age":30으로 변한 것을 통해 이를 확인할 수 있습니다).
  • JSON.stringify 호출 시 무시되는 프로퍼티
    - 함수 프로퍼티 (메서드)
    - 심볼형 프로퍼- (키가 심볼인 프로퍼티)
    - 값이 undefined인 프로퍼티
  • JSON.stringify는 순환 참조가 있으면 원하는 대로 객체를 문자열로 바꾸는 게 불가능합니다.

replacer로 원하는 프로퍼티만 직렬화하기

  • let json = JSON.stringify(value[, replacer, space])
  • value : 인코딩 하려는 값
  • replacer : JSON으로 인코딩 하길 원하는 프로퍼티가 담긴 배열. 또는 매핑 함수 function(key, value)
  • space : 서식 변경 목적으로 사용할 공백 문자 수
let room = {
  number: 23
};

let meetup = {
  title: "Conference",
  participants: [{name: "John"}, {name: "Alice"}],
  place: room // meetup은 room을 참조합니다
};

room.occupiedBy = meetup; // room은 meetup을 참조합니다

alert( JSON.stringify(meetup, function replacer(key, value) {
  alert(`${key}: ${value}`);
  return (key == 'occupiedBy') ? undefined : value;
}));

/* replacer 함수에서 처리하는 키:값 쌍 목록
:             [object Object]
title:        Conference
participants: [object Object],[object Object]
0:            [object Object]
name:         John
1:            [object Object]
name:         Alice
place:        [object Object]
number:       23
*/

space로 가독성 높이기

  • JSON.stringify(value, replacer, space)의 세 번째 인수 space는 가독성을 높이기 위해 중간에 삽입해 줄 공백 문자 수를 나타냅니다.

커스텀 “toJSON”

  • toString을 사용해 객체를 문자형으로 변환시키는 것처럼, 객체에 toJSON이라는 메서드가 구현되어 있으면 객체를 JSON으로 바꿀 수 있을 겁니다. JSON.stringify는 이런 경우를 감지하고 toJSON을 자동으로 호출해줍니다.

JSON.parse

JSON.parse를 사용하면 JSON으로 인코딩된 객체를 다시 객체로 디코딩 할 수 있습니다.
let value = JSON.parse(str, [reviver]);

  • str: JSON 형식의 문자열
  • reviver: 모든 (key, value) 쌍을 대상으로 호출되는 function(key,value) 형태의 함수로 값을 변경시킬 수 있습니다.
  • JSON은 주석을 지원하지 않는다는 점도 기억해 놓으시기 바랍니다. 주석을 추가하면 유효하지 않은 형식이 됩니다.

reviver 사용하기

let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

let meetup = JSON.parse(str, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

alert( meetup.date.getDate() ); // 이제 제대로 동작하네요!



📚 참고 : javascript.info

좋은 웹페이지 즐겨찾기