티스토리 뷰

∙React

Rc-Tree 사용하기

coor 2024. 8. 5. 12:39

1. Rc-Tree 개념과 사용


[ Rc-Tree 이란? ]

Rc-Tree는 부모와 자식 즉, 폴더와 문서로 이루어진 관계로 폴더 안에 폴더를 넣거나 여러 가지 문서를 넣어 편하게 관리할 수 있는 라이브러리이다. 트리 구조를 쉽게 시각화하고 조작할 수 있는 기능을 제공합니다.

출처 - https://www.npmjs.com/package/rc-tree

 

[ Rc-Tree 설치 ]

npm i rc-tree

npm rc-tree 공식 사이트

 

개발환경 : React 18, rc-tree 2.1.2

[ Rc-Tree 생성 ]

import React, { Component } from "react";
import Tree, { TreeNode } from "rc-tree";
import "rc-tree/assets/index.css"

const treeData = [
  {
    key: "0-0",
    title: "parent 1",
    children: [
      {
        key: "0-0-0",
        title: "parent 1-1",
        children: [{ key: "0-0-0-0", title: "parent 1-1-0" }]
      },
      {
        key: "0-0-1",
        title: "parent 1-2",
        children: [
          { key: "0-0-1-0", title: "parent 1-2-0", disableCheckbox: true },
          { key: "0-0-1-1", title: "parent 1-2-1" },
          { key: "0-0-1-2", title: "parent 1-2-1" },
          { key: "0-0-1-3", title: "parent 1-2-1" },
          { key: "0-0-1-4", title: "parent 1-2-1" },
          { key: "0-0-1-5", title: "parent 1-2-1" },
          { key: "0-0-1-6", title: "parent 1-2-1" }
        ]
      }
    ]
  }
];

export default class Example extends Component {
  constructor(props) {
    super(props);
    this.state = {
      defaultExpandedKeys: ["0-0-1"],
    };
  }

  render() {
    return (
          <div className="draggable-container">
            <Tree
              treeData={treeData}
              defaultExpandedKeys={this.state.defaultExpandedKeys}
            />
         </div>
    );
  }
}

import를 통해 rc-tree 라이브러리를 가져옵니다. 트리를 만들기 위해 부모와 자식으로 이루어진 데이터인 treeData를 <Tree> 컴포넌트의 treeData 속성과 연결시켜 줍니다. 여기서 중요한 건 부모와 자식을 구별하기 위해 children 키 값에 자식을 넣어줘야 하고, 부모와 자식의 키 값은 고유한 키 값이어야 합니다. 트리의 기본 구성 요소인 key, title, children 등 정해진 속성을 통해 작성해야 합니다.

 

 

 


지금까지 rc-tree 기본적인 사용법은 알아보았습니다.
이제 rc-tree 응용하여 도서 관리 시스템에 적용시켜보겠습니다!

도서 목록(제목/카테고리/서브카테고리)

요구사항은 다음과 같습니다.

  1. 카테고리별로 도서 나누기
  2. 카테고리 추가, 수정, 삭제
  3. 드래그를 통한 도서 이동

 



1. 카테고리별로 도서 나누기


카테고리별로 도서를 나누기 위해, 카테고리는 상위 노드를 설정하고 도서는 자식 노드로 분리합니다. 트리 구조에서 children 배열을 사용하여 카테고리와 도서의 관계를 정의합니다. 코드는 다음과 같습니다.

// import 생략
const initialTreeData = [
  {
    key: "0-0-1",
    title: "소설",
    isLeaf: false,
    children: [
      { key: "0-0-1-1", title: "나미야 잡화점의 기적" },
      { key: "0-0-1-2", title: "데미안" },
      { key: "0-0-1-3", title: "오만과 편견" },
      { key: "0-0-1-4", title: "백년의 고독" },
      { key: "0-0-1-5", title: "채식주의자" },
      { key: "0-0-1-6", title: "파친코" },
      { key: "0-0-1-7", title: "토지" },
      { key: "0-0-1-8", title: "태백산맥" },
      { key: "0-0-1-9", title: "삼국지" },
      { key: "0-0-1-10", title: "그리스인 조르바" },
      { key: "0-0-1-11", title: "죄와 벌" },
      { key: "0-0-1-12", title: "안나 카레니나" },
    ],
  },
  { key: "0-0-2", title: "자기계발", isLeaf: false, children: [{ key: "0-0-2-1", title: "미움받을 용기" }] },
  { key: "0-0-3", title: "에세이", isLeaf: false, children: [ { key: "0-0-3-1", title: "지금 이대로 좋다" }, { key: "0-0-3-2", title: "모리와 함께한 화요일" }, { key: "0-0-3-3", title: "도둑 맞은 가난" }, ], },
  { key: "0-0-4", title: "판타지", isLeaf: false, children: [{ key: "0-0-4-1", title: "해리 포터와 마법사의 돌" }] },
  { key: "0-0-5", title: "경제", isLeaf: false, children: [{ key: "0-0-5-1", title: "고요한 흑자" }] },
  { key: "0-0-6", title: "기타", isLeaf: false, children: [] },
];

