[TIL] Vue - TreeView

jeongjwon·2023년 12월 21일

Vue

목록 보기
7/19

TreeView

https://vuejs.org/examples/#tree

컴포넌트의 재귀적 사용을 보여주는 간단한 트리 뷰 구현의 예이다.

부모 컴포넌트인 TreeExample.vue 파일과 자식 컴포넌트인 TreeView.vue 로 나누어 재귀적으로 표현해보고자한다.







TreeExample.vue

먼저 부모 컴포넌트에서 부모와 자식을 관계를 나타내는 객체를 생성한다.
name 과 children 으로 이루어져 있으며, 자식이 있다면 children 배열이 존재하고, 그렇지 않다면 존재하지 않는 그런 구조이다.

const treeData = ref({
  name: "My Tree",
  children: [
    { name: "hello" },
    { name: "world" },
    {
      name: "child folder",
      children: [
        {
          name: "child folder",
          children: [{ name: "hello" }, { name: "world" }],
        },
        { name: "hello" },
        { name: "world" },
        {
          name: "child folder",
          children: [{ name: "hello" }, { name: "world" }],
        },
      ],
    },
  ],
});

이 객체는 자식 컴포넌트에서도 쓰일 예정이기 때문에 자식 컴포넌트의 props 로 넘겨줄 예정이다.

따라서 html 은 다음과 같이 작성해준다.

<template>
  <ul>
    <TreeView
      :model="treeData"
    ></TreeView>
  </ul>
</template>





TreeView.vue

부모 컴포넌트에서 생성한 객체를 자식 컴포넌트에서 props 로 정의하여 받아오도록 한다.

const props = defineProps({
  model: Object,
});

이제 자식 컴포넌트에서 동작하는 로직을 생성해야한다. 로직의 과정은 다음과 같다.

  • chilren 이 있는 경우
    • [+]문자열을 클릭한다. -> 문자열은 [-]로 변경된다.
    • children 을 나타낸다. (children 의 children 이 있을 경우 반복)
    • children 의 가장 마지막에는 +문자열을 클릭하여 new stuff 를 추가할 수 있다.


그렇다면, 클릭해야하는 부분은 눈으로 보이는 것은 두가지로 정리할 수 있다.

1-1. children 이 있는 경우 -> [+]문자열을 클릭한다. -> 문자열은 [-]로 변경된다. -> isOpen

  • 현재 폴더가 열려있는지 여부를 나타낸다.
  • ref 로 정의되어 있어 true 혹은 false 로 나타낼 수 있고, 초기값은 false 이다.
  • toggle 메서드를 통해 값을 변경할 수 있고 폴더를 열거나 닫을 수 있도록 한다.
    const isOpen = ref(false);
    function toggle(){
     isOpen.value = !isOpen.value;
    }

1-2. children 을 나타낸다. -> isFolder

  • 현재 모델이 폴더인지 아닌지 나타낸다.
  • props.model.chilren 이 존재하고 그 길이가 1이상일 때 true 를 반환하고, 그렇지 않을 경우 false 를 반환한다. -> computed propterty 계산된 속성으로 정의한다.
    const isFolder = computed(() => {
     //자식이 존재한다면 isFolder 를 + 로
     return props.model.children && props.model.children.length;
    });
  1. +문자열을 클릭하여 new stuff 를 추가하는 경우
 <li @click="addChild">+</li>
function addChild() {
 props.model.children.push({ name: "new stuff" });
}

li 태그를 클릭시 addChild 메서드가 실행이 되는데, props로 정의한 model 의 children에 name 이 new stuff 인 값을 추가하도록 push 해주었다.

하지만 위와 같이 자식 컴포넌트에서 props.model.children 을 직접 변경하려고 하고 있기 때문에 오류가 발생하였다. vue 에서는 부모에서 자식으로 전달된 props 객체는 변경이 금지되어 있다. 대신, 변경사항을 부모에게 알리고 부모 컴포넌트에서 데이트를 업데이트하는 방법을 사용한다면 변경할 수 있다.

따라서, 다시 한번 부모 컴포넌트에서의 코드를 살펴보면

