Vue Recursive, Circular reference 컴포넌트 만들기

dante Yoon·2023년 11월 11일
1

vue

목록 보기
3/5
post-thumbnail

Recursive (재귀) 컴포넌트 만들기

트리 구조의 데이터가 주어졌을때 폴더 구조의 컴포넌트를 렌더링하려고 합니다.

Vue2 Self-reference

Recursive components are those that call themselves in their template. This is common in scenarios where you have a nested data structure, like a tree view where each node might contain other nodes.

재귀 컴포넌트는 template 태그 내부에서 정의된 자기 자신을 다시 선언합니다. 폴더 구조의 데이터를 처리할 때 자주 사용됩니다.
아래 TreeNode 컴포넌트는 재귀 컴포넌트입니다.

<script>
// TreeNode.vue
export default {
  props: {
    treeData: {
      type: Array,
      required: true,
    },
  },
};
</script>

<template>
  <ul>
    <li v-for="item in treeData" :key="item.id">
      {{ item.name }}
      <!-- children이 있는지 확인하고 재귀 형식으로 다시 TreeNode를 렌더링합니다. -->
      <TreeNode
        v-if="item.children && item.children.length"
        :treeData="item.children"
      />
    </li>
  </ul>
</template>

TreeNode 컴포넌트는 상위 컴포넌트인 App에서 아래와 같이 표기됩니다.

<template>
  <div id="app">
    <TreeNode :treeData="nestedData" />
  </div>
</template>

<script>
import TreeNode from "./components/TreeNode.vue";

const nestedData = [
  {
    id: 1,
    name: "Folder 1",
    children: [
      { id: 2, name: "File 1-1" },
      { id: 3, name: "File 1-2" },
    ],
  },
  {
    id: 4,
    name: "Folder 2",
    children: [
      { id: 5, name: "File 2-1" },
      {
        id: 6,
        name: "Folder 2-1",
        children: [
          {
            id: 7,
            name: "File 2-1-1",
            children: [
              {
                id: 8,
                name: "Folder 2-1-1",
                children: [{ id: 9, name: "File 2-1-1-1" }],
              },
            ],
          },
        ],
      },
    ],
  },
];

export default {
  name: "App",
  components: {
    TreeNode,
  },
  data() {
    return {
      nestedData,
    };
  },
};
</script>

Vue2

Vue2 에서 SFC를 사용했을 때 재귀 컴포넌트는 항상 name을 options api작성할때 포함시켜야 합니다. 지금 TreeNode에는 name 옵션이 없기 때문에 아래와 같이 정상적으로 렌더링되지 않습니다.

Vue 2.7.5 버전을 사용합니다.

<script>
export default {
  name: "TreeNode",
  props: {
    treeData: {
      type: Array,
      required: true,
    },
  },
};
</script>

<template>
  <ul>
    <li v-for="item in treeData" :key="item.id">
      {{ item.name }}
      <!-- Check if the item has children and recursively call TreeNode component -->
      <TreeNode
        v-if="item.children && item.children.length"
        :treeData="item.children"
      />
    </li>
  </ul>
</template>

name 옵션을 파일 이름과 동일하게 작성한 후에야 정상적으로 렌더링됩니다.

Vue3

Vue 3.3.4 버전을 사용했습니다.

webpack이나 Vite.js 모두 각 번들러 플러그인에서 script, template, style 태그를 파싱할 때 파일 이름까지 전달받기 때문에 컴포넌트 이름을 명시적으로 작성하지 않아도 기본적으로 컴포넌트를 다른 곳에서 모듈로 참조하여 사용하는데 문제가 없습니다.

composition API를 사용하는 Vue3는 script setup 태그를 사용하여 컴포넌트를 작성하는 경우 별도 script 태그를 한번 더 작성해줌으로 컴포넌트 이름을 작성할 수 있습니다.

<script>
  export default {
    name: "TreeNode"
  }
</script>

<script setup>
// ..
</script>

먼저 <script setup> 부분은 여러 상황에서 동일하므로 먼저 공유합니다.

<script setup>
// TreeNode.vue
defineProps({
  treeData: {
    type: Array,
    required: false,
  }
})
</script>

non-dynamic component

컴포넌트가 자기 자신의 이름을 그대로 참조하는 경우는 name옵션을 사용하지 않아도 가능합니다. 별도 스크립트 태그를 작성하여 name 키값을 명시하지 않았습니다.

<template>
  <ul>
    <li v-for="item in treeData" :key="item.id">
      {{ item.name }}
      <TreeNode :treeData="item.children"></TreeNode>
    </li>
  </ul>
</template>

dynamic component - name을 명시하지 않았을 때는 정상적으로 표기되지 않습니다.

with-no-name specified

Vue3에서 재귀 컴포넌트를 작성할 때 name 누락이 문제가 되는 경우는 동적 컴포넌트를 사용했을 때입니다.

dynamic 컴포넌트를 사용했을 때는 name을 명시적으로 작성하지 않았을 때는 Vue2와 마찬가지로 정상적으로 표기가 되지 않습니다. vue-devtools로 렌더링된 컴포넌트의 계층 구조를 살펴봐도 하위 컴포넌트들이 렌더링되지 않음을 확인할 수 있습니다.

<template>
  <ul>
    <li v-for="item in treeData" :key="item.id">
      {{ item.name }}
      <component is="TreeNode" :treeData="item.children">
        <!-- content -->
      </component>
    </li>
  </ul>
