JavaScript 개발을 하다 보면 데이터베이스나 API에서 받은 평면(flat) 배열을 카테고리, 조직도, 메뉴 등에 활용할 수 있도록 중첩된 트리(nested tree) 구조로 변환해야 하는 경우가 자주 있습니다.
이번 글에서는 점(.)으로 구분된 계층형 코드를 기준으로 평면 배열을 트리 구조로 변환하는 방법을 예제와 함께 살펴보겠습니다.
문제 상황
다음과 같이 부모-자식 관계가 코드(code) 값의 계층 구조로 표현된 배열이 있다고 가정해 보겠습니다.
const arr = [{
"code": "2",
"name": "PENDING"
},
{
"code": "2.2",
"name": "PENDING CHILDREN"
},
{
"code": "2.2.01.01",
"name": "PENDING CHILDREN CHILDREN"
},
{
"code": "2.2.01.02",
"name": "PENDING CHILDREN CHILDREN02"
},
{
"code": "1",
"name": "ACTIVE"
},
{
"code": "1.1",
"name": "ACTIVE CHILDREN"
},
{
"code": "1.1.01",
"name": "ACTIVE CHILDREN CHILDREN"
}];여기서 각 항목의 code 값을 보면, 점(.)으로 구분된 단계 수가 곧 트리의 깊이를 의미합니다. 예를 들어 "2"는 최상위 노드, "2.2"는 그 아래 첫 번째 자식, "2.2.01.01"은 세 단계 아래에 위치한 노드입니다.
우리는 이러한 배열을 입력받아 각 노드의 계층 관계를 children 속성으로 표현하는 JavaScript 함수를 작성해야 합니다.
기대 결과
변환이 완료되면 위 배열은 다음과 같은 중첩 구조의 객체 배열로 바뀌어야 합니다.
const output = [{
"code": "2",
"name": "PENDING",
"children": [{
"code": "2.2",
"name": "PENDING CHILDREN",
"children": [{
"code": "2.2.01.01",
"name": "PENDING CHILDREN CHILDREN"
},
{
"code": "2.2.01.02",
"name": "PENDING CHILDREN CHILDREN02"
}]
}]
},
{
"code": "1",
"name": "ACTIVE",
"children": [{
"code": "1.1",
"name": "ACTIVE CHILDREN",
"children": [{
"code": "1.1.01",
"name": "ACTIVE CHILDREN CHILDREN"
}]
}]
}];구현 예제
이 변환을 수행하는 함수는 다음과 같이 작성할 수 있습니다.
const arr = [{
"code": "2",
"name": "PENDING"
},
{
"code": "2.2",
"name": "PENDING CHILDREN"
},
{
"code": "2.2.01.01",
"name": "PENDING CHILDREN CHILDREN"
},
{
"code": "2.2.01.02",
"name": "PENDING CHILDREN CHILDREN02"
},
{
"code": "1",
"name": "ACTIVE"
},
{
"code": "1.1",
"name": "ACTIVE CHILDREN"
},
{
"code": "1.1.01",
"name": "ACTIVE CHILDREN CHILDREN"
}];
const transformToTree = (arr, root = '') => {
let map = {}, last = [root], level = 0;
map[root] = {};
arr.forEach(el => {
let parent = root;
while (level && last[level].length >= el.code.length) {
level--;
};
parent = last[level];
level++;
last.length = level;
last.push(el.code);
map[el.code] = el;
map[parent].children = map[parent].children || [];
map[parent].children.push(el);
});
return map[root].children;
};
console.log(JSON.stringify(transformToTree(arr), undefined, 4));코드 동작 원리
- map 객체: 각 노드의
code값을 키로 사용하여 모든 노드에 빠르게 접근할 수 있도록 합니다. - last 배열: 현재까지 순회하며 만난 각 깊이(level)의 마지막 노드 경로를 추적합니다.
- level 변수: 현재 노드가 속한 트리의 깊이를 나타냅니다. 새로운 노드의
code길이를 기준으로 적절한 부모 위치를 찾습니다. - children 배열: 부모 노드에 아직
children속성이 없다면 새로 생성한 뒤, 현재 노드를 자식으로 추가합니다.
즉, 입력 배열이 이미 코드 길이(계층 깊이) 순으로 정렬되어 있다는 전제 하에, 한 번의 순회만으로 O(n) 시간 복잡도에 트리를 구성할 수 있는 효율적인 방식입니다.
실행 결과
콘솔에 출력되는 결과는 다음과 같습니다.
[
{
"code": "2",
"name": "PENDING",
"children": [
{
"code": "2.2",
"name": "PENDING CHILDREN",
"children": [
{
"code": "2.2.01.01",
"name": "PENDING CHILDREN CHILDREN"
},
{
"code": "2.2.01.02",
"name": "PENDING CHILDREN CHILDREN02"
}
]
}
]
},
{
"code": "1",
"name": "ACTIVE",
"children": [
{
"code": "1.1",
"name": "ACTIVE CHILDREN",
"children": [
{
"code": "1.1.01",
"name": "ACTIVE CHILDREN CHILDREN"
}
]
}
]
}
]마무리
이처럼 점(.)으로 구분된 계층형 코드가 포함된 평면 배열은 별도의 재귀 호출 없이도 스택 방식의 순회만으로 손쉽게 트리 구조로 변환할 수 있습니다. 이 기법은 파일 시스템 탐색기, 다단계 카테고리 메뉴, 조직도 UI 등 계층 데이터를 화면에 렌더링해야 하는 다양한 상황에서 유용하게 활용할 수 있습니다.