[JS] 배열 내 객체 핸들링하기

Yunhye Park·2025년 1월 31일
0

Back To Basic

목록 보기
10/10
post-thumbnail

많은 데이터를 한번에 받아오다보면 데이터 구조가 자연히 복잡해진다. 그러다보면 중첩된 객체에서 특정 키만 기깔나게 핸들링 해야할 때를 만나게 된다. 겹겹인 모양새를 보면 약간 아득해지곤 했는데 사실 그렇게 복잡할 게 없다.

배열 내 중첩객체 중복 key 제거

데이터를 그냥 data로 받아들이면 영 재미가 없어서 적당히 몰입할 상황을 만들어봤다.

Scenario

연차를 사용한 날짜가 담긴 offDates에 실수로 같은 날짜가 두번 들어갔다. 소중한 연차가 날아갈 지경에 이르렀으니, 중복된 날짜를 제거해보자.

const offDates = [
  { id: 1, date: '2024-09-01'}, 
  { id: 2, date: '2024-09-01'}, // <-- 제거
  { id: 3, date: '2024-11-21'}
]

우선 배열을 다루는 것이니 filter를 써볼 생각이 가장 먼저 든다.

Approach 1. filterfindIndex

filter: 주어진 배열을 순회하여 조건에 맞는 요소만 골라낸 새 배열을 반환한다.

offDates.filter((element, index, array) => 
	// 필터 조건
)
  • element: 배열의 각 요소
  • index: 각 요소의 인덱스
  • array: filter 메서드를 적용한 배열 (여기서는 offDates)

배열 요소의 순회에 그칠 게 아니라, 객체의 key까지 접근해야 한다. 두번째 인자로 index가 주어졌으니 중복된 키 값이 있는 데이터의 인덱스를 구하면 좋겠다.

findIndex: 배열을 순회하며 조건을 만족하는 첫번째 값의 인덱스를 반환한다.
cf. find는 값을 반환

현재 순회 중인 요소의 특정 키(date)와 원본 배열의 키가 동일한 요소의 인덱스를 구하고, 이를 현재 순회 중인 요소의 인덱스와 비교한다.

const result = offDates.filter((element, index, array) => (
  (index) === array.findIndex(data => data.date === element.date))
);

console.log(result); // [{ id: 1, date: '2024-09-01'}, { id: 3, date: '2024-11-21'}]

여기에 함정이 있다. 배열 안에서 배열을 순회한다. 고로 위 연산은 이중 순회 중이다. O(n²)의 시간복잡도를 갖기 때문에 데이터 수가 많아질수록 곱절로 복잡도가 높아진다.

그럼 배열을 한번만 순회하는 방법엔 뭐가 있을까?

Approach 2. 중복 없는 값 컬렉션 Set

자바스크립트 내장 객체 중 하나인 Set. new 연산자로 생성자 함수를 호출하여 addhas 등의 메서드를 활용해 값을 핸들링한다. 삽입 순서대로 순회하며 중복 값은 허용하지 않는다.

이때 자바스크립트의 객체참조 타입이라는 점을 잊지 말자!

원시형이든 참조형이든 엄밀히 말하면 값이 담긴 메모리주소를 가리키는 건 동일하다. 하지만 편의상 원시형은 값이 담기고, 참조형은 값을 참조하는 주소가 담긴다고 표현한다.

const a = 3처럼 원시적인 데이터는 주소값을 참조하면, 바로 a의 값인 3을 만난다. 참조형은 어떤가? const obj = {name: 'Cait', age: 30}는 각 key와 value가 담긴 메모리주소가 별도로 존재한다. 고로 변수 obj의 참조 주소를 타고 들어가면, 특정 값이 아닌 내부 참조 주소를 만난다.

위의 설명을 비유적으로 도식화하면 아래처럼 표현할 수 있겠다.

Set 객체는 값만 저장하기 때문에 위의 예시로 든 일반 객체와 차이가 있지만, 객체 타입인 건 근본적으로 같아서 마찬가지로 참조주소가 저장된다.

이렇듯 객체는 참조 주소로 그 값을 인식하므로, 객체 간의 비교는 문자열 비교에서 으레하는 일치 연산자(===)로는 사람이 원하는 대로 의도하지 않는다. 다른 말로 하자면, 객체를 주소가 아닌 값 즉 원시형으로 바꿔주면 될 일이다. 객체를 문자열로 바꿔주는 JSON.stringfy()를 사용하면 되겠다.

const offDatesSet = new Set();

const result = offDates.filter(element => {
  const el = JSON.stringify(element);
  if (offDatesSet.has(el)) {
    return false;
  }
  offDatesSet.add(el);
  return true;
});

console.log(result); // [{ id: 1, date: '2024-09-01'}, { id: 3, date: '2024-11-21'}]

