[모던JS: Core] 자료구조와 자료형 (2)

KG·2021년 5월 11일
0

모던JS

목록 보기
6/47
post-thumbnail

Intro

본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.

Iterable 객체

Iterable 객체는 반복 가능한 객체로, 배열을 일반화 한 객체이다. 이 개념을 사용하면 어떤 객체라도 for...of를 적용해 순회할 수 있다.

Iterable 객체는 앞서 다룬 Symbol 기능을 이용해 시스템 심볼 [Symbol.iterator] 프로퍼티가 정의되어 있다면 반복 가능한 객체로 인식한다. 자바스크립트에서 기본적으로 built-in되어있는 Iterable객체는 크게 다음과 같다.

  • 배열
  • 문자열
  • Map
  • Set
  • ...

1) Symbol.iterator

어떤 객체가 [Symbol.iterator] 프로퍼티를 가지고 있다면 해당 객체는 Iterable 하다. 즉 for...of를 통해 순회를 할 수 있다. 기본적으로 built-in 되어있는 위의 객체 외에도 개발자가 직접 해당 속성을 정의할 수 있다.

먼저 for...of의 매커니즘은 다음과 같다.

  1. for...of가 시작되면 Symbol.iterator를 찾아 호출. 없다면 에러 발생
  2. Symbol.iterator는 반드시 iterator객체를 반환 (메서드 next를 가지고 있는 객체)
  3. 이후 for...of는 반환된 iterator 객체만을 대상으로 동작
  4. 다음에 순회할 값이 있다면 for...ofiterator 객체의 next() 메서드 호출
  5. next() 메서드는 { done: Boolean, value: any } 형태의 값을 반환. done: true 일 경우엔 더 이상 순회할 값이 없음을 의미, 즉 반복 종료
let range = {
  from: 1,
  to: 5,
};

// range 객체에 Symbol.iterator 프로퍼티 지정
// Symbol.iterator 프로퍼티는 next 메서드를 가진 객체를 반환하는
// 일종의 함수
range[Symbol.iterator] = function () {
  return {
    current: this.from,
    last: this.to,
    
    // next 메서드의 반환 형태는 done과 value를 지정해야 함
    next() {
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  }
};


for(let num of range) {
  console.log(num);	// 1, 2, 3, 4, 5
}

또한 Symbol.iterator는 함수형 프로퍼티이기 때문에 앞서 객체 메서드에서 살펴본 바와 같이 단축 문법을 이용해 선언할 수도 있다.

let range = {
  from: 1,
  to: 5,
  
  // range 객체 자체를 반환
  // 반환 시 아래 정의된 next 메서드가 포함됨
  [Symbol.iterator]() {
    this.current = this.from;
    return this;
  },
  
  next() {
    if (this.current <= this.to) {
      return { done: false, value: this.current++ };
    } else {
      return { done: true };
    }
  }
};

for(let num of range) {
  console.log(num);	// 1, 2, 3, 4, 5
}

다만 for...of 반복을 하나의 Iterable 객체에 대해 동시에 적용할 수 없는 단점이 있다. 이는 두 반복문이 반복 상태를 공유하고 있기 때문이다. 그러나 동시에 두 개의 반복문을 처리하는 것은 비동기 로직에서도 흔한 케이스는 아니다.

2) Iterable 과 유사 배열

배열과 유사 배열은 서로 다르듯이, Iterable과 유사 배열 역시 다르다. (배열은 Iterable에 속하는 자료형이다)

  • Iterable : [Symbol.iterator]가 구현된 객체
  • 유사 배열 : 인덱스와 length 프로퍼티만 구현된 객체로 배열처럼 보이는 객체

Iterable과 유사 배열 모두 배열과 어딘가 닮아 보이지만 배열 자체는 아니다. 때문에 배열에서 제공하는 내장 메서드를 이용할 수 없다는 단점이 있다.

만약 이를 배열로 변환하고 싶다면 Array.from() 메서드를 이용할 수 있다. 이때 해당 메서드의 인자로 들어갈 수 있는 자료형은 Iterable 이거나 유사 배열이어야 한다.

// arrayLike는 인덱스와 length만 가진 유사 배열 객체
let arrayLike = {
  0: 'hello',
  1: 'world',
  length: 2,
};

let realArr = Array.from(arrayLike);

