Vue.js 재사용성을 높이자! Slot편

Minho Yoo·2022년 10월 5일
4

Vue.js

목록 보기
5/12
post-thumbnail

Vue.js 프레임워크에서 제공하는 함수들로 컴포넌트의 재사용성을 높일 수 있다.
재사용성을 높이게 되면 코딩할 때 편하고 배우는것도 많으니 배우고나서 실전에 써보는걸 추천한다.

슬롯

슬롯(slot)은 컴포넌트의 재사용성을 높여주는 기능이다.
특정 컴포넌트에 등록한 하위 컴포넌트의 마크업을 확장하거나 재정의할 수 있다.

슬롯 코드 형식

<!-- ButtonTab.vue -->
<template>
  <div class="tab penel">
    <!-- 탭 헤더 -->
    <slot></slot>
    <!-- 탭 본문 -->
    <div class="content">
      Tab Contents
    </div>
  </div>
</template>

위 코드는 ButtonTab 컴포넌트의 코드다.
탭을 구현한다고 생각하고 탭 헤더와 본문을 구분하는 태그를 작성하였다.
여기서 탭 헤더에 들어갈 구체적인 태그를 정하지 않고 일단 slot 태그로 빈 칸을 남겨놓는다.
만약 이 컴포넌트를 등록한 상위 컴포넌트에서 slot 태그 영역을 구현하지 않으면 해당 부분은 공백으로 표시된다.
slot 태그의 위치에 주목하면서 ButtonTab 컴포넌트를 TabContainer 컴포넌트의 하위 컴포넌트로 등록한다.

<!-- TabContainer.vue -->
<template>
  <button-tab>
    <!-- slot 영역 -->
    <h1>First Header</h1>
  </button-tab>
  <button-tab>
    <!-- slot 영역 -->
    <h1>Second Header</h1>
  </button-tab>
  <button-tab>
    <!-- slot 영역 -->
    <h1>Third Header</h1>
  </button-tab>
</template>

<script>
  export default {
  	conponent: {
  		ButtonTab
  	}
  }
</script>

TabContainer 컴포넌트에 ButtonTab 컴포넌트를 등록하고 ButtonTab 컴포넌트를 세 곳에 표시했다.
여기서 button-tab 컴포넌트 태그의 안에 각기 다른 헤더의 내용을 정의했다.
만약 ButtonTab 컴포넌트에 slot 태그를 정의하지 않았다면 컴포넌트를 등록하는 시점에 마크업을 재정의할 수 는 없었을 것이다.

이처럼 슬롯을 사용하면 컴포넌트의 특정 마크업 영역을 재정의하여 같은 컴포넌트를 각기 다르게 표현 할 수 있다.

디자인(CSS)이 똑같은 컴포넌트의 경우에 매우 편함 ex) 버튼, 테이블, 메뉴

Named Slots

위에서는 슬롯의 개념을 이해하기 위해 1개의 슬롯만 사용했다.
슬롯은 name 속성을 지정하여 여러개 사용할 수 있다.
전의 예제에 네임드 슬롯을 적용해보면 다음과 같다.

<!-- ButtonTab.vue -->
<template>
  <div class="tab panel">
    <!-- 탭 헤더 -->
    <slot name="header"></slot>
    <!-- 탭 본문 -->
    <slot name="content"></slot>
  </div>
</template>
<!-- TabContainer.vue -->
<template>
  <button-tab>
    <!-- slot 영역 -->
    <h1 slot="header">First Header</h1>
    <div slot="content" class="content">Tab Content #1</div>
  </button-tab>
  <button-tab>
    <!-- slot 영역 -->
    <h1 slot="header">Second Header</h1>
    <div slot="content" class="content">Tab Content #2</div>
  </button-tab>
  <button-tab>
    <!-- slot 영역 -->
    <h1 slot="header">Third Header</h1>
    <div slot="content" class="content">Tab Content #2</div>
  </button-tab>
</template>

