이제는 모던 자바스크립트를 알아야지 - 객체, Map, Set 골라쓰기

황주현·2022년 3월 30일
1

들어가며

변수 선언은 단지 선언의 기능만을 하는게 아닌, 의도를 전달해야 한다.

내가 Javascript를 공부하며 깨달았던 중요한 사실 중 하나는 이거다.
이전 이제는 모던 자바스크립트를 알아야지 (let-const 편) 게시물에서도 한번 이야기 했다시피, 선언은 의도를 전달 할 수 있어야 한다.

이는 컬렉션을 만들때도 동일하게 적용된다.

모던 자바 스크립트에서는 기존의 Object(객체)이외에 Map(맵)Set(세트)라는 컬렉션이 추가됐다.

이번에는 이것이 무엇인지, 왜 추가 되었는지, 어떻게 사용해야 하는지에 대해 알아보도록 하자.



Key-Value(키-값)

배열이 있는데 객체는 뭐하러 쓰는거죠?? 🤔

Javascript에서 데이터를 저장 하는 방법은 정말 많다.
많이 사용되는 것 중 하나는 Array(배열)인데, 순서가 중요하지 않은 데이터의 경우 오히려 더 복잡해 질 수 있다.

말로하면 잘 이해가 안되니 예제를 보며 진행해보자.



객체는 왜 쓰나?

정적인 데이터일때는 객체를 쓰자

만약 가상의 (로그인, 로그아웃, 회원가입) 3가지 종류의 인증키를 배열로 저장해 놓은 코드가 있다고 가정해보자.

const keys = ['DORHNJ#!IRAFD', '123!@I#JNF', '!$OMJFDQFfvdsf'];

과연 개발자는 이 코드를 보고 123!@I#JNF가 어디에서 사용하는 인증키인지 알 수 있을까?

누구에게 물어보지 않는 이상 쉽지 않을 것이다.

그렇다고 123!@I#JNF 인증키가 들어가야 하는 곳에 !$OMJFDQFfvdsf를 넣는것도 안될 것이다.

이 처럼 이 데이터는 순서가 상관 없으며, 다른 값이 뭔지 상관 없고, 데이터가 교체되면 안된다는 3가지 특징을 가진다.

이 경우 Key-Value 중 객체를 사용하면 된다.

위 예제 코드를 객체로 바꾸어 표현하면 아래와 같다.

const keys = {
  login : 'DORHNJ#!IRAFD',
  logout : '123!@I#JNF',
  join : '!$OMJFDQFfvdsf'
};

이제 각 값을 얻으려면 몇 번째의 값이 어떤 값인지 찾아 볼 필요가 없다.
login 인증 키를 얻고 싶다면 keys.login 또는 keys['login'] 으로 값을 참조 할 수 있다.

console.log(keys.login);
// output : DORHNJ#!IRAFD

이 같은 방법은 별 거 아니어 보이지만, 훨신 직관적이고 사용하기가 쉽다.
때문에 거의 대부분의 개발 과정에서 객체를 가장 많이 사용한다.



그럼 다 객체를 쓰나?

ㅎㅎ 그럼 이제 뭐든지 객체로 만들면 되겠네요! 😊

결론부터 말하자면 된다.

하지만 그게 전부라면 이 게시글을 작성하지도 않았을 것이다.

조금 더 정확히 말하자면... 가능하긴 하다.
( 그 외에도 되긴된다, 할수야 있다, 추천하진 않는다 등이 있다...)

이전에 템플릿 리터럴에 대해 설명하며 아래와 같은 말을 한 적 있다.

문자열 조합에 정답은 없고, 여러 기능 중 상황을 고려해 가장 적합한 것을 사용해야 한다.

이는 사실 모든 방면에 해당 된다.

모든 정보를 객체로 만드는 것이 '안된다'라고 할 수 없으나,
경우에 따라 '더 좋은 방법이 있다'라고는 할 수 있다.

모던 자바 스크립트에서 MapSet이 탄생한 이유이다.



Map은 뭐지?

(이 Map 아님)


MDN에는 Map의 장점과, 컬렉션 선택을 객체로 할지 Map으로 할지에 대해 간단히 서술되어 있다.
자세한 내용을 서술하기에는 너무 길어서, 나중에 따로 포스팅 할 예정이다.

Map 의 장점
1. 객체의 Key는 strings지만, Map의 Key는 모든 값을 가질 수 있다.
2. 객체는 수동으로 크기를 추적해야 하지만, Map은 크기를 쉽게 알 수 있다.
3. Map은 삽입된 순서대로 반복된다.
4. 객체에는 prototype 이 있어 Map에 기본 키들이 있다.

객체 또는 Map을 결정하는 사항

  • Map : 실행 시 까지 키를 알 수 없고, 모든 키가 동일한 type이며 모든 값들이 동일한 type 일 때
  • 객체 : 각 개별 요소에 대해 적용해야 하는 로직이 있을 때