//TreeExample.vue
function addChildToTreeData(model) {
  model.children.push({ name: "new stuff" });
}
 <TreeView
      :model="treeData"
      :addChild="addChildToTreeData"
    ></TreeView>

로직은 똑같은 addChildToTreeData 함수와 props로 전달해준다.

그렇다면 자식 컴포넌트에서 props 를 추가한다.

//TreeView.vue
const props = defineProps({
  model: Object,
  addChild: Function, //추가한 함수
});
function addChild() {
  props.addChild(props.model);
}

기존의 addChild 메서드에서는 바로 추가하는 로직이었지만, props.addChild 에 매개변수로 함께 접근하는 것으로 변경해주었다.








재귀적

그림코드

그림과 코드를 같이 보면 반복되는 구조를 가지는 것을 볼 수 있다.

리스트를 표현하는 template 이 있는 자식 컴포넌트인 TreeView.vueTreeView.vue 를 중첩시켜주면 된다.
부모 컴포넌트인 TreeExample.vue 에서 TreeView.vue 에 필요한 props 를 입력해주면 되는 것처럼 필요한 데이터와 추가적으로 필요한 데이터를 입력해주면된다.

 <TreeView
        v-for="childModel in model.children"
        :model="childModel"
        :key="childModel.name"
        :addChild="props.addChild"
></TreeView>

필수적인 필요한 props는 model과 addChild 메서드였다.
먼저 model 은 현재 model 의 children 으로써 배열의 형태이기 때문에 v-for를 이용한다.
key는 유일성을 나타내는 것이기에 childModel.name 을 사용한다.
addChild 는 부모 컴포넌트에서 선언한 메서드를 그대로 이용하면 되기 때문에 props.addChild 를 이용한다.







전체코드

TreeExample.vue

<script setup>
import { ref } from "vue";
import TreeView from "./TreeView.vue";

const treeData = ref({
  name: "My Tree",
  children: [
    { name: "hello" },
    { name: "world" },
    {
      name: "child folder",
      children: [
        {
          name: "child folder",
          children: [{ name: "hello" }, { name: "world" }],
        },
        { name: "hello" },
        { name: "world" },
        {
          name: "child folder",
          children: [{ name: "hello" }, { name: "world" }],
        },
      ],
    },
  ],
});

function addChildToTreeData(model) {
  model.children.push({ name: "new stuff" });
}
</script>

<template>
  <ul>
    <TreeView
      class="item"
      :model="treeData"
      :addChild="addChildToTreeData"
    ></TreeView>
  </ul>
</template>

<style>
.item {
  cursor: pointer;
  line-height: 1.5;
}
.bold {
  font-weight: bold;
}
</style>

TreeView.vue

<script setup>
import { ref, computed, defineProps } from "vue";

const props = defineProps({
  model: Object,
  addChild: Function,
});

const isOpen = ref(false);

const isFolder = computed(() => {
  //자식이 존재한다면 isFolder 를 + 로
  return props.model.children && props.model.children.length;
});

function toggle() {
  isOpen.value = !isOpen.value;
}
function addChild() {
  props.addChild(props.model);
  //   props.model.children.push({ name: "new stuff" });
}
console.log("isFolder : ", isFolder);
</script>

<template>
  <li>
    <div :class="{ bold: isFolder }" @click="toggle">
      {{ model.name }}
      <span v-if="isFolder"> [{{ isOpen ? "-" : "+" }}] </span>
    </div>
    <ul v-if="isFolder" v-show="isOpen">
      <TreeView
        v-for="childModel in model.children"
        :model="childModel"
        :key="childModel.name"
        :addChild="props.addChild"
      ></TreeView>
      <li @click="addChild">+</li>
    </ul>
  </li>
</template>
<style></style>



마치며

기존 코딩을 할 때 재귀는 까다로운 알고리즘이라고 생각한 적이 있는데, vue 를 이용하는 법도 꽤나 어려웠다. 필요한 데이터를 부수적으로 추가할 수 있어야 하고, vue 특성상 부모의 메서드를 함부로 건드릴 수 없다는 것을 잘 알게 되었다.
다음은 간단한 todo 웹 예제를 살펴볼 예정이다.



0개의 댓글