Computer >> 컴퓨터 >  >> 프로그래밍 >> JavaScript

JavaScript로 평면 배열 객체를 트리 구조로 변환하는 방법

실제 개발을 하다 보면 parentId처럼 부모 참조만 담고 있는 평면(flat) 형태의 배열을 마주하게 됩니다. 이런 데이터를 그대로 사용하기보다는, 계층 관계가 드러나는 트리(tree) 구조로 변환해야 화면에 카테고리나 메뉴를 직관적으로 렌더링할 수 있습니다.

이번 글에서는 다음과 같은 배열이 주어졌을 때, 각 자식 요소를 부모 객체와 연결하여 트리 데이터 구조를 만들고, 결과를 중첩 리스트(nested list) 형태로 화면에 출력하는 방법을 알아보겠습니다.

const arr = [
    { id: '1', name: 'name 1', parentId: null },
    { id: '2', name: 'name 2', parentId: null },
    { id: '2_1', name: 'name 2_1', parentId: '2' },
    { id: '2_2', name: 'name 2_2', parentId: '2' },
    { id: '3', name: 'name 3', parentId: null },
    { id: '4', name: 'name 4', parentId: null },
    { id: '5', name: 'name 5', parentId: null },
    { id: '6', name: 'name 6', parentId: null },
    { id: '7', name: 'name 7', parentId: null },
    { id: '1_1', name: 'name 1_1', parentId: '1' },
    { id: '1_2', name: 'name 1_2', parentId: '1' },
    { id: '1_3', name: 'name 1_3', parentId: '1' },
    { id: '1_4', name: 'name 1_4', parentId: '1' },
    { id: '1_5', name: 'name 1_5', parentId: '1' },
    { id: '2_1_1', name: 'name 2_1_1', parentId: '2_1' },
    { id: '2_1_2', name: 'name 2_1_2', parentId: '2_1' },
    { id: '2_1_3', name: 'name 2_1_3', parentId: '2_1' },
    { id: '2_1_4', name: 'name 2_1_4', parentId: '2_1' },
    { id: '2_1_5', name: 'name 2_1_5', parentId: '2_1' },
];

구현 목표

위와 같은 객체 배열을 입력으로 받아 다음 작업을 수행하는 것이 목표입니다.

  • 각 항목의 parentId 값을 기준으로 자식 요소를 부모 객체에 연결합니다.
  • 완성된 트리 구조를 화면에 중첩 리스트 형태로 출력합니다.
  • 부모 노드를 클릭하면 하위 자식 노드를 펼치거나 접을 수 있도록 만듭니다.

예제 코드

HTML 코드

먼저 트리가 렌더링될 컨테이너를 준비합니다.

<html>
    <head></head>
    <body>
        <div id = "tree">
        </div>
    </body>
</html>

JavaScript 코드

배열을 순회하면서 parentId가 null인 항목은 최상위 부모로 처리하고, 나머지 항목은 해당 부모의 DOM 요소를 찾아 자식 컨테이너에 추가합니다. 부모 노드에는 클릭 이벤트를 등록해 자식 영역의 펼침/접힘을 토글합니다.

const arr = [
    { id: '1', name: 'name 1', parentId: null },
    { id: '2', name: 'name 2', parentId: null },
    { id: '2_1', name: 'name 2_1', parentId: '2' },
    { id: '2_2', name: 'name 2_2', parentId: '2' },
    { id: '3', name: 'name 3', parentId: null },
    { id: '4', name: 'name 4', parentId: null },
    { id: '5', name: 'name 5', parentId: null },
    { id: '6', name: 'name 6', parentId: null },
    { id: '7', name: 'name 7', parentId: null },
    { id: '1_1', name: 'name 1_1', parentId: '1' },
    { id: '1_2', name: 'name 1_2', parentId: '1' },
    { id: '1_3', name: 'name 1_3', parentId: '1' },
    { id: '1_4', name: 'name 1_4', parentId: '1' },
    { id: '1_5', name: 'name 1_5', parentId: '1' },
    { id: '2_1_1', name: 'name 2_1_1', parentId: '2_1' },
    { id: '2_1_2', name: 'name 2_1_2', parentId: '2_1' },
    { id: '2_1_3', name: 'name 2_1_3', parentId: '2_1' },
    { id: '2_1_4', name: 'name 2_1_4', parentId: '2_1' },
    { id: '2_1_5', name: 'name 2_1_5', parentId: '2_1' },
]
const tree = document.getElementById("tree")
arr.forEach(a => {
    const div = document.createElement("div")
    if (a.parentId === null) {
        div.classList.add("parent")
        div.setAttribute("id", `id${a.id}`)
        div.innerText = a.name
        tree.appendChild(div)
    }
    else {
        const parent = document.getElementById(`id${a.parentId}`)
        if (!parent.classList.contains("parent")) {
            parent.classList.add("parent")
        }
        console.log(`${a.id} `, parent)
        let childContainer = null
        if (parent.childElementCount === 0) {
            childContainer = document.createElement("div")
            childContainer.classList.add("childContainer")
            parent.appendChild(childContainer)
            childContainer.classList.add("hidden")
        }
        else {
            childContainer = document.querySelector(`#id${a.parentId}
            .childContainer`)
        }
        div.classList.add("child")
        div.setAttribute("id", `id${a.id}`)
        div.innerText = a.name
        childContainer.appendChild(div)
    }
})
const parents =
Array.from(document.getElementsByClassName("parent")).filter(p =>
p.childElementCount !== 0)
parents.forEach(p => {
    p.addEventListener("click", function(e) {
        e.preventDefault()
        const container = this.getElementsByClassName("childContainer")[0]
        container.classList.toggle("visible")
        e.stopPropagation()
    })
})

CSS 코드

부모/자식 노드의 시각적 계층을 표현하고, 자식 컨테이너는 기본적으로 숨긴 뒤 visible 클래스가 추가될 때만 보여줍니다.

.parent, .child {
    cursor: pointer;
}
.parent {
    margin: 10px 0;
    font-size: 1.3rem;
}
.parent::before {
    content: "\25BA";
    margin-right: 10px;
}
.childContainer {
    margin-left: 20px;
    display: none;
}
.childContainer.visible {
    display: block;
}
.child {
    font-size: 1rem;
}
.child::before {
    content: "\25BA";
}

실행 결과

코드를 실행하면 최상위 항목들이 먼저 출력되고, 부모 노드를 클릭할 때마다 해당 노드에 속한 자식 항목들이 아래와 같이 펼쳐집니다.

JavaScript로 평면 배열 객체를 트리 구조로 변환하는 방법

정리

핵심 아이디어는 단순합니다. parentId가 없는 항목을 루트로 삼고, 나머지 항목은 부모 DOM 요소를 조회해 자식 컨테이너에 순서대로 붙여 넣는 것입니다. 이 방식은 별도의 재귀 함수 없이도 DOM 기반으로 트리를 구성할 수 있다는 장점이 있습니다. 다만 데이터 양이 매우 크거나 깊은 계층 구조를 다룰 때는, DOM 조작 대신 순수 객체 트리를 먼저 만든 후 한 번에 렌더링하는 방식이 성능 면에서 더 유리할 수 있습니다.