export default class Example extends Component {
  constructor(props) {
    super(props);
    this.state = {
      defaultExpandedKeys: ["0-0"],
    };
  }

  render() {
    return (
      <div className="tree-container" style={{ padding: "20px" }}>
        <Tree treeData={treeData} defaultExpandedKeys={this.state.defaultExpandedKeys} autoExpandParent />
      </div>
    );
  }
}

initialTreeData 변수를 보면 상단에 있는 "key: 0.0.0, title: 소설"이 부모 노드로 되어 있고, children 배열 안에 있는 값들이 자식 노드로 정리됩니다. 만약 카테고리는 있는데 도서가 없는 경우 children: [] 비어있는 값으로 되는데 이런 경우 자식 노드로 인식되기 때문에 isLeaf: false 값을 넣어줘야 합니다. 이렇게 설정하면 해당 노드가 자식 노드가 아니라 부모 노드라는 것을 인식하게 됩니다.

{ key: "0-0-6", title: "기타", isLeaf: false, children: [] },

 
 

 

2. 카테고리 추가, 수정, 삭제


Rc-tree에서 지원하는 checkbox 기능이 있습니다. 이 기능을 통해서 추가, 수정, 삭제를 해보겠습니다. 일단 checkbox를 사용하기 위해서 <Tree> 컴포넌트에 checkable, onCheck, checkStrictly 설정이 필요합니다. 

<Tree
   treeData={treeData}
   defaultExpandedKeys={defaultExpandedKeys}
   checkable			        // 체크박스 설정
   onCheck={this.onCheck}		// 체크 시 이벤트
   checkStrictly={true}			// 체크 시 부모/자식 분리
/>

각 속성은 다음과 같습니다.

  • checkable : checkbox 활성화
  • onCheck : 노드를 체크했을 때 어떤 이벤트를 실행한 건지 정의
  • checkStrictly : 부모 체크 시 모든 자식이 체크되는 걸 방지
// 체크 이벤트
onCheck = (checkKey, info) => {
    const categoryIds = [];
    const bookIds = [];

    // 체크된 노드들을 카테고리과 도서 분리
    info.checkedNodes.map((c) => {
      if (c.props.isLeaf === false) {
        categoryIds.push(c.key);
      } else {
        bookIds.push(c.key);
      }
    });

    this.setState({
      parentTree: categoryIds,
      childrenTree: bookIds,
    });
};

체크 이벤트인 onCheck 함수는 선택된 노드들을 카테고리와 도서를 "isLeaf == false" 기준으로 분리합니다. 체크된 노드들 중 카테고리인 노드와 도서인 노드를 key 값을 분리하여 parentTree, childreTree 상태에 저장합니다. 아래 사진처럼 checkbox 활성화되는 것을 볼 수 있습니다.

단어 정의
> 카테고리, 서브 카테고리 : 부모 노드
> 도서 : 자식 노드

 

[ 추가 ]

카테고리 추가하는 방법은 2가지가 있습니다. 
1. 새로운 카테고리 만들기
2. 카테고리 안에 서브 카테고리 만들기

<button onClick={() => this.openAddModal()}>추가</button>

// 모달 열기
openAddModal = () => {
    const { parentTree } = this.state;
    
    // 유효성 검사
    if (parentTree.length >= 2) {
      alert("한 개의 카테고리만 선택할 수 있습니다.");
      return;
    }
    
    if (parentTree.length === 1) {
      const checkedCategory = this.findCategory(treeData, parentTree[0]);
      if (!checkedCategory || (checkedCategory.hasOwnProperty("isLeaf") && checkedCategory.isLeaf === true)) {
        alert("부모 노드만 선택할 수 있습니다.");
        return;
      }
    }
    this.setState({ showModal: true });
};

카테고리를 추가하기 위해 추가 버튼을 넣어주고 버튼을 클릭 시 유효성 검사를 하고 모달 창을 open 합니다. 유효성 검사는 체크한 부모 노드가 2개 이상 체크가 되었는지, 부모 노드가 한 개만 체크되었다면 부모 노드가 체크되었는지 유효성 검사합니다.