console.log(realArr);	// ['hello', 'world'];
// 실제 배열로 변환, 관련 배열 메서드 사용 가능

Map & Set

맵과 셋은 ES6(ES2015)에서 추가된 자료구조형이다. 두 자료구조 모두 Iterable 객체이다.

1) 맵(Map)

맵은 키가 있는 데이터를 저장하는 자료구조이며, 때문에 기존에 살펴본 객체와 유사하다. 가장 큰 차이점은 객체의 경우엔 프로퍼티의 키 값이 문자열 또는 symbol만 가능했지만, 맵의 경우엔 자료형 제약이 없다. 즉 문자열, 숫자, 불린, 심지어 객체 자체 등 다양한 값을 키 값으로 활용할 수 있다.

맵은 객체와 그 쓰임이 유사하기 때문에 마치 객체 처럼 프로퍼티에 접근은 할 수 있다.

const map = new Map();
const key = 'someKey name';

map[key] = 2;		// 문법적으로 가능, 권장(X)
map.set(key, 2);	// 전용 메서드 사용, 권장(O)

그러나 이와 같은 방식은 맵을 일반 객체처럼 취급하기 때문에 맵을 사용하는 이점이 사라진다. 따라서 전용 메서드인 set, get을 이용해 접근하는 것을 권고한다.

map.set을 호출할 때마다 맵 자신이 반환된다. 따라서 다음과 같이 체이닝이 가능하다.

map.set(1, 1).set(2, 2).set(3, 3).set(4, 4);

맵의 전용 메서드를 이용해 맵 각 요소에 대한 반복 작업을 수행할 수 있다.

  1. map.keys() : 각 요소의 키만 모은 Itreable객체 반환
  2. map.values() : 각 요소의 값만 모은 Itreable 객체 반환
  3. map.entries() : 각 요소의 [키, 값]을 한 쌍으로 하는 Iterable 객체 반환

이들은 Iterable 객체이기 때문에 for...of를 통해 순회할 수 있다.

for(const key of map.keys()) {
  console.log(key);
}

for(const value of map.values()) {
  console.log(value);
}

for(const entry of map.entries()) {
  console.log(entry);
}

맵 생성자를 이용해 초기화 용도로 새로운 맵을 만드는 경우엔 [키, 값] 쌍을 가진 배열이나 Iterable 객체를 전달할 수 있다.

const map = new Map([
  [1, 1],
  [2, 2],
  [3, 3],
]);

이때 일반 객체의 경우엔 Iterable 객체가 아니기 때문에 객체를 맵으로 변환하기 위해서는 Object.entries(obj) 메서드를 활용할 수 있다. 해당 메서드는 객체의 키-값 쌍을 요소([key, value])로 가지는 배열을 반환한다.

const obj = {
  name: 'John',
  age: 20,
};

const map = new Map(Object.entries(obj));

2) 셋(Set)

셋은 중복을 허용하지 않는 컬렉션으로 수학에서의 집합과 유사하다. 특히 배열의 중복값을 제거할 때 Set으로 변환하면 손쉽게 중복값을 제거할 수 있다.

셋 역시 맵과 유사하게 반복 작업을 처리하기 위한 메서드를 지원한다.

  1. set.keys() : 셋 내의 모든 값을 포함하는 Itreable객체 반환
  2. set.values() : set.keys()와 동일. 맵과의 호환성을 위해 구현
  3. set.entries() : 각 요소의 [value, value]을 한 쌍으로 하는 Iterable 객체 반환. 역시 맵과의 호환을 위해 구현

맵과 셋은 모두 요소 또는 값을 추가한 순서대로 반복 작업이 보장된다. 이 역시 객체가 순서를 보장하지 않는다는 점과 구분된다. 그러나 두 컬렉션은 배열처럼 다시 정렬을 하거나 인덱스를 이용해 특정 요소 또는 값을 가지고 오는 작업은 불가하다.

WeakMap & WeakSet

위크맵(WeakMap)과 위크셋(WeakSet)은 주로 메모리 최적화와 관련되어 사용된다. 기본적인 개념은 위에서 다룬 맵, 셋과 동일하다.

앞서 가비지 컬렉션 포스트를 참고하면 자바스크립트의 엔진은 도달 가능한 값만 메모리에 유지하고 있다. 따라서 선언된 객체의 참조값의 연결을 끊어버리면 해당 객체는 메모리에서 삭제된다.