하위 컴포넌트에서 정의한 슬롯 태그 영역에 마크업을 재정의할 때 위와 같이 HTML 표준 태그를 사용하는 방법도 있지만 아래와 같이 template 태그를 사용할 수도 있다.

<button-tab>
  <!-- slot 영역 -->
  <template slot="header">
    <h1>First Header</h1>
  </template>
  <template slot="content">
    <div class="content">Tab Contents #1</div>
  </template>
</button-tab>

Vue.js 2.6 버전부터 Named Slots 문법이 바뀌었다.

<!-- 기존(2.5 이하) -->
<template slot="header">
  <h1>First Header</h1>
</template>
<!-- 이후(2.6 이상) -->
<template v-slot:header>
  <h1>First Header</h1>
</template>

스콥드 슬롯

슬롯(slot)이 컴포넌트 템플릿의 재사용성을 늘려주기 위한 기능이라면 스콥드 슬롯(Scoped slot)은 컴포넌트 데이터의 재사용성을 높여주는 기능이다.
일반적으로 뷰에서는 프롭스 속성이나 이벤트 발생과 같은 컴포넌트 통신 방식을 제외하고는 다른 컴포넌트의 값을 참조할 수 없다.
하지만 스콥드 슬롯은 하위 컴포넌트의 값을 상위 컴포넌트에서 접근하여 사용할 수 있다.

스콥드 슬롯 코드 형식

스콥드 슬롯을 사용하기 위해서는 기본적으로 상위, 하위 2개의 컴포넌트가 필요하고 각각의 컴포넌트에 아래와 같이 구현해야한다.

<!-- 하위 컴포넌트의 슬롯 태그 -->
<slot :상위 컴포넌트로 전달할 속성 이름="하위 컴포넌트의 데이터"></slot>

먼저 하위 컴포넌트에서 slot 태그를 정의하고 v-bind 디렉티브의 양식 문법인 :를 이용하여 하위 컴포넌트의 데이터를 연결한다.
컴포넌트 통신 방법에서 배운 프롭스 속성 전달 방법과 비슷한 형식이다.

이제 상위 컴포넌트를 보면 다음과 같다.

<!-- 상위 컴포넌트에 등록된 하위 컴포넌트 태그 부분 -->
<child-component>
  <template slot-scope="임의의 변수">
    {{ 임의의 변수. 상위 컴포넌트로 전달할 속성 이름 }}
  </template>
</child-component>

하위 컴포넌트인 child-component 태그를 보면 위에서 slot 태그를 미리 정의해놨기 때문에 child-component 태그 사이에 들어가는 코드는 모두 슬롯으로 처리가 된다.
여기서 slot-scope 라는 속성으로 하위 컴포넌트에서 올려보내준 데이터를 전달 받을 수 있다.

slot-scope 속성이 정의한 template 태근느 실제 DOM이 아니라 속성을 전달받기 위한 껍데기다.
실제로 화면 렌더링이 되고 나면 template 태그는 가시적인 태그로 변환하지 않는다.

스콥드 슬롯 코드 예제

앞에서 살펴본 코드 형식을 참고하여 간단한 예제를 만들어보자.
상위 컴포넌트는 App.vue이고 하위 컴포넌트는 ChildComponent.vue 이다.

하위 컴포넌트의 코드부터 보겠다

<!-- ChildComponent.vue -->
<template>
  <div>
    <h1>Child Component</h1>
    <slot :message="message"></slot>
  </div>
</template>

<script>
  export default {
  	data() {
  		return {
  			message: 'Hello'
  		}
  	}
  }
</script>

하위 컴포넌트에 slot 태그를 정의하고, v-bind 디렉티브 (:)를 이용하여 message 속성을 전달하였다.

이번엔 상위 컴포넌트의 코드를 살펴 보겠다

<!-- App.vue -->
<template>
  <div>
    <child-component>
      <template slot-scope="scopeProps">
        {{ scopeProps.message }}
      </template>
    </child-component>
  </div>