// 카테고리 추가 이벤트
addCategory = () => {
    const { categoryName, treeData, parentTree } = this.state;
    // 유효성 검사
    if (!categoryName) {
      alert("카테고리를 입력해주세요.");
      return;
    }

    // 새로운 카테고리 생성
    const newCategory = {
      key: `0-0-${Date.now()}`,
      title: categoryName,
      children: [],
      isLeaf: false,
    };

    const updatedTreeData = [...treeData];
    // 새로운 카테고리 만들기
    if (parentTree.length == 0) {
      updatedTreeData.push(newCategory);
    } 
    
    // 카테고리 안에 서브 카테고리 만들기
    else {
      const checkedCategory = this.findCategory(treeData, parentTree[0]);
      checkedCategory.children.unshift(newCategory);
    }
    this.setState({ treeData: updatedTreeData, showModal: false, categoryName: "" });
};

// 해당 카테고리 찾기 이벤트
findCategory = (data, key) => {
    for (let item of data) {
      if (item.key === key) return item;
      if (item.children) {
        const found = this.findCategory(item.children, key);
        if (found) return found;
      }
    }
    return null;
};

 

위 코드는 카테고리 추가 버튼에 이벤트로, 흐름은 다음과 같습니다.

  1. 유효성 검사 : 입력한 값이 비어있는지 확인
  2. 새로운 카테고리 생성 : 고유한 키 값과 부모 노드로 인식하기 위해 isLeaf : false 속성 추가
  3. 새로운 카테고리 만들기 : 부모 노드가 체크한 게 없으면 진행을 하고 해당 노드는 전체 노드에서 하단에 추가
  4. 카테고리 안에 서브 카테고리 만들기 : 부모 노드가 체크한 게 있으면 진행을 하고 체크한 노드가 어디에 속해있는지 알기 위해 findCategory( ) 함수에서 재귀적으로 트리 구조를 탐색하여 item.key == key 키 값이 동일한 노드를 찾습니다. 체크한 노드를 찾게 되면 children 배열에 새로운 카테고리를 넣어줍니다.

만약 고유 키를 생성할 때 다른 노드와 겹치게 되면 고유한 값이 되지 않기 때문에 치명적인 오류가 발생할 수 있습니다. 실제 콘솔 창에서도 Warning 오류가 나옵니다.

그러므로 노드의 키는 꼭 고유한 값으로 설정해야 합니다. 그 다음 treeData 변수에 생성한 노드를 추가해줍니다. 

 

 

 

[ 수정 ]

수정은 단일 노드만 선택했는지와 부모 노드인지 확인한 후, 모달 창을 열어 카테고리명을 수정하는 방향으로 진행해보겠습니다.

<button onClick={() => this.openUpdateModal()}>수정</button>

// 모달 열기
openUpdateModal = () => {
    const { parentTree, treeData } = this.state;
    if (parentTree.length !== 1) {
      alert("한 개의 카테고리만 선택할 수 있습니다.");
      return;
    }

    const checkedCategory = this.findCategory(treeData, parentTree[0]);
    if (!checkedCategory || (checkedCategory.hasOwnProperty("isLeaf") && checkedCategory.isLeaf === true)) {
      alert("부모 노드만 선택할 수 있습니다.");
      return;
    }

    this.setState({
      categoryName: checkedCategory.title,
      showModal: true,
    });
};

일단 수정할 수 있도록 수정 버튼을 만듭니다. 수정 모달 창이 열릴 때 유효성 검사는 2가지입니다. 첫 번째는 한 개의 노드만 체크를 했는지이고, 두 번째는 부모 노드를 체크를 했는지 확인해야 합니다. 그리고 모달 창이 열렸을 때 해당 노드의 카테고리명을 나올 수 있도록 categoryName : checkedCategory.title 값을 넣어줍니다.

// 카테고리 수정 이벤트
updateCategory = () => {
    const { treeData, categoryName, parentTree } = this.state;

    // 유효성 검사
    if (!categoryName) {
      alert("카테고리를 입력해주세요.");
      return;
    }

    // 카테고리 수정
    const updateTreeData = (data) => {
      const checkedCategory = this.findCategory(treeData, parentTree[0]);
      return data.map((item) => {
        if (item.key === checkedCategory.key) {
          return { ...item, title: categoryName };
        } else if (item.children) {
          return { ...item, children: updateTreeData(item.children) };
        }
        return item;
      });
    };
    
    const updatedTreeData = updateTreeData(treeData);
    this.setState({ treeData: updatedTreeData, showModal: false, categoryName: "" });
};

카테고리 수정 이벤트는 입력한 값이 비어있는지 유효성 검사를 먼저 해줍니다. 그다음 updateTreeData에서 체크한 노드를 찾아서 그 노드에 카테고리명을 수정해줍니다. 

 

 

[ 삭제 ]