요약하자면 경우에 따라 객체만 사용하기엔 불편한 점이 있어서 이를 보완하고자 Map을 사용한다는 것이다.

이 내용을 기반으로 Map을 어떤 경우에 쓰는지 자세히 알아보자.



데이터 변경이 잦을 경우 Map을 사용하자

나는 예시 중 하나로 데이터 변경이 잦은 정보에 Map을 사용하는 이유에 대해 적어보려 한다.
실제 개발에서 '데이터 변경이 잦은'정보라 하면 뭐가 있을까?

대표적인 예로 쇼핑몰 등의 검색 사이트에서 필터링이 있다.
실제로 개발을 안해봤어도 필터링기능이 뭔지는 다들 알 것이다.

네이버 쇼핑을 예시로 한번 알아보자.


키보드를 찾고자 하는 모습이다.
하단을 보면 카테고리, 제조사, 키 방식 등등 많은 필터링 기능을 제공하는 것이 확인된다.

객체로 필터링 기능 구현

네이버 쇼핑의 검색 정보가 아래와 같다고 가정해보자.

const data = [
  {
    이름 : 'SPA-NKG1CU',
    카테고리 : '유선키보드',
    제조사 : '삼성전자',
    키방식 : '기계식',
    접점방식 : '청축',
  },
  {
    이름 : 'GK898B',
    카테고리 : '무선키보드',
    제조사 : '한성컴퓨터',
    키방식 : '무접점(광축)',
    접점방식 : '광축(클릭)',
  },
]

필터링할 내용을 저장 할 빈 객체를 하나 만든다.
그리고 '필터링 목록 추가', '필터링 목록 삭제', '필터링 목록 초기화' 기능을 만들어준다.

// filters 빈 객체 생성
let filters = {};
// 필터링 목록 추가
const addFilters = (filters, key, value) => {
  filters[key] = value;
};

// 필터링 목록 삭제
const removeFilters = (filters, key) => {
  delete filters[key];
};

// 필터링 목록 초기화
const clearFilters = (filters) => {
  filters = {};
  return filters;
}

완벽한데요! 객체를 써도 문제 없겠어요. 🤗

라고 생각했다면 모던 자바 스크립트에는 맞지 않다.
물론 기능 자체는 잘 돌아가지만 이상한 부분이 있다.

필터링 목록을 조작하는 추가, 삭제, 전체삭제 3가지 기능이 존재하는데 그 동작 방법이 모두 다르다는 것이다.

이게 무슨 말일까? 좀 더 자세히 알아보자.


  • 추가 : [key]를 이용
  • 삭제 : delete를 이용
  • 전체삭제 : 변수 재할당 (new Object())를 이용

이제 좀 보이는가?
이는 예측 불가하고 명확하지 않은 코드를 야기시킨다.

우리는 단순히 돌아가는 코드를 찍어내는게 중요한 게 아니다.
최대한 예측 가능한 코드를 만들어 개발 속도를 높이고, 읽는 시간을 줄여야 한다.

Map에서는 이를 보완하기 위해 전용 메서드가 존재하는 등 예측이 가능하도록 했다.
한번 Map을 이용해서 다시 개발해보자.



Map으로 필터링 구현

이번엔 Map으로 filters를 선언해보자.
Mapnew Map();으로 선언 할 수 있다.

let filters = new Map();


그리고 데이터를 추가하려면 set() 메서드를 사용한다.
인자값은 두 개가 전달되며, 순서대로 key, value이다.

filters.set('이름', 'SPA-NKG1CU');


체이닝을 사용하면 새 Map을 생성하고 바로 데이터를 추가할 수 있다.

let filters = new Map()
	.set('이름', 'SPA-NKG1CU')
	.set('카테고리', '유선키보드')
	.set('제조사', '삼성전자')
	.set('키방식', '기계식')
	.set('접점방식', '청축');

또는 배열을 통해 작성 또한 가능하다.

let filters = new Map(
	[
      ['이름', 'SPA-NKG1CU'],
      ['카테고리', '유선키보드'],
      ['제조사', '삼성전자'],
      ['키방식', '기계식'],
      ['접점방식', '청축'],
    ]
);


Map의 데이터를 key로 가져오려면 get() 메서드를 사용하면 된다.

filters.get('이름');



Map의 데이터를 제거하려면 delete() 메서드를 사용한다.

filters.delete('이름');
filters.get('이름');



마지막으로, Map의 모든 데이터를 삭제하려면 clear() 메서드를 사용한다.

filters.clear();
filters.get('이름');


자, 그럼 이제 위에서 만들었던 필터링 목록을 조작하는 추가, 삭제, 전체삭제 기능을 Map으로 만들어보자.

const filters = new Map();

// 필터링 목록 추가
const addFilters = (filters, key, value) => {
  filters.set(key,value);
};

// 필터링 목록 삭제
const removeFilters = (filters, key) => {
  filters.delete(key);
};

// 필터링 목록 초기화
const clearFilters = (filters) => {
  filters.clear();
}