</template>

<script>
  import ChildComponent from './ChildComponent.vue';
  
  export default {
  	components: {
  		ChildComponent
  	}
  }
</script>

하위 컴포넌트인 child-component 태그의 슬롯 영역에 slot-scope 속성을 정의하였다.
여기서 사용한 변수 scopedProps는 하위 컴포넌트에서 올려준 값을 받기 위한 임의의 변수이다.
임의의 변수이기 때문에 원하는 변수 명을 지정하여 사용할 수 있다.
scopeProps 에는 하위 컴포넌트에서 올려준 값 message이 들어있다.

위의 코드를 실행하면 결론적으로 h1 제목 태그와 함께 하위 컴포넌트의 message 값인 Hello가 화면에 출력된다.

뷰 버전 2.6에서 슬롯의 문법이 v-slot 디렉티브로 바뀐 이유는 스콥스 슬롯의 변수 접근 범위 때문이다.
일반 HTML 태그에서 스콥드 슬롯을 적용하면 이해하는데 크게 문제가 없으나 컴포넌트 태그에 스콥드 슬롯을 적용하는 경우 변수의 접근 범위에 대해 혼란이 있을 수 있다.
따라서, 기존 레거시 코드와 겹치지 않게 v-slot#으로 개편하였다.

v-slot

Vue 2.6.0에서 v-slot 디렉티브가 새롭게 소개되었다.
v-slot은 네임드 슬롯(Named Slots)과 스콥드 슬롯(Scoped Slot) 사용을 통합한 새로운 디렉티브이다.
기존의 스콥드 슬롯(Scoped Slot) 표현은 Vue 3.0에서는 사라진다.

이전 버전에서 스콥드 슬롯이 가지고 있었던 문제점과 함께 v-slot 디렉티브가 어떻게 그 문제점을 해결했는지 살펴보자.

스콥드 슬롯의 문제점

Vue.js에서 슬롯을 사용하는 이유는 재사용성이 높은 컴포넌트를 설계하기 쉽기 때문이다.
함수형 프로그래밍에서 함수를 작성할 때 단일 책임 원칙(Single Responsibility Principle) 을 지켜주면 좋은 것처럼 Vue.js 컴포넌트를 설계할 때에도 하나의 컴포넌트가 하나의 역할을 할 수 있도록 설계하는 것이 좋다.
스콥드 슬롯은 슬롯을 사용할 때, 하위 컴포넌트의 값을 상위 컴포넌트에서 참조할 수 있게 한다.
상위 커포넌트의 데이터를 하위 컴포넌트 슬롯으로 바인딩 해주는 것이 자연스러워 보이지만, 하위 컴포넌트가 단일 책임을 지기 위해 데이터를 가지고 있어야 한다.
그리고 데이터를 상위에서 참조해야 할 때가 있는데 그때 사용하는 것이 스콥드 슬롯이다.

하지만 Vue.js에서 스콥드 슬롯을 소개했을 때, slot-scope 속성을 추가한 template 태그를 사용해 구문이 길어지는 문제가 있다.

<!-- 하위 컴포넌트(Foo.vue) -->
<template>
  <div>
    <slot :fooProps="fooProps"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      fooProps: {
        id: 1,
        msg: 'Hello',
      } 
    }
  }
}
</script>
<!-- 상위 컴포넌트 -->
<template>
  <foo>
    <template slot-scope="fooProps">
      <div>{{ fooProps }}</div>
    </template>
  </foo>
</template>

<script>
import Foo from './Foo.vue';

export default {
  components: {
    Foo
  }
}
</script>

스콥드 슬롯을 사용하기 위해서 template 태그를 항상 사용해야 했기 때문에 Vue 2.5에서는 스콥드 슬롯을 template 태그 대신, 표현하고자 하는 태그에 직접 작성할 수 있도록 변경되었다.

<foo>
  <div slot-scope="fooProps">{{ fooProps }}</div>