삭제는 체크된 기준으로 부모 노드들을 삭제하려면 parentTree를 기반으로 트리 데이터를 삭제하는 로직을 작성해야 합니다. 이를 위해 먼저 체크된 키들을 기반으로 트리 데이터에서 해당 노드들을 찾아 삭제하는 방향으로 진행해보겠습니다.

<button onClick={() => this.removeCategory()}>삭제</button>

// 카테고리 삭제 이벤트
removeCategory = () => {
    const { parentTree, treeData } = this.state;
    if (parentTree.length == 0) {
      alert("카테고리를 선택해주세요.");
      return;
    }

    const updatedTreeData = this.removeNodesFromTree(treeData, parentTree);
    this.setState({ treeData: updatedTreeData });
};

// 트리에서 노드 삭제
removeNodesFromTree = (treeData, keysToRemove) => {
    return treeData.filter((item) => {
      if (keysToRemove.includes(item.key)) {
        return false;
      }
      if (item.children) {
        item.children = this.removeNodesFromTree(item.children, keysToRemove);
      }
      return true;
    });
};

이 코드는 removeCategory 메서드는 parentTree에 포함된 노드를 removeNodesFromTree 메서드를 사용하여 트리 데이터에서 제거합니다. removeNodesFromTree 메서드는 재귀적으로 트리 구조를 탐색하며 삭제할 키를 찾고, 해당 키가 포함된 노드를 트리 데이터에서 제거합니다.

 

 

 

 

 

3. 마우스 드래그를 통한 도서이동


<Tree 
  checkable 			     		  
  selectable  				  	
  treeData={groups}  				 
  onCheck={this.onCheck}  		   	 
  selectable={false}  			 
  checkStrictly={true} 				
  draggable   				 	 // 드래그 활성화
  onDragStart={this.onDragStart}  	    	 // 드래그 시작 이벤트
  onDrop={this.onDrop}  	 		  // 드래그 드롭 이벤트
/>

드래그를 활성화하기 위해 draggable, onDragStart, onDrop 속성을 이용해야 합니다. 

  • draggable: 트리 컴포넌트에서 항목을 드래그 가능할 수 있도록 함
  • onDragStart: 드래그가 시작될 때 호출되는 이벤트 핸들러
  • onDrop: 드래그된 항목이 드롭될 때 호출되는 이벤트 핸들러
// 드래그 시작 이벤트
onDragStart = (event) => {
    const currentNode = event.node.props;
    
    // 도서 확인
    if (!currentNode || (currentNode.hasOwnProperty("isLeaf") && currentNode.isLeaf === false)) {
      alert("도서만 선택해주세요.");
      return;
    }

    // 드래그 도서 저장
    const newChildrenNode = {
      key: currentNode.eventKey,
      title: currentNode.title,
      children: currentNode.children,
    };
    this.setState({ dragBooks: newChildrenNode });
};

이 코드는 onDragStart 속성의 드래그가 시작될 때 실행되는 이벤트 핸들러입니다. 시작 이벤트는 드래그한 노드가 도서인지 확인을 합니다. 도서가 맞다면 노드의 키, 제목, 자식 노드 정보를 포함한 객체를 생성하고, 이를 dragBooks 상태에 저장합니다. 

// 드래그 드롭 이벤트
onDrop = (event) => {
    const { treeData, dragBooks } = this.state;
    const dropKey = event.node.props.eventKey;

    // 드래그한 노드를 트리에서 제거
    const newTreeData = this.removeNodesFromTree(treeData, [dragBooks.key]);

    // 드롭된 노드를 찾아서 드래그 노드 추가
    const dropNode = this.findCategory(newTreeData, dropKey);
    if (dropNode) {
      dropNode.children.push(dragBooks);
    }

    this.setState({ treeData: newTreeData, dragBooks: null });
};

이 코드는 onDrag 속성의 드래그가 드롭 이벤트가 발생했을 때 실행되는 이벤트 핸들러입니다. 드롭 이벤트는 treeData에서 드래그된 노드를 찾아 제거한 후, 드롭된 위치의 노드를 찾아서 드래그된 노드를 해당 노드의 자식으로 추가합니다. 그리고 업데이트된 트리 데이터를 상태에 저장하고 드래그된 노드를 초기화합니다.

경제 카테고리 안에 "고요한 흑자" 도서를 판타지에 드래그를 이동하면 오른쪽 사진과 같이 판타지 카테고리 안에 도서를 들어가는 것을 볼 수 있습니다.

 

 

 

[출처]
npm rc-tree      
Github - React-Component/Tree

'∙React' 카테고리의 다른 글

하이차트 (Highcharts) 사용하기  (0) 2024.08.05
React-Tooltip 사용하기  (0) 2024.08.05
AG-Grid 사용하기  (4) 2024.07.30
Excel 변환하기 - React  (0) 2022.02.15