let obj = { name: 'KG' };	// obj에는 참조값 할당

obj = null;	// 메모리에서 obj 객체가 삭제

이때 자료구조를 구성하는 요소는 자신이 속한 자료구조가 메모리에 남아있는 한 대게 도달 가능한 값으로 취급되어 메모리에서 삭제되지 않는다. 즉 객체의 프로퍼티, 배열의 요소, 맵이나 셋을 구성하는 요소가 객체인 경우, 해당 객체의 참조가 끊어지더라도 상위 자료구조가 메모리에 남아있는 한 해당 객체는 메모리에 계속 남아있게 된다.

let obj = { name: 'KG' };
let arr = [ obj ];

obj = null;	// obj 객체 참조값 연결을 끊음

// 그러나 arr 배열이 아직 메모리에 할당되어 있기에
// obj 는 가비지 컬렉터의 대상이 되지 않아 제거되지 않는다.

하지만 위크맵과 위크셋을 사용하면 위와 같은 문제를 해결할 수 있다.

1) 위크맵(WeakMap)

맵과 위크맵의 가장 큰 차이점은 맵과 달리 위크맵의 키는 반드시 객체여야 한다는 점이다. 원시값은 위크맵의 키가 될 수 없다.

위크맵의 키로 사용된 객체가 도중에 참조값이 사라지는 경우에는 가비지 컬렉터의 대상이 되어 자동으로 메모리와 위크맵에서 제거된다.

let obj = { name: 'KG' };
let weakMap = new WeakMap();
weakMap.set(obj, 'value');

obj = null;	// obj 객체 참조값 연결 해제

// obj 객체는 메모리와 위크맵에서 제거됨

따라서 위크맵은 부차적인 데이터를 저장하고, 해당 데이터가 유효한 경우에만 메모리에 적재되는 경우라던가 캐싱 과정에서 유용하게 사용할 수 있다.

2) 위크셋(WeakSet)

위크셋 역시 기본적인 매커니즘인 위크맵과 동일하다. 그리고 기본 컨셉은 셋과 유사하다. 따라서 위크셋의 요소는 객체만 저장할 수 있으며 원시값은 저장할 수 없고, 셋 안의 객체는 도달 가능할 때만 메모리에 유지되고 참조값 연결이 끊기면 가비지 컬렉터에 의해 자동으로 위크셋과 메모리에서 제거된다.

따라서 위크셋 역시 부차적인 데이터 저장에 유용하게 사용할 수 있다. 다만 위크맵과 달리 복잡한 데이터 저장 보다는 단순한 체크 용도의 데이터를 저장하는데 좀 더 유용하다.

위크맵과 위크셋의 사용 예시에 대한 코드는 다음 링크를 참조하자.

3) 한계

그러나 위크맵과 위크셋은 일반적인 맵, 셋과 달리 반복 작업이 불가능하다. 메모리 최적화를 위해 기본 맵과 셋에서 제공되는 반복 관련 메서드를 하나도 제공하지 않는다. 따라서 위크맵과 위크셋에 저장된 자료를 한 번에 얻어오는 작업은 불가능하다. 따라서 본인이 필요한 메인 기능을 잘 구분하여 적절한 자료형을 사용함이 필요하다.

객체 순회

내장 Iterable 객체의 경우엔 keys(), values() 그리고 entries() 메서드를 통해 순회가 가능하다 (문자열 제외)

  • Array
  • Map
  • Set

이때 일반 객체에도 이와 유사하게 순회 관련 메서드가 제공되지만 그 문법이 다르다.

  • Object.keys(obj) : 객체의 키만 담은 배열 반환
  • Object.values(obj) : 객체의 값만 담은 배열 반환
  • Object.entries(obj) : [키, 값] 쌍을 담은 배열 반환

주요 차이점은 Iterable 객체에서의 순회 메서드는 반환 값이 다시 Iterable 객체인 반면, 일반 객체의 순회 메서드는 진짜 배열을 반환한다는 점이다.

