트리 구조 API란
트리 구조 API는 계층형 데이터(부모-자식 관계)를 클라이언트에 구조적으로 전달하는 API입니다.
프론트엔드는 이 API를 통해 트리 형태의 UI를 구성합니다.
이 구조가 널리 쓰이는 이유
✅ 프론트에서 바로 사용 가능 | React, Vue, Angular에서 children 기반으로 메뉴 컴포넌트를 재귀 렌더링 |
✅ UI 연동이 쉬움 | 아이콘, 경로, title 등 필요한 정보를 함께 포함시켜 UX까지 포함된 메뉴 구성 제공 |
✅ null 또는 []로 리프 노드 명확화 | 자식이 없는 경우를 명확하게 처리할 수 있어 로직 구현에 유리 |
대표적인 트리 구조 데이터 예시
메뉴 : 상위-하위 메뉴 구조
{
"code": 200,
"message": "메뉴 조회 성공",
"data": [
{
"menuId": 1,
"title": "대시보드",
"icon": "dashboard",
"path": "/dashboard",
"children": [
{
"menuId": 2,
"title": "방문자 통계",
"path": "/dashboard/visitors",
"children": []
},
{
"menuId": 3,
"title": "운영 통계",
"path": "/dashboard/operations",
"children": []
}
]
},
{
"menuId": 4,
"title": "사용자 관리",
"icon": "user",
"path": "/user",
"children": null
}
]
}
댓글 : 댓글-대댓글 관계
{
"code": 200,
"message": "댓글 목록 조회 성공",
"data": [
{
"commentId": 1,
"writer": "user1",
"content": "이 게시글 정말 좋네요!",
"createdAt": "2025-06-25T10:30:00",
"children": [
{
"commentId": 2,
"writer": "user2",
"content": "맞아요. 공감합니다!",
"createdAt": "2025-06-25T10:45:00",
"children": []
}
]
},
{
"commentId": 3,
"writer": "user3",
"content": "궁금한 게 있는데요...",
"createdAt": "2025-06-25T11:00:00",
"children": []
}
]
}
트리 구성 방식
A. WITH RECURSIVE 방식 (재귀 쿼리)
- SQL에서 재귀적으로 자식 노드를 조회
- 테이블 구조는 단순 (하나만 있으면 됨)
- 트리 구조가 자주 바뀌거나 CRUD가 많을 때 적합
테이블 예시
3단계 이상의 트리 깊이와 여러 루트 노드를 포함한 복합 구조
id | name | parent_id |
1 | 대시보드 | NULL |
2 | 방문자 통계 | 1 |
3 | 시간대별 | 2 |
4 | 일별 | 2 |
5 | 운영 통계 | 1 |
6 | 사용자 관리 | NULL |
7 | 사용자 목록 | 6 |
8 | 사용자 상세 | 7 |
9 | 설정 | NULL |
10 | 시스템 설정 | 9 |
11 | 보안 설정 | 10 |
SQL 예시
WITH RECURSIVE menu_tree AS (
-- 루트 노드 (parent_id가 NULL)
SELECT
id,
name,
parent_id,
0 AS depth,
CAST(name AS CHAR(1000)) AS path
FROM menu
WHERE parent_id IS NULL
UNION ALL
-- 재귀적으로 자식 노드 탐색
SELECT
m.id,
m.name,
m.parent_id,
t.depth + 1,
CONCAT(t.path, ' > ', m.name)
FROM menu m
INNER JOIN menu_tree t ON m.parent_id = t.id
)
SELECT *
FROM menu_tree
ORDER BY path;
SQL 쿼리 결과 예시
id | name | parent_id | depth | path |
1 | 대시보드 | NULL | 0 | 대시보드 |
2 | 방문자 통계 | 1 | 1 | 대시보드 > 방문자 통계 |
3 | 시간대별 | 2 | 2 | 대시보드 > 방문자 통계 > 시간대별 |
4 | 일별 | 2 | 2 | 대시보드 > 방문자 통계 > 일별 |
5 | 운영 통계 | 1 | 1 | 대시보드 > 운영 통계 |
6 | 사용자 관리 | NULL | 0 | 사용자 관리 |
7 | 사용자 목록 | 6 | 1 | 사용자 관리 > 사용자 목록 |
8 | 사용자 상세 | 7 | 2 | 사용자 관리 > 사용자 목록 > 사용자 상세 |
9 | 설정 | NULL | 0 | 설정 |
10 | 시스템 설정 | 9 | 1 | 설정 > 시스템 설정 |
11 | 보안 설정 | 10 | 2 | 설정 > 시스템 설정 > 보안 설정 |
B. Closure Table 방식
- 모든 조상-자손 관계를 별도 테이블에 저장 (ancestor_id, descendant_id, depth)
- 조회는 빠르지만 트리 변경 시 추가 작업 필요
- 트리 깊거나, 읽기 성능 중요한 경우 적합
기본 테이블 예시
기본 메뉴 정보는 여전히 parent_id 없이도 괜찮지만, 대부분 실무에서는 유지
id | name | parent_id |
1 | 대시보드 | NULL |
2 | 방문자 통계 | 1 |
3 | 시간대별 | 2 |
4 | 일별 | 2 |
5 | 운영 통계 | 1 |
6 | 사용자 관리 | NULL |
7 | 사용자 목록 | 6 |
8 | 사용자 상세 | 7 |
9 | 설정 | NULL |
10 | 시스템 설정 | 9 |
11 | 보안 설정 | 10 |
클로저 테이블 (menu_closure)
모든 조상-자손 경로를 저장합니다
ancestor_id | descendant_id | depth |
1 | 1 | 0 |
1 | 2 | 1 |
1 | 3 | 2 |
1 | 4 | 2 |
1 | 5 | 1 |
2 | 2 | 0 |
2 | 3 | 1 |
2 | 4 | 1 |
3 | 3 | 0 |
4 | 4 | 0 |
5 | 5 | 0 |
6 | 6 | 0 |
6 | 7 | 1 |
6 | 8 | 2 |
7 | 7 | 0 |
7 | 8 | 1 |
8 | 8 | 0 |
9 | 9 | 0 |
9 | 10 | 1 |
9 | 11 | 2 |
10 | 10 | 0 |
10 | 11 | 1 |
11 | 11 | 0 |
트리 조회 쿼리 예시
전체 트리를 계층 깊이별로 조회
→ 대시보드(id=1)의 전체 하위 메뉴들을 깊이 순서로 조회
SELECT
c.ancestor_id,
c.descendant_id,
c.depth,
m.name
FROM menu_closure c
JOIN menu m ON m.id = c.descendant_id
WHERE c.ancestor_id = 1
ORDER BY c.depth;
자식 노드만 조회
바로 아래 단계 자식 노드만 조회 (depth = 1)
SELECT m.*
FROM menu_closure c
JOIN menu m ON m.id = c.descendant_id
WHERE c.ancestor_id = :parentId
AND c.depth = 1;
✅ Closure Table 방식이 적합한 상황
closure table은 data를 한번 탐색할때마다
- 트리 구조가 자주 변경되지 않고 정적일 때 (예: 메뉴, 카테고리, 조직도 등)
- 성능이 매우 중요한 경우
(대규모 트리 조회, 다양한 계층별 조건 조회) - 깊이 제한, 정렬 등을 자주 해야 할 때
- 상위-하위 탐색이 빈번할 때
✅ WITH RECURSIVE 방식이 적합한 상황
- 데이터가 자주 삽입/삭제되는 경우 (예: 댓글, 파일 업로드 구조, 실시간 입력 데이터)
- 트리 구조가 복잡하지 않고 깊이가 얕은 경우 (2~3 depth 수준)
- 빠른 구현이 필요하거나 MVP 단계
- 클로저 테이블 동기화가 부담일 때
'개발기술 > 단순구현' 카테고리의 다른 글
게시판 구현 : 권한에 따른 동적인 메뉴 (0) | 2025.03.07 |
---|