문자열로 바뀐 객체가 Set 객체에 존재하면 false를 반환하여 새 배열에서 제외한다. 중복이 아닌 요소만 모아 result가 완성된다.

지금은 두개의 key 모두를 반환해야 해서 직렬화 과정이 추가된 것이라, 특정 키(ex. date)만 필요했다면 has 메서드에 곧장 값을 넣는 것만으로 중복 체크가 충분했을 거다.

Approach 3. 키-값 쌍의 Map 객체

Set처럼 순서가 있는 자바스크립트 내장 객체, Map도 있다. 다른 점이 있다면 키-값 쌍으로 저장한다는 거다.

const offDatesMap = new Map();

const result = offDates.filter(element => {
  if (offDatesMap.has(element.date)) {
    return false;
  }
  offDatesMap.set(element.date, true);
  return true;
});

console.log(result); // [{ id: 1, date: '2024-09-01'}, { id: 3, date: '2024-11-21'}]

전체 흐름은 Set과 거의 동일하다. 하지만 set을 할 때 키와 값 모두를 입력해 줘야 해서 element.date를 key로 넣고, 임의로 true를 값으로 넣어주었다.

Approach 4. reducefind

reduce를 처음 접했을 때만 해도 누적 연산 용도로 알고 있었다. 특히 '누산기'라는 단어 때문인지 사칙연산 외의 사용범위가 아리송했다. 그런데 '배열을 순회하며 값을 누적(추가)한다'는 점이 여느 배열 메서드와 다르지 않다.

offDates.reduce((acc, cur, idx, src) => {
	// 누산 연산
}, '초기값 또는 형태')
  • acc: 누산기
  • cur: 현재 요소 (배열의 각 요소)
  • idx: 현재 인덱스 (각 요소의 인덱스)
  • src: 원본 배열

가만보면 배열 메서드의 매개변수가 다 거기서 거기다. 다만 reduce에는 '누산기'라는, 입맛대로 데이터 만들기 좋은 요소가 존재한다. 콜백함수 다음 인자는 반환 형태를 결정할 초기 세팅값이 누산기(acc)의 초기 상태이다. 만약 []이라면 빈 배열을 시작으로 누산이 이루어진다.

세번째 매개변수인 idx를 활용해 앞선 방식대로 findIndex를 쓸 수도 있다. 하지만 여기서는 누산기와 현재 요소, 두가지만으로 충분히 핸들링 가능하니 조건에 맞는 첫번째 요소를 반환하는 find 메서드를 써봤다.

const result = offDates.reduce((acc, cur) => {
	if (!acc.find((el) => el.date === cur.date)) {
		acc.push(cur);
	}
	return acc;
}, []);

console.log(result); // [{ id: 1, date: '2024-09-01'}, { id: 3, date: '2024-11-21'}]

배열을 순회하며 date 키 값이 같은 요소가 없을 때에만 누산기에 현재 요소를 추가한다. 누산기가 있다는 게 이 메서드의 특징인 만큼 연산의 결과로서 대개 하나의 변수(acc)를 반환한다.

배열 내 객체의 특정 key만 필요할 때

이번엔 특정 key 혹은 key들만 콕콕 집고 싶을 때 유용하다.

const characters = [
  {nickname: 'VI', name: 'Violet', age: 24},
  {nickname: 'Cait', name: 'Caitlyn', age: 22}
];

const names = characters.map(({ nickname, name }) => ({ nickname, name }));

console.log(names); // [{ nickname: 'VI', name: 'Violet' }, { nickname: 'Cait', name: 'Caitlyn' }]

여기서 어처구니 없는 실수 하나를 첨언하자면.

데이터 있는데 undefined?

소괄호(())와 중괄호({})를 겹쳐가며 코드를 작성할 때 가끔 undefined를 만나곤 한다.

값이 있는 데이터를 가공한 건데 왜 타입 에러가 나는가 하면, 겉을 중괄호로 감싸고서 return을 안 해서다. 함수는 본디 반환값이 있어야 하고, 화살표함수로 표현할 땐 중괄호와 return문 대신 소괄호로 묶으면 return을 생략할 수 있었을 뿐이다.

또, 객체 구조 분해로 꺼내온 값들을 묶어주지 않았을 때에도 오류를 만날 수 있다. 위의 예시에서는 characters에서 nicknamename을 객체 구조 분해 할당으로 꺼내서 콜백함수 인자로 넘겨주는데, 이때 ()로 묶어주지 않으면 함수의 매개변수로 인식하질 못하고 본문으로 받아들인다.


참고

📚 코어 자바스크립트 - 1. 데이터타입
📄 MDN 문서

profile
일단 해보는 편

0개의 댓글

관련 채용 정보