이처럼 문법이 다른 이유는 유연성을 보장하기 위해서다. 자바스크립트에서는 복잡한 자료형이 대개 객체에 기반한다. 이때 객체 자체에 메서드로 keys() 라던가 values() 라는 메서드가 이미 선언되어 있을 수 있다. 따라서 Iterable 객체처럼 접근하는 경우엔 기존에 선언된 메서드의 동작을 덮어쓸 수 있기 때문에 Object.values(obj) 같은 형태로 호출하는 것이다.

구조분해할당

배열과 객체는 구조분해할당 기법을 이용해 조금 더 쉽게 요소에 접근할 수 있다.

1) 배열 구조분해할당

배열은 구조분해할당에서 순서가 중요하다. 입력 순서가 보장되고 재정렬 또한 가능한 자료형이기 때문에 구조분해할당에서도 역시 순서에 유의해야 한다.

const arr = ['hello', 'world'];

const [first, second] = arr;

console.log(first);	// 'hello'
console.log(second);	// 'world'

이때 할당연산자 우측에는 배열뿐만 아니라 반복가능한 모든 Iterable 객체가 가능하다.

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

또한 필요없는 값은 쉼표로 구분하여 무시할 수 있다.

const [a, , c] = "abc";
console.log(a);	// "a"
console.log(c);	// "c"

할당할 값이 없는 경우라면 할당연산자 =을 이용해 기본값을 지정해줄 수 있다.

const [a, b, c=100] = [1, 10];
console.log(a, b, c);	// 1, 10, 100

구조분해할당 기법을 이용해 변수 교환 트릭을 이용할 수 있다.

let guest = 'guest';
let admin = 'admin';

[guest, admin] = [admin, guest];

console.log(guest, admin);	// "admin guest"

2) 객체 구조분해할당

배열과 마찬가지로 객체 역시 동일하게 구조분해할당이 가능하다. 객체는 배열과 달리 중괄호({})를 통해 구조분해할당을 한다. 또한 객체는 배열과 달리 순서가 보장되지 않기 때문에 순서에 유의할 필요가 없다.

const options = {
  title: 'hi',
  width: 100,
  height: 200,
};

const { title, width, height } = options;
const { width, title, height } = options;  // 순서 상관 X

또한 프로퍼티와 다른 이름으로 할당을 지정할 수 있다.

const { title: t, width: w, height: h } = options;

console.log(t);	// 'hi'
console.log(w);	// 100
console.log(h);	// 200

배열과 동일하게 기본값 할당이 가능하다.

const { title, width, height, content = 'world' } = options;

3) 나머지 패턴(...)

배열과 객체 구조분해할당 모두 스프레드 연산자(...)를 이용해 나머지 값을 할당할 수 있다.

const arr = ['a', 'b', 'c', 'd', 'e'];
const obj = {
  name: 'kg',
  age: 20,
  status: 'student',
  nation: 'korea'
};

const [a, b, ...rest] = arr;
console.log(rest);	// ['c', 'd', 'e'];

const {name, age, ...rest} = obj;
console.log(rest);	// { status: 'student, nation: 'korea' }

이때 rest는 어떠한 변수명도 가능하다.

4) 함수 매개변수

함수의 매개변수에도 구조분해할당을 적용할 수 있다. 매개변수가 많고, 각 매개변수가 필수가 아닌 선택적인 경우 유용하다. 함수의 매개변수는 선언된 순서에 맞게 인자를 넘겨주어야 하는데, 위에서 보았듯이 객체 구조분해할당에는 이 순서가 상관없기 때문이다. 이는 React 컴포넌트에서 상위 컴포넌트의 props를 받아올 때도 많이 사용하는 패턴이다.

let options = {
  title: 'my title',
  items: [1, 2, 3, 4, 5];
};

function myFunc({title, items}) {
  console.log(title);
  console.log(items);
}

myFunc(options);

Date 객체

Date는 날짜 저장 및 날짜 관련 메서드를 제공해주는 자바스크립트의 내장 객체이다. Date 객체를 활용해 시간 측정 및 날짜 출력 또는 계산을 수행할 수 있다.

1) 객체생성