</template>

Circular reference (순환참조)

A -> B

B -> A

두 컴포넌트가 렌더링하기 위해 각자 상대방을 참조하는 경우입니다.

각자가 상대 컴포넌트의 의존성이 되어 각 컴포넌트가 해석되기까지 상대 컴포넌트의 정보를 가져와야 하기 때문에 dependency resolution에서 이슈가 발생합니다.

src/
|-- components/
    |-- AComponent.vue
    |-- BComponent.vue
|-- App.vue
|-- main.js

AComponent, BComponent를 작성할 때 주의해야 할 점은 base case를 정해줘야 stack overflow 에러를 방지할 수 있습니다. 그렇지 않으면 maxiumum callstack exceeed가 콘솔로그에 표기될 것입니다.

문제가 발생하지 않게 순환참조 컴포넌트를 작성합니다.

AComponent.vue

// AComponent.vue
<template>
  <div>
    <h3>This is Component A</h3>
    <!-- Render BComponent conditionally -->
    <b-component
      v-if="shouldRenderB"
      @toggle-render="toggleRender"
    ></b-component>
  </div>
</template>

<script>
export default {
  name: "AComponent",
  components: {
    BComponent: () => import("./BComponent.vue"),
  },
  props: {
    shouldRenderB: {
      type: Boolean,
      default: true,
    },
  },
  methods: {
    toggleRender() {
      this.$emit("toggle-render");
    },
  },
};
</script>

AComponent, BComponent 모두 내부 로직은 동일합니다. 여기서 중요한 것은 v-if 디렉티브로 base case를 만들어 stack overflow를 사전에 차단해야 합니다. 또한 각 컴포넌트를 비동기적으로 로딩합니다.

components: {
  AComponent: () => import("./AComponent.vue")
}

methods 내부에는 이벤트 전달 함수인 toggleRender를 정의합니다. toggle-render 이벤트 커뮤니케이션이 부모 컴포넌트와 자식 컴포넌트간 일어나게 합니다. 이 이벤트를 발생하여 최상위 컴포넌트에서 렌더링을 조절하도록 합니다.

BComponent.vue

BComponent는 ACopmonent와 유사합니다.

// BComponent.vue
<template>
  <div>
    <h3>This is Component B</h3>
    <!-- Render AComponent conditionally -->
    <a-component
      v-if="shouldRenderA"
      @toggle-render="toggleRender"
    ></a-component>
  </div>
</template>

<script>
export default {
  name: "BComponent",
  components: {
    AComponent: () => import("./AComponent.vue"),
  },
  props: {
    shouldRenderA: {
      type: Boolean,
      default: false, // Initially set to false to avoid immediate rendering
    },
  },
  methods: {
    toggleRender() {
      this.$emit("toggle-render");
    },
  },
};
</script>

이 두 컴포넌트를 사용하는 App.vue 코드는 아래와 같이 생겼습니다.

app.vue

<template>
  <div id="app">
    <a-component
      :shouldRenderB="showB"
      @toggle-render="toggleComponents"
    ></a-component>
    <b-component
      :shouldRenderA="showA"
      @toggle-render="toggleComponents"
    ></b-component>
  </div>
</template>

<script>
import AComponent from "./components/AComponent.vue";
import BComponent from "./components/BComponent.vue";

export default {
  name: "App",
  components: {
    AComponent,
    BComponent,
  },
  data() {
    return {
      showA: true,
      showB: false,
    };
  },
  methods: {
    toggleComponents() {
      this.showA = !this.showA;
      this.showB = !this.showB;
    },
  },
};
</script>

코드 샌드박스를 통해 실제 컴포넌트가 화면에 어떻게 표기되는지 확인해보세요.

Synchronous Import vs Asynchronous Import

위에서 A,B 컴포넌트가 서로를 참조할 때 () => import("AComponent.vue")와 같이 표기합니다. 이러한 방식을 Asynchronous Import라고 합니다. 모듈을 비동기식으로 호출하는 것입니다.

Synchronous Import

표준 임포트 구문은 동기식입니다. 자바스크립트 엔진이 import 구문을 마주할 때 스크립트 나머지 부분의 실행을 중지하고 모듈이 로딩되고 평가될때까지 기다립니다. 보통의 경우 파일의 최상단 부분에서 작성하여 가져온 모듈을 사용합니다. 임포트 구문 보다 앞서서 모듈을 사용하려고 하면 에러가 발생하는 경험을 해보셨을 것입니다.

import AComponent from './AComponent.vue';

ASynchronous Import

비동기 임포트 구문은 뷰에서 동적 컴포넌트를 로딩할때 사용합니다. import() 구문을 사용하는 함수는 Promise를 반환하기 때문에 () => import("./AComponent.vue")는 비동기 함수입니다. 함수가 실행될 때 비동기적으로 모듈을 불러옵니다. 로딩이 비동기이기 때문에 다른 코드 실행이 멈추지 않습니다.

비동기 모듈 로딩을 하기 때문에 참조 순환 오류를 발생시키지 않고 두 컴포넌트를 불러올 수 있습니다.

<script>
export default {
  components: {
    // Asynchronously load the component
    LazyComponent: () => import('./LazyComponent.vue')
  }
}
</script>

다른 프레임워크와 마찬가지로 뷰에서도 Lazy Loading을 할 때 사용하기도 합니다.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글