</foo>

여기서 눈여겨봐야 할 부분은 하위 컴포넌트인 foo 태그의 프롭스 속성이 상위 컴포넌트의 div 태그로 전달되었다는 점이다.

또한, HTML의 기본 태그뿐 아니라 컴포넌트 태그에서도 스콥드 슬롯을 표현할 수 있다.

<foo>
  <bar slot-scope="fooProps">{{ fooProps }}</bar>
</foo>

하지만 위와 같이 사용했을 때, 컴포넌트의 복잡성이 늘어나면 어떤 컴포넌트의 slot-scope 변수를 사용하는지 명확하지 않다는 문제점이 발생한다.

<foo>
  <bar slot-scope="fooProps">
    <baz slot-scope="barProps">
      <div slot-scope="bazProps">
        {{ fooProp }} {{ barProp }} {{ bazProp }}
      </div>
    </baz>
  </bar>
</foo>

자식 컴포넌트인 foo 태그가 스콥드 슬롯을 사용하기 위해 bar 태그에 slot-scope를 사용했고, 이어서 bar 태그는 이어서 baz 태그에 사용했다.
이렇게 중첩된 컴포넌트에서 스콥드 슬롯을 사용하게 되면 어떤 컴포넌트가 어떤 props을 올려주고 있는지 명확하지 않게 된다.

즉 2.5 버전에서 스콥드 슬롯을 사용할 때 slot-scope 속성을 template 태그에만 사용하는 것이 아니라, 기존 태그에 사용할 수 있도록 허용한 것이 문제가 되었다.
따라서 v-slot 디렉티브가 등자하게 되었다.

Vue 3.0에서는 slot-scope 문법이 제거되었기 때문에, Vue 3.0에서 slot-scope를 사용하면 eslint가 v-slot으로 변환해준다. (Vue CLI를 이용해 Vue 3.0을 설치했을 때)

v-slot 기본 사용 방법

v-slot은 슬롯에 name 속성을 지정하여 여러 개 사용할 수 있도록 하는 네임드 슬롯(Named Slots)과 스콥드 슬롯을 한 번에 표시할 수 있다.

네임드 슬롯 표현

<!-- 하위 컴포넌트(Foo.vue) -->
<div>
  <!-- 헤더 영역 -->
  <slot name="header"></slot>
  <!-- 본문 영역 -->
  <slot name="content"></slot>
  <!-- 푸터 영역 -->
  <slot name="footer"></slot>
</div>
<!-- 상위 컴포넌트 -->
<foo>
  <!-- 헤더 영역 -->
  <template v-slot:header>
    <h1>Header</h1>
  </template>
  
  <!-- 본문 영역 -->
  <template v-slot:default>
    <div>Body</div>
  </template>
  
  <!-- 푸터 영역 -->
  <template name="footer">
    <div>Footer</div>
  </template>
</foo>

v-slot:default는 슬롯에 name 속성을 붙이지 않은 영역에 표현된다.

렌터링 된 HTML은 다음과 같다.

<h1>Header</h1>
<div>Body</div>
<div>Footer</div>

스콥드 슬롯 표현

<!-- 하위 컴포넌트(Foo.vue) -->
<template>
  <slot :msg="msg"></slot>
</template>

<script>
export default {
  data() {
    return {
      msg: 'Hello!',
    };
  },
};
</script>
<!-- 상위 컴포넌트 -->
<foo>
  <template v-slot:default="slotProps">
    <h1>{{ slotProps.msg }}</h1>  <!-- <h1>Hello!</h1> -->
  </template>
</foo>

v-slot의 이점

template 태그를 사용함으로써 장황해지는 표현을 간략하게 할 수 있으며, 덕분에 가독성 또한 좋아진다.

<!-- old -->
<foo>
  <template slot-scope="{ msg }">
    {{ msg }}
  </template>
</foo>
<!-- new -->
<foo v-slot="{ msg }">{{ msg }}</foo>