new Date() 생성자 패턴으로 새로운 Date 객체를 생성할 수 있다. 이때 인수는 다양한 값이 가능하다.

  1. new Date()
    • 현재 날짜와 시간이 저장된 Date 객체 반환
  2. new Date(milliseconds)
    • UTC기준 1970년 1월 1일 0시 0분 0초에서 milliseconds 밀리초 이후 시점의 Date 객체 반환
  3. new Date(datestring)
    • 문자열은 자동으로 구문 분석되어 전달된다.
    • 단, 문자열의 형식은 YYYY-MM-DDTHH:mm:ss.sssZ처럼 생겨야 한다.
  4. new Date(year, month, date, hours, minutes, seconds, ms)
    • 주어진 인수를 조합한 Date 객체 반환
    • 첫번째와 두번째 인수만 필수값

2) 자동고침 (autocorrection)

Date 객체는 자동고침 기능을 제공한다. 범위를 벗어나는 값을 설정 시 이를 자동 고침하여 날짜를 반영한다. 즉 입력받은 날짜 구성 요소가 범위를 벗어난다면 초과분은 자동으로 다른 날짜 구성요소에 배분된다.

let date = new Date(2016, 1, 28);
date.setDate(date.getDate() + 2);

console.log( date ); // 2016년 3월 1일

3) Date.now()

Date.now() 메서드는 별도로 Date 객체를 만들지 않고 현재 타임스탬프를 반환한다. 따라서 Date 객체를 직접 생성 후 현재 시간을 구하는 방법보다 효율적이고 또한 가비지 컬렉터의 부담 역시 줄여준다는 장점이 있다.

// 두 방식은 의미적으로는 동일하지만
// 전자의 방식이 더 빠르다.
const now = Date.now();
const cur = new Date.getTime();

그 외 자세한 Date 객체의 설명과 메서드에 대한 부분은 MDN 문서를 참고하자.

JSON

JSON(Javascript Object Notation)은 값이나 객체를 나타내는 범용 포맷으로 앞에서 깊은 복사(deep copy)에 대해 다룰때 잠깐 언급되었다.

JSON은 본래 자바스크립트에서 복잡한 객체를 다루고 있을 때 이를 네트워크를 통해 서버 등으로 보내야할 때 이를 문자열로 변환해서 전송하기 위한 데이터 교환 목적으로 창안되었다. 자바스크립트에서 사용할 목적으로 만들어졌긴 하지만 다른 언어에서도 해당 포맷을 다룰 수 있기 때문에 JSON은 오늘날 데이터 교환 목적으로 사용하는 경우가 많다. 특히 클라이언트 측 언어가 자바스크립트인 경우 매우 유용하고, 서버 측 언어에는 큰 제약을 받지 않는다.

자바스크립트에서는 다음과 같은 JSON 관련 메서드를 지원한다.

  • JSON.stringify : 객체를 JSON 포맷으로 직렬화
  • JSON.parse : JSON 포맷을 객체로 역 직렬화

이때 직렬화 과정은 객체의 중첩구조까지 모두 변환이 가능하다. 다만 다음의 경우 실패할 수 있으므로 이를 유의해야 한다.

  1. JSON 명세에 적용되지 않는 자바스크립트 자료형
    • 객체, 배열, 원시형은 모두 지원
    • 함수형, 심볼형, undefined는 지원하지 않음
  2. 순환 참조 객체는 변환 불가

1번의 이슈때문에 JSON.stringify를 통한 완전한 깊은 복사는 불가능하다. 2번의 경우는 변환 과정에서 무한 루프가 발생하기 때문에 변환이 불가하다.

두 메서드는 모두 함수를 추가 인수로 전달할 수 있는데 이 경우 원하는 값만 매핑할 수 있도록 설정할 수 있다.

let room = {
  number: 23
};

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

room.occupiedBy = meetup;

// room.occupiedBy는 meetup을 참조하고
// meetup.place는 room을 참조하기 때문에 순환참조이다.
// 이는 JSON으로 변환이 불가하기에 둘 중 하나의 값(occupiedBy)을
// 변환하지 않도록 매핑함수를 통해 지정
JSON.stringify(meetup, function replacer(key, value) {
  alert(`${key}: ${value}`);
  return (key == 'occupiedBy') ? undefined : value;
});

// 현재 date는 문자열 값을 가지고 있다.
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

// 문자열로 전달된 date 값을 Date 객체로 변환하고 싶다면
// 다음과 같이 매핑함수를 지정하여 변환할 수 있다.
let meetup = JSON.parse(str, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

References

  1. https://ko.javascript.info/data-types
  2. https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Iteration_protocols
profile
개발잘하고싶다

0개의 댓글