문제 상황
JavaScript 개발을 하다 보면 두 개 이상의 배열을 특정 키 값을 기준으로 하나로 합쳐야 하는 경우가 자주 발생합니다. 예를 들어 설문조사 데이터를 처리할 때, 한 배열에는 응답자 정보가, 다른 배열에는 각 질문에 대한 선택지 정보가 담겨 있고, 이를 질문 번호를 기준으로 하나의 구조로 묶어야 할 수 있습니다.
다음과 같이 두 개의 객체 배열이 있다고 가정해 보겠습니다. 첫 번째 배열(arr1)은 여러 사람이 답변한 질문 정보를, 두 번째 배열(arr2)은 각 질문에 해당하는 선택지 정보를 담고 있습니다.
const arr1 = [
{ personalID: '11', questionNumber: '1', value: 'Something' },
{ personalID: '12', questionNumber: '2', value: 'whatever' },
{ personalID: '13', questionNumber: '3', value: 'anything' },
{ personalID: '14', questionNumber: '4', value: 'null' }
];
const arr2 = [
{ questionNumber: '2', chID: '111', cValue: 'red' },
{ questionNumber: '2', chID: '112', cValue: 'green' },
{ questionNumber: '2', chID: '113', cValue: 'blue' },
{ questionNumber: '3', chID: '114', cValue: 'yellow' },
{ questionNumber: '4', chID: '115', cValue: 'red' }
];
여기서 작성해야 할 함수는 두 배열의 데이터를 고유한 응답자(personalID)별로 그룹화하여, 각 질문과 그에 딸린 선택지(choice)를 하나의 객체로 표현하는 것입니다.
원하는 출력 결과
const output = [
{
personalID: '11',
questionNumber: '1',
value: 'Something'
},
{
personalID: '12',
questionNumber: '2',
value: 'whatever',
choice: [
{ questionNumber: '2', chID: '111', cValue: 'red' },
{ questionNumber: '2', chID: '112', cValue: 'green' },
{ questionNumber: '2', chID: '113', cValue: 'blue' }
]
},
{
personalID: '13',
questionNumber: '3',
value: 'anything',
choice: [
{ questionNumber: '3', chID: '114', cValue: 'yellow' }
]
},
{
personalID: '14',
questionNumber: '4',
value: 'null',
choice: [
{ questionNumber: '4', chID: '115', cValue: 'red' }
]
}
];
map()과 filter()를 활용한 해결 방법
가장 깔끔하고 가독성이 좋은 방법은 map()으로 arr1을 순회하면서, 각 객체의 questionNumber와 일치하는 항목만 filter()로 골라내어 새로운 choice 속성으로 추가하는 것입니다.
const mergeArray = (arr1 = [], arr2 = []) => {
return arr1.map(obj => {
const choice = arr2.filter(
item => item.questionNumber === obj.questionNumber
);
return choice.length ? { ...obj, choice } : { ...obj };
});
};
console.log(JSON.stringify(mergeArray(arr1, arr2), undefined, 4));
실행 결과
콘솔에는 아래와 같은 결과가 출력됩니다.
[
{
"personalID": "11",
"questionNumber": "1",
"value": "Something"
},
{
"personalID": "12",
"questionNumber": "2",
"value": "whatever",
"choice": [
{ "questionNumber": "2", "chID": "111", "cValue": "red" },
{ "questionNumber": "2", "chID": "112", "cValue": "green" },
{ "questionNumber": "2", "chID": "113", "cValue": "blue" }
]
},
{
"personalID": "13",
"questionNumber": "3",
"value": "anything",
"choice": [
{ "questionNumber": "3", "chID": "114", "cValue": "yellow" }
]
},
{
"personalID": "14",
"questionNumber": "4",
"value": "null",
"choice": [
{ "questionNumber": "4", "chID": "115", "cValue": "red" }
]
}
]
코드 동작 원리
- map():
arr1의 모든 요소를 순회하며 새로운 객체를 반환합니다. 원본 배열은 변경되지 않습니다. - filter():
arr2중에서 현재 객체의questionNumber와 일치하는 선택지만 추출합니다. - 스프레드 연산자(...): 기존 객체의 속성을 그대로 유지하면서
choice배열만 추가합니다.
대용량 데이터를 위한 성능 최적화
위 방식은 직관적이지만, 두 배열의 크기가 커질수록 시간 복잡도가 O(n×m)으로 증가해 성능이 저하될 수 있습니다. 데이터가 많다면 Map을 사용해 questionNumber별로 미리 그룹화한 뒤 병합하면 O(n+m)으로 개선할 수 있습니다.
const mergeArrayOptimized = (arr1 = [], arr2 = []) => {
const grouped = new Map();
arr2.forEach(item => {
const list = grouped.get(item.questionNumber) || [];
list.push(item);
grouped.set(item.questionNumber, list);
});
return arr1.map(obj => {
const choice = grouped.get(obj.questionNumber) || [];
return choice.length ? { ...obj, choice } : { ...obj };
});
};
마무리
두 배열을 특정 키 기준으로 병합할 때는 map()과 filter() 조합이 코드를 명확하고 안전하게 만들어 줍니다. 데이터 양이 많아지면 Map 기반 인덱싱으로 성능을 개선할 수 있으며, 무엇보다 원본 배열을 수정하지 않으면서 원하는 구조의 새로운 배열을 만드는 것이 핵심입니다.