훨신 깔끔해 진 모습이 보여진다.
이게 무슨 차이인가... 싶을 수도 있지만 실제 개발에서 이 같은 부분은 굉장히 큰 차이를 불러일으킨다.

Map에 대한 사용 방법은 더 방대하지만, 일단 여기서는 기본 메서드만 알아보았다.

어떤가? 이제 Set은 무엇일지 굉장히 기대되지 않는가.
바로 한번 알아보도록 하자.



Set이란?

Set은 각 고유 항목을 하나만 갖는 간단한 컬렉션이다.
백문이 불여일견, 한번 예제를 통해 알아보자.

const data = [
  {
    이름 : 'SPA-NKG1CU',
    카테고리 : '유선키보드',
    제조사 : '삼성전자',
    키방식 : '기계식',
    접점방식 : '청축',
  },
  {
    이름 : 'GK898B',
    카테고리 : '무선키보드',
    제조사 : '한성컴퓨터',
    키방식 : '무접점(광축)',
    접점방식 : '광축(클릭)',
  },
  {
    이름 : 'SKG-3000UB',
    카테고리 : '유선키보드',
    제조사 : '삼성전자',
    키방식 : '멤브레인',
    접점방식 : '멤브레인',
  },
];

아까 그 네이버 검색 예제가 다시 등장했다.
만약 이 데이터들의 제조사를 따로 수집하고 싶다면 어떻게 해야할까?

map() 메서드를 아는 사람들은 map()으로, 모른다면 단순 for반복문으로 가능할 것이다.
(위에서 알아본 Map과 다른 것이다.)

// map()을 아는 사람
data.map(value=>value['제조사']);
// map()을 모르는 사람
let newData = new Array();
for(let i = 0; i < data.length; i++) {
  newData.push(data[i].제조사);
}

실행결과 : ['삼성전자', '한성컴퓨터', '삼성전자']

실행결과는 위와 같다. 이게 우리가 원하던 실행 결과일까?
아니다. 삼성전자, 한성컴퓨터 두 개만 보여져야 할 것이다.

그럼 중복된 값들을 걸러내는 로직이 추가되어야 겠다.
for를 이용해 계속 작성해보자.


const getUnique = (element) => {
  const newArray = new Array();
  for(let i = 0; i < element.length; i++) {
    if(!newArray.includes(element[i])) {
      newArray.push(element[i]);
    }
  }
  return newArray;
}


원했던 결과가 나타났다.
하지만 이번에는 과정이 너무 복잡해보인다.

이때 Set을 이용하면 코드를 간단하게 바꿀 수 있다.



const 제조사 = ['삼성전자', '한성컴퓨터', '삼성전자'];
const newArray = new Set(제조사);

보다시피 어떠한 로직 없이 정리가 되었다.
그러나 한가지가 남았다. newArrayArray가 아닌 Set 형식이다.

이를 스프레드 연산자를 활용해 변경할 수 있다.


const getUnique = (element) => {
  return [...new Set(element)];
}

이제 그만...! 진짜 끝난거죠? 😥


이제 마지막이다.
위 코드는 매우 간결해졌지만, 우선적으로 data 배열을 모두 반복한 후 다시 분류하는 과정이 있다.

이 부분은 비효율적이므로 조금 더 최적화해보려 한다.

Set 또한 Map과 동일한 기능을 제공하는 add(), delete(), clear() 메서드를 제공한다.
그리고 중복이 없는 Set 특성 상 이미 존재하는 데이터를 add하면 무시한다.

우리는 이 특징을 이용할거다.

const uniqueCreater = (data) => {
  const unique = new Set();
  data.map((value) => {
  	unique.add(value.제조사);
  });
  return [...unique];
}

드디어 Set을 이용해 원하는 속성을 한번에 중복 없이 분류하는 것에 성공했다.
reduce()를 사용하면 이 코드에서 더 간단하게 하는 것도 가능하다.



정리

사실 MapSet 이외에 WeakMapWeakSet이라는 컬렉션도 존재한다.
이는 MDN에서 확인 가능하다.

이제 무조건 객체로 만드는 것이 아닌, 상황에 따라 Map, Set을 적절하게 활용해
모던 자바 스크립트에 한 걸음 더 다가가보자.


  • 모던 자바 스크립트에서는 객체 이외에 MapSet 컬렉션도 사용한다.
  • Map은 데이터의 변경이 자주 일어날 때 사용한다.
  • Set은 데이터의 중복값을 걸러내야 할 때 사용한다.

+ 읽어주셔서 감사합니다.
+ 오타, 내용 지적, 피드백을 환영합니다. 많이 해주실 수록 제 성장의 밑거름이 됩니다.
profile
반갑습니다. 프론트엔드 개발자 황주현 입니다. 🤗

1개의 댓글

comment-user-thumbnail
2024년 1월 19일

우연히 포스팅 보게 됫는 데, 정성이 들어간 글이네요
잘 읽었습니다!

  • 추가적으로 Map 이나 Set의 단점은 없을까요?
답글 달기