배열 안에 배열이 중첩된 형태의 데이터를 다루다 보면, 이를 사람이 읽기 쉬운 객체(object) 형태로 바꿔야 하는 경우가 자주 있습니다. 이번 글에서는 키-값 쌍으로 이루어진 중첩 배열을 객체 배열로 변환하는 방법을 알아보겠습니다.
문제 상황
다음과 같이 배열 안에 배열이 중첩된 데이터가 있다고 가정해 보겠습니다.
const arr = [
[
['firstName', 'Joe'],
['lastName', 'Blow'],
['age', 42],
['role', 'clerk'],
[
['firstName', 'Mary'],
['lastName', 'Jenkins'],
['age', 36],
['role', 'manager']
]
]
];
각 요소는 [키, 값] 형태의 쌍이며, 그중 하나는 또 다른 쌍들의 배열, 즉 새로운 사용자 정보로 되어 있습니다. 우리가 작성할 함수는 이런 배열을 입력받아 고유한 사용자마다 하나의 객체를 갖는 배열을 반환해야 합니다.
따라서 기대하는 출력 결과는 다음과 같습니다.
const output = [
{ firstName: 'Joe', lastName: 'Blow', age: 42, role: 'clerk' },
{ firstName: 'Mary', lastName: 'Jenkins', age: 36, role: 'manager' }
];
재귀 함수를 이용한 해결 방법
배열의 중첩 깊이가 정해져 있지 않으므로 재귀(recursion)로 접근하는 것이 가장 안전합니다. 요소의 첫 번째 값이 문자열이면 키-값 쌍으로 처리하고, 배열이면 새로운 사용자 그룹으로 판단해 재귀 호출하는 방식입니다.
const convertToObject = (data) => {
const result = [];
let current = {};
// 지금까지 모은 속성으로 객체를 완성해 결과에 추가
const flush = () => {
if (Object.keys(current).length) {
result.push(current);
current = {};
}
};
const build = (items) => {
items.forEach((item) => {
if (Array.isArray(item[0])) {
// 중첩 배열을 만나면 현재 객체를 먼저 저장한 뒤
flush();
// 새로운 사용자로 처리하기 위해 재귀 호출
build(item);
} else {
// 일반적인 [키, 값] 쌍인 경우
current[item[0]] = item[1];
}
});
flush();
};
build(data);
return result;
};
console.log(convertToObject(arr));
출력 결과
[
{ firstName: 'Joe', lastName: 'Blow', age: 42, role: 'clerk' },
{ firstName: 'Mary', lastName: 'Jenkins', age: 36, role: 'manager' }
]
코드 동작 원리
핵심 로직은 다음과 같습니다.
- current 객체: 배열을 순회하면서 키-값 쌍을 하나씩 모아 두는 임시 객체입니다.
- flush() 함수: current에 담긴 속성이 있으면 result에 저장한 뒤 초기화합니다.
- 중첩 배열 판별: Array.isArray(item[0])로 첫 번째 요소가 배열인지 확인하고, 배열이라면 지금까지의 객체를 마무리한 후 해당 배열을 새로운 사용자로 재귀 처리합니다.
주의할 점
변환 과정에서 하나의 객체를 계속 재사용하거나 참조를 공유하면 모든 속성이 하나의 객체에 뒤섞이는 오류가 발생할 수 있습니다. 반드시 사용자 단위로 새로운 객체를 만들어 결과에 추가해야 합니다.
참고: 단순 배열이라면 Object.fromEntries 활용
중첩 없이 단순한 키-값 쌍 배열만 있다면 ES2019에서 도입된 Object.fromEntries()로 한 줄로 해결할 수 있습니다.
const pairs = [['firstName', 'Joe'], ['lastName', 'Blow'], ['age', 42]];
const obj = Object.fromEntries(pairs);
console.log(obj);
// { firstName: 'Joe', lastName: 'Blow', age: 42 }
다만 Object.fromEntries()는 중첩 구조를 사용자 단위로 분리해 주지 않으므로, 위 예제처럼 여러 명의 정보가 섞여 있는 데이터에는 앞서 소개한 재귀 방식이 적합합니다.