오늘도 커밋하는 북극곰

점점 이도저도 아닌 개발자가 되어가는중

개발/Vue

[Vuetify] v-treeview로 전체 열기/닫기 하려다가 결국 재귀 컴포넌트로 바꾼 이야기

미니백곰 2025. 4. 27. 15:23

1. 하고 싶었던 것

매체 카테고리 트리 구조를 Vue 3 + Vuetify 3로 구현하고 있었는데

카테고리 리스트가 트리 구조라서

“모두 펼치기” / “모두 닫기” 기능이 필요했다.

 

Vuetify 3에 v-treeview가 있어서

v-model:open으로 열고 닫을 노드 ID 배열만 넘겨주면 된다길래

“오 이거 쉽게 되겠네” 하고 가볍게 시작했다.

 

 

2. 첫 번째 시도 ( v-treeview + open 배열)

<v-treeview
  :items="mediaItemCategoryVo"
  v-model:open="expandedItems"
  item-key="id"
  item-children="children"
/>
expandAll() {
  // 부모 노드만 id
  this.expandedItems = this.mediaItemCategoryVo.map(item => item.id);
}

 

근데 막상 써보니까

부모 노드만 열리고, 자식 노드는 안 열린다.

 

3. 그러면 하위까지 다 넣으면 되지 않을까?

collectAllNodeIds(items: any[]): string[] {
  const ids: string[] = [];
  items.forEach(item => {
    ids.push(item.id);
    if (item.children?.length) {
      ids.push(...this.collectAllNodeIds(item.children));
    }
  });
  return ids;
}

expandAll() {
  this.expandedItems = this.collectAllNodeIds(this.mediaItemCategoryVo);
}

부모 + 자식 id 전부 넣어줬다.

그래도 안 열림.

 

4. 대체 왜 안 되는 건데?

내부 코드를 살펴보니

if (this.open.includes(this.item.id)) {
  // 해당 노드만 열림
  // 자식은 무시
}

아… 이거

open 배열에 id가 있어도 그 노드만 열고,

자식은 알아서 열리는 구조가 아님.

 

내가 기대했던

“부모가 열리면 자식도 재귀적으로 같이 열려야 한다”

 

이 로직 자체가 없음.

 

5. 이건 그냥 내가 방법을 모르는 게 아니라, 애초에 구조가 안 되는 거다

처음에는 내가 뭔가 잘못 쓴 줄 알았는데,

찾아보니까 Vuetify 3에서는

Vuetify 2에 있던 updateAll 같은 API도 없음.

this.treeview.updateAll is not a function
TypeError: this.treeview.updateAll is not a function

이 에러까지 보고

아… 이거는 구조적으로 애초에 전체 열기/닫기가 불가능하구나

확신이 들었다.

6. 결국 재귀 컴포넌트로 갈아탔다

억지로 우회해서 v-treeview를 살릴까 고민도 해봤지만

이런 뻔한 기능 하나 못 하면서 쓰기에는 애매해서

깔끔하게 재귀 컴포넌트로 직접 구현하는 쪽으로 방향을 바꿨다.

 

7. 최종 구현 코드

부모:  MediaCategoryComponent.vue

<ProductCategoryNode
  v-for="item in mediaItemCategoryVo"
  :key="item.id"
  :node="item"
  v-model:expanded="expandedItems"
/>
expandAll() {
  this.expandedItems = this.collectAllParentIds(this.mediaItemCategoryVo);
}

collapseAll() {
  this.expandedItems = [];
}

 

자식: ProductCategoryNode.vue

@Prop({ required: true }) readonly node!: any;
@Prop({ required: true }) readonly expanded!: string[];

get isExpanded(): boolean {
  return this.expanded.includes(String(this.node.id));
}

@Emit('update:expanded')
toggleExpand(): string[] {
  if (!this.node.children?.length) {
    return this.expanded; // leaf 노드는 열지 않음
  }

  const id = String(this.node.id);
  const expandedCopy = [...this.expanded];

  return this.isExpanded
    ? expandedCopy.filter(item => item !== id)
    : [...expandedCopy, id];
}

 

마무리 (배운 점)

 

이번에 느낀 건

 

“왜 안 되지?“ 라는 고민보다 “이 구조에서 되는 기능인가?“ 를 먼저 확인해야 한다.

 

라이브러리를 쓰면서

막히는 포인트가 기능이 없는 건지, 내가 방법을 모르는 건지

구분 못 하고 오래 헤매면 시간만 버린다.

 

애초에 구조적으로 불가능한 걸 억지로 해결하려다 시간 낭비했는데,

결국 재귀 컴포넌트로 구현하니까

커스터마이징도 훨씬 자유롭고 마음이 편해졌다.

 

다음에는…

기능이 안 될 때는 구조부터 확인하자.

안 되는 걸 붙잡고 있는 시간보다 방향 전환이 빠르다.