본문 바로가기

FrontEnd

Vue3 컴포넌트 디자인 패턴 : Scoped Slots

반응형

Scoped Slot을 이용해 하위 컴포넌트의 데이터에 접근하는 방법을 알아봅니다.

 

이전 시리즈

2022.12.21 - [Vue.js] - Vue3 컴포넌트 디자인 패턴 소개

 

Vue3 컴포넌트 디자인 패턴 소개

vue mastery의 컴포넌트 디자인 패턴(component-design-patterns) 강의를 학습하고 정리한 내용입니다. 디자인 패턴에 관하여 어제 한 후배한테 실무에서 디자인 패턴을 사용하냐는 질문을 들었는데, 리액

itchallenger.tistory.com

2022.12.22 - [Vue.js] - Vue3 컴포넌트 디자인 패턴 : Props

 

Vue3 컴포넌트 디자인 패턴 : Props

컴포넌트 디자인 패턴은 컴포넌트를 개발하면서 마주치게 되는 특정 문제를 해결하면서 소프트웨어의 유지보수성 / 재사용성을 높이는 개발을 위한 도구다. 이전 시리즈 링크 : 2022.12.21 - [분류

itchallenger.tistory.com

2022.12.22 - [분류 전체보기] - Vue3 컴포넌트 디자인 패턴 : Slots

 

Vue3 컴포넌트 디자인 패턴 : Slots

Vue3에서 slot을 사용하여 유연한 구조, 유연한 합성이 가능한 컴포넌트를 개발하는 방법을 알아봅니다. 이전 시리즈 2022.12.21 - [Vue.js] - Vue3 컴포넌트 디자인 패턴 소개 2022.12.22 - [분류 전체보기] -

itchallenger.tistory.com

Scoped Slots 소개

슬롯을 사용하면 개발자가 하위 컴포넌트에 콘텐츠를 유연하게 제공할 수 있습니다.
하위 컴포넌트가 상위 컴포넌트가 필요로 하는 데이터를 제어한다면 어떻게 접근할까요?
이러한 경우 범위 지정 슬롯(scoped slot)이 도움이 됩니다.

Scoped Slots이 뭐죠?

scoped 슬롯은 컴포넌트가 슬롯의 템플릿 블록에 데이터를 노출할 수 있도록 하는 기술입니다.
크레인 게임에 비유하면 우리는 슬롯을 통해 부모로부터 데이터를 넣을 수 있습니다.

하지만 자식의 데이터에 액세스하려는 경우 표준 슬롯은 해당 데이터를 가져올 수 있는 권한을 부여하지 않습니다.
우리는 원하는 대로 데이터를 잡아서 슬롯에 넣을 수 있도록 자식의 데이터를 노출해야 합니다.

다음과 같이 v-slot 디렉티브에 속성을 할당하면, 자식의 데이터를 부모에서 접근할 수 있습니다.

slotProps는 자식이 전달한 props를 다 갖고 있습니다.


Scoped Slots 사용하기

scoped 슬롯을 사용하는 방법을 알아보기 위해 header라는 이름을 가진 일반적인 named slot을 사용하겠습니다.

📄LogoHeader.vue

<template>
  <slot name="header" />
</template>
<script>
export default {
  data() {
    return {
      logoImage: '/images/logo.png'
    }
  }
}
</script>
이 시나리오에서는 logoImage 속성을 슬롯에 노출하려고 한다고 가정해 보겠습니다.
이를 위해 슬롯을 위한 prop(일반 컴포넌트와 마찬가지로)을 정의합니다.

📄LogoHeader.vue

<template>
  <slot name="header" :logo="logoImage" />
</template>
<script>
export default {
  data() {
    return {
      logoImage: '/images/logo.png'
    }
  }
}
</script>
이러한 "slot props"를 정의하면 slotProps 값을 노출하여 <template> 블록에서 액세스할 수 있습니다.

자식 컴포넌트에서 slot prop을 노출하면 <template> 블록 내에서 사용할 수 있습니다.

자식 컴포넌트에서 slot prop을 노출하면 <template> 블록 내에서 사용할 수 있습니다.

📄LogoHeader.vue

<template>
  <slot name="header" :logo="logoImage" />
</template>
<script>
export default {
  data() {
    return {
      logoImage: '/images/logo.png'
    }
  }
}
</script>

📄App.vue

<template>
  <LogoHeader>
    <template v-slot:header="slotProps">
      {{ slotProps.logo }}
    </template>
  </LogoHeader>
</template>

다시 한번 DIY

해당 흐름은 은근히 이해가 잘 안갑니다.

이전의 Library.vue 및 Book.vue 예제에서 scoped slot을 사용한 bookTitle 속성을 구현해 보겠습니다.

 

1단계. 자식 컴포넌트에서 원하는 슬롯에 데이터를 prop으로 노출

📄Book.vue

<template>
  <div class="book">
    <slot name="title" :bookTitle="bookTitle"></slot>
  </div>
</template>
<script>
export default {
  data() {
    return {
      bookTitle: 'Child Providing Data'
    }
  }
}
</script>

2단계.  부모 컴포넌트에서 <template> 블록에 slot prop 노출

📄Library.vue

<template>
  <Book>
    <template v-slot:title="slotProps">
      <!-- How do we get the bookTitle from Book.vue? -->
    </template>
  </Book>
</template>

3단계.  부모 컴포넌트에서 <template> 블록에서 원하는 slot prop 사용

📄Library.vue

<template>
  <Book>
    <template v-slot:title="slotProps">
      {{ slotProps.bookTitle }}
    </template>
  </Book>
</template>

제한사항과 모범사례

왜 Scoped Slot인가요?

v-slot 디렉티브를 적용한 슬롯으로 prop 적용 범위가 한정되기 때문입니다.

슬롯 프로퍼티의 바인딩 흐름

자식 컴포넌트부터 살펴보겠습니다.

📄Book.vue

<template>
  <div class="book">
    <slot name="title" :bookTitle="bookTitle" />
  </div>
</template>
<script>
export default {
  data() {
    return {
      bookTitle: 'Child Providing Data'
    }
  }
}
</script>
title 슬롯에 bookTitle 데이터를 제공한 다음 상위 컴포넌트에서 액세스합니다.

📄Library.vue

<template>
  <Book>
    <template v-slot:title="slotProps">
      <h1>{{ slotProps.bookTitle }}</h1>
    </template>
  </Book>
</template>

slotProps를 통해 노출된 bookTitle 속성에 대한 일반적인 오해는
상위 컴포넌트의 method, computed 또는 기타 위치에서 사용할 수 있다는 것입니다.

 

따라서 Library.vue 컴포넌트에 대문자 제목을 만들고자 하는 경우 다음과 같은 코드를 작성할 수 있습니다.

📄Library.vue

<template>
  <Book>
    <template v-slot:title="slotProps">
      <h1>{{ slotProps.bookTitle }}</h1>
    </template>
  </Book>
</template>
<script>
export default {
  computed: {
    uppercaseTitle() {
      // 🛑THIS DOES NOT WORK
      this.slotProps.bookTitle.toUpperCase()
    }
  }
}
</script>
그러나 자식이 부모에게 노출하는 데이터는 슬롯 템플릿 블록으로만 범위가 지정되기 때문에 동작하지 않습니다.
이는 불편해 보일 수 있지만 궁극적으로 각 컴포넌트 관심사를 올바른 위치에 유지하도록 권장함으로써 개발자에게 도움이 됩니다.
즉, Library.vue 컴포넌트에서 계산된 속성을 생성하려고 시도하는 대신
해당 논리를 Book.vue 내부에 유지히며 슬롯 prop을 통해 노출하는 것이 더 일관성이 있습니다.
 

📄Book.vue

<template>
  <div class="book">
    <slot name="title" 
      :bookTitle="bookTitle" 
      :uppercaseBookTitle="uppercaseTitle"
    />
  </div>
</template>
<script>
export default {
  data() {
    return {
      bookTitle: 'Child Providing Data'
    }
  },
  computed: {
    uppercaseTitle() {
      return this.bookTitle.toUpperCase()
    }
  }
}
</script>
다시 말하지만 slot props를 통해 노출되는 모든 데이터는 슬롯 템플릿으로 접근 범위가 제한됩니다.

Slot Props 구조분해

개발자는 코드를 더 간결하게 만드는 방법, 즉 코드를 줄이는 방법을 찾는 것이 일반적입니다.
그리고 slotProps는 JavaScript 객체를 생성하기 때문에 ES6 구조 분해를 사용하여 코드를 좀 더 읽기 쉽게 만들 수 있습니다.

📄Library.vue (before)

<template>
  <Book>
    <template v-slot:title="slotProps">
      <h1>{{ slotProps.bookTitle }}</h1>
    </template>
  </Book>
</template>

📄Library.vue (after)

<template>
  <Book>
    <template v-slot:title="{ bookTitle }">
      <h1>{{ bookTitle }}</h1>
    </template>
  </Book>
</template>​

단일 v-slot 축약 문법

위에서 v-slot은 템플릿 요소에만 적용해야 한다고 했으나 사실 거짓말입니다😅.
기술적으로 예외가 있으며 지금부터 살펴보겠습니다.

Book.vue 컴포넌트에 단일 title 슬롯이 있는 것처럼 컴포넌트가 단일 슬롯과 함께 사용되는 경우,

📄Library.vue (before v-slot abbreviated shorthand)

<template>
  <Book>
    <template v-slot:title="{ bookTitle }">
      <h1>{{ bookTitle }}</h1>
    </template>
  </Book>
</template>
컴포넌트에 직접 v-slot 지시문을 직접 적용한 뒤 템플릿 블록을 제거할 수 있습니다.

📄Library.vue (with v-slot abbreviated shorthand)

<template>
  <Book v-slot:title="{ bookTitle }">
    <h1>{{ bookTitle }}</h1>
  </Book>
</template>
그러나 다른 슬롯을 사용해야 하는 경우 동작하지 않습니다.
예를 들어 Book.vue 컴포넌트에 추가 description 슬롯이 있는 경우입니다.
다음과 같은 코드를 작성하고 싶은 유혹을 느낄 수 있습니다.

📄Library.vue (with multiple slots and incorrect syntax)

<template>
  <Book v-slot:title="{ bookTitle }">
    <h1>{{ bookTitle }}</h1>    
    <!-- 🛑THIS DOES NOT WORK -->
    <template v-slot:description>
      <p>My Description</p>
    </template>
  </Book>
</template>
이것은 동작하지 않습니다! 하나 이상의 슬롯이 필요한 순간 ​​템플릿 블록으로 돌아가야 합니다.

📄Library.vue (with multiple slots)

<template>
  <Book>
    <template v-slot:title="{ bookTitle }">
      <h1>{{ bookTitle }}</h1>
    </template>
    <template v-slot:description>
      <p>My Description</p>
    </template>
  </Book>
</template>

그래서 저는 그냥 항상 탬플릿 블록을 사용하는 것을 권장합니다.

참고

이와 같이 scoped slot을 사용하는 패턴을 렌더리스 컴포넌트 패턴이라 한다.

https://adamwathan.me/renderless-components-in-vuejs/

 

Renderless Components in Vue.js

Software developer, author, and host of Full Stack Radio. March 22, 2018

adamwathan.me

 

 

반응형