콘텐츠로 건너뛰기

Ant Design Tree를 활용한 계층형 데이터 UI 공통 컴포넌트 구현

  • 테크

프론트엔드 개발을 하다 보면 비슷한 형태의 UI를 구현하지만, 계층 구조의 depth나 선택 방식 등 세부적인 요구사항이 서로 다른 경우가 종종 발생합니다. 최근 프로젝트에서도 depth가 서로 다른 데이터를 동일한 형태의 트리 UI로 표현하고 관리해야 했습니다.
이번 포스팅에서는 서로 다른 depth의 계층형 데이터를 하나의 공통 컴포넌트로 관리하고, 이를 Ant Design의 Tree 컴포넌트를 활용하여 효율적으로 구현한 방법을 작성하고자 합니다.

1. 문제 정의

  • 계층형 데이터가 다양한 depth를 가질 경우, 각각 별도의 컴포넌트를 구현하면 코드 중복과 유지보수의 어려움이 발생합니다.
  • 서로 다른 depth의 데이터를 하나의 공통 컴포넌트로 관리해, 코드 중복을 최소화하고 유지보수성을 높이는 방안이 필요합니다.

2. Ant Design Tree 컴포넌트의 특징

  • 직관적인 계층 구조 표현: 복잡한 데이터 구조를 사용자가 쉽게 이해할 수 있는 명확한 UI로 제공합니다.
  • 내장된 체크박스 기능: 다수 항목을 선택하거나 부모-자식 간의 선택 상태를 관리할 수 있습니다.
  • 검색 기능 지원: 계층 데이터에서 원하는 노드를 찾을 수 있도록 자동 완성 및 검색 결과의 자동 확장 기능을 지원합니다.
  • 노드 확장 및 축소 기능: 사용자가 필요한 부분만 보도록 선택적 확장 및 축소가 가능합니다.
  • 유연한 데이터 관리: 동적으로 데이터를 로딩하거나 데이터 변경 시 즉시 UI에 반영할 수 있습니다.

3. 동적 트리 데이터 생성

서로 다른 depth의 데이터를 공통적으로 처리하기 위해 데이터와 delimiter를 활용해 동적으로 depth를 생성하는 buildTreeData 함수를 구현했습니다.

// 동적으로 트리 구조 데이터를 생성하는 함수
export default function buildTreeData(data, delimiter, ctcd) {
  const rootMap = {};

  // 원본 데이터를 순회하며 각 아이템을 트리 형태로 구성
  data.forEach((item) => {
    // delimiter를 기준으로 문자열 분리
    const parts = item.attr.split(delimiter);
    if (!parts.length) return; // 빈 문자열일 경우 처리하지 않음

    let currentMap = rootMap; // 루트부터 탐색 시작

    // 각 부분을 순회하면서 트리 구조 형성
    parts.forEach((part, idx) => {
      // 각 depth마다 고유한 경로키 생성
      const pathKey = parts.slice(0, idx + 1).join(delimiter);

      // 해당 경로가 없으면 새로운 노드 생성
      if (!currentMap[pathKey]) {
        currentMap[pathKey] = {
          title: part,
          key: `${ctcd}${idx + 1}-${pathKey}`,
          children: {}, // 자식 노드 초기화
        };
      }

      // 현재 노드를 다음 자식 노드의 부모로 설정
      currentMap = currentMap[pathKey].children;
    });

    // 마지막 depth의 노드에 원본에서 제공된 attr_nm을 title로 대체
    let cursor = rootMap;
    for (let i = 0; i < parts.length; i++) {
      const pk = parts.slice(0, i + 1).join(delimiter);

      if (i === parts.length - 1) {
        cursor[pk].title = item.attr_nm; // 최종 depth에서 사용자 친화적인 이름 설정
        cursor[pk].key = `${ctcd}${parts.length}-${item.attr}`; // 고유 키 재설정
      }
      cursor = cursor[pk].children; // 다음 자식으로 이동
    }
  });

  // 최종적으로 객체를 Ant Design Tree가 이해할 수 있는 배열 형태로 변환
  const mapToTree = (obj) =>
    Object.values(obj).map((node) => ({
      title: node.title,
      key: node.key,
      children: mapToTree(node.children), // 재귀적으로 자식 노드를 처리
    }));

  return mapToTree(rootMap);
}

4. 체크박스 상태 관리

부모 노드를 선택하면 자식 노드들이 자동 선택되는 요구사항을 충족하면서, 자식 노드들이 모두 선택된 부모 노드에 대한 유니크 키를 구하기 위해 다음과 같은 로직을 적용했습니다.

// 부모가 체크된 경우 자식 노드들의 중복 체크 상태 제거하는 함수
const removeChildrenIfParentIsChecked = (checkedKeys, treeData) => {
  const setOfKeys = new Set(checkedKeys); // 선택된 키들을 Set으로 관리

  // 부모 체크 상태를 전파
  const dfs = (node, ancestorIsChecked) => {
    const currentIsChecked = setOfKeys.has(node.key);

    // 부모 노드가 체크된 경우 현재 노드 체크 상태 삭제
    if (ancestorIsChecked) setOfKeys.delete(node.key);

    // 자식 노드로 탐색 계속 진행
    node.children?.forEach((child) => {
      dfs(child, ancestorIsChecked || currentIsChecked);
    });
  };

  treeData.forEach((root) => dfs(root, false));

  // 중복 제거 후 배열로 반환
  return Array.from(setOfKeys);
};

5. 검색 시 자식 노드 자동 확장 기능 구현

트리 내 검색어 입력 시 해당 노드와 자식 노드들이 자동 확장되는 기능을 다음과 같이 구현했습니다.

// 검색어와 일치하는 모든 노드와 자식 노드를 자동 확장시키는 함수
const findAllMatchedKeys = (nodes, searchValue) => {
  const result = [];

  // dfs 트리 탐색
  const dfs = (node, ancestors) => {
    const currentPath = [...ancestors, node.key];

    // 노드의 title이 검색어를 포함하면
    if (node.title.includes(searchValue)) {
      result.push(...currentPath); // 현재 경로 추가
      result.push(...getAllDescendantKeys(node)); // 자식 노드 모두 추가
    }

    // 자식 노드 계속 탐색
    node.children?.forEach((child) => dfs(child, currentPath));
  };

  nodes.forEach((root) => dfs(root, []));

  // 중복 키 제거 후 반환
  return [...new Set(result)];
};

이번 포스팅에서는 다양한 depth의 계층형 데이터를 하나의 공통 컴포넌트로 관리하고, Ant Design의 Tree 컴포넌트를 활용하여 확장 가능한 UI를 구현하는 방법에 대해 살펴보았습니다.
이러한 접근 방식을 통해 코드 중복을 줄이고 체크박스 상태 관리와 검색 시 자동 확장 기능 등 사용자 편의성을 향상시킬 수 있습니다.

Ref.
Tree – Ant Design

최신 마케팅/고객 데이터 활용 사례를 받아보실 수 있습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다