다음과 같은 객체가 있다고 가정해 보겠습니다.
const obj = {
"part1": [{"id": 1, "a": 50},{"id": 2, "a": 55},{"id": 4, "a": 100}],
"part2":[{"id": 1, "b": 40}, {"id": 3, "b": 45}, {"id": 4, "b": 110}]
};
여기서 필요한 것은 이 객체를 입력으로 받아 part1과 part2 배열을 id 값을 기준으로 병합하여, 아래와 같이 하나의 객체 배열을 만들어내는 JavaScript 함수입니다.
const output = [
{"id": 1, "a": 50, "b": 40},
{"id": 2, "a": 55},
{"id": 3, "b": 45},
{"id": 4, "a": 100, "b": 110}
];
즉, 두 배열에 모두 존재하는 id(1, 4)는 속성이 합쳐지고, 한쪽에만 있는 id(2, 3)는 그대로 유지됩니다.
예제 코드
이를 구현한 전체 코드는 다음과 같습니다.
const obj = {
"part1": [{"id": 1, "a": 50},{"id": 2, "a": 55},{"id": 4, "a": 100}],
"part2":[{"id": 1, "b": 40}, {"id": 3, "b": 45}, {"id": 4, "b": 110}]
};
const mergeObject = (obj = {}) => {
let result = [];
result = Object.keys(obj).reduce(function (hash) {
return function (r, k) {
obj[k].forEach(function (o) {
if (!hash[o.id]) {
hash[o.id] = {};
r.push(hash[o.id]);
}
Object.keys(o).forEach(function (l) {
hash[o.id][l] = o[l];
});
});
return r;
};
}(Object.create(null)), []).sort((a, b) => {
return a['id'] - b['id'];
});
return result;
};
console.log(mergeObject(obj));
코드 동작 원리
- 해시 맵 준비: Object.create(null)로 프로토타입 체인이 없는 빈 객체를 만들어 id를 키로 사용하는 해시 맵 역할을 부여합니다. 이렇게 하면 내장 속성과의 충돌 없이 안전하게 키를 관리할 수 있습니다.
- 순회하며 병합: Object.keys(obj)로 part1, part2를 차례대로 순회합니다. 각 객체의 id가 해시 맵에 없으면 새 객체를 만들어 결과 배열(r)에 추가하고, 이미 존재하면 기존 객체에 나머지 속성을 덮어써 자연스럽게 병합됩니다.
- 정렬: 마지막에 sort()를 이용해 id 기준 오름차순으로 정렬하여 일관된 출력 순서를 보장합니다.
이 방식은 각 요소를 한 번씩만 순회하므로 사실상 O(n) 수준의 성능을 보이며, 두 배열의 길이가 서로 달라도 문제없이 동작합니다.
대안: ES6 Map 활용하기
ES6의 Map을 사용하면 같은 로직을 더 직관적으로 표현할 수 있습니다.
const mergeObject = (obj = {}) => {
const map = new Map();
Object.values(obj).flat().forEach(item => {
if (map.has(item.id)) {
Object.assign(map.get(item.id), item);
} else {
map.set(item.id, { ...item });
}
});
return [...map.values()].sort((a, b) => a.id - b.id);
};
console.log(mergeObject(obj));
Map은 삽입 순서를 유지하고 키 조회 성능이 뛰어나므로, 최신 JavaScript 환경이라면 이 방법을 권장합니다.
출력 결과
두 코드 모두 실행하면 콘솔에 다음과 같은 결과가 출력됩니다.
[
{ id: 1, a: 50, b: 40 },
{ id: 2, a: 55 },
{ id: 3, b: 45 },
{ id: 4, a: 100, b: 110 }
]