어떤 컴포넌트의 스콥드 슬롯인지 명확하게 표현이 된다.

<!-- old -->
<foo>
  <bar slot-scope="foo">
    <baz slot-scope="bar">
      <div slot-scope="baz">
        {{ foo }} {{ bar }} {{ baz }}
      </div>
    </baz>
  </bar>
</foo>
<!-- new -->
<foo v-slot="foo">
  <bar v-slot="bar">
    <baz v-slot="baz">
      {{ foo }} {{ bar }} {{ baz }}
    </baz>
  </bar>
</foo>

v-slot 응용 표현 방식

축약 표현

스콥드 슬롯은 v-slot 뒤에 props를 작성해 주면 하위 태그에서 사용할 수 있게 된다.
앞서 Vue 2.5에서 문제가 되었던 template 태그를 다시 사용해야 한다는 단점이 생길 수 있다.
하지만, 여러 개의 슬롯을 사용하지 않고 default 슬롯만 사용한다면 아래와 같이 컴포넌트 자체가 v-slot 슬롯을 지정해 사용할 수 있다.

<foo v-slot:default="slotProps">
  <h1>{{ slotProps.msg }}</h1>
</foo>

하지만 슬롯이 여러 개일 경우는 반드시 template 태그를 사용해 컴포넌트 하위에 표현해 주어야 한다.

또한 v-slot:defaultv-slot으로 축약해서 표현할 수 있다.

<foo v-slot="slotProps">
  <h1>{{ slotProps.msg }}</h1>
</foo>

단, 컴포넌트 자체에 v-slot을 사용하고, 하위에 이름을 가진 v-slot을 사용할 수는 없다.
스콥드 슬롯의 변수 범위가 모호해지기 때문이다.
아래의 예시 코드는 default 슬롯이 foo 컴포넌트에 선언되었고, 하위에 other 이름을 가진 슬롯이 존재한다.

<!-- v-slot:other이 있기 때문에 오류 발생 -->
<foo v-slot="slotProps">
  {{ slotProps.msg }}
  <template v-slot:other="otherSlotProps">
    <!-- 이곳에 slotProps를 적용할 수 없음. -->
  </template>
</foo>

위 코드가 동작하려면 template 태그로 명확한 범위를 지정해 주어야 한다.
그리고, default name을 가진 v-slot 축약 문법은 template 태그로 명확한 범위를 지정해 준다면, 이름을 가진 슬롯 (v-slot:other 등)과 혼용해서 사용할 수 있다.

<foo>
  <template v-slot="slotProps">
    {{ slotProps.msg }}
  </template>
  <template v-slot:other="otherSlotProps">
    {{ otherSlotProps.something }}
  </template>
</foo>

특수 기호 표현

v-slotv-bind(:), v-on(@)과 같이 특수 기호를 통해 나타낼 수 있다.
특수 기호는 #이다.
예를 들어 v-slot:default#default로 표현될 수 있다.

<foo #default="slotProps">
  <h1>{{ slotProps.msg }}</h1>
</foo>

#="slotProps"구문은 불가능하다.
특수 기호로 표현하고 싶다면, #default="slotProps" 와 같이 반드시 슬롯 이름을 지정해 주어야 한다.

Destructuring 표현

스콥드 슬롯의 변수에 ES6 문법인, 구조 분해 문법(Destructuring) 표현도 가능하다.

<foo v-slot="{ msg }">
  <h1>{{ msg }}</h1>
</foo>

Dynamic Slot Names 표현

슬롯 name을 동적으로 표현할 수 있다.

<foo>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</foo>

스콥드 슬롯의 slot-scope 표현은 Vue 3.0 이하의 버전에서는 계속 사용할 수 있다.
단 Vue 3.0 이상 버전에서 삭제되었으므로 Vue 2.6 이상을 사용하고 있다면, v-slot 디렉티브를 통해 슬롯을 사용하도록 권장한다.

profile
Always happy coding 😊

0개의 댓글