Vue 슬롯 시스템 완벽 가이드: 기본 슬롯부터 스코프드 슬롯까지

슬롯의 핵심 개념

Vue의 슬롯(slot)은 컴포넌트 콘텐츠를 유연하게 배치할 수 있는 콘텐츠 분배 메커니즘이다. 부모 컴포넌트가 자식 컴포넌트의 특정 영역에 마크업을 주입할 수 있게 해주며, 이는 단순한 props 전달과는 다른 패러다임을 제공한다.

기본 슬롯 구현

가장 단순한 형태의 슬롯은 자식 컴포넌트 내에 <slot> 요소를 배치하는 것으로 시작한다.

자식 컴포넌트 (BaseCard.vue)

<template>
  <article class="card-wrapper">
    <header class="card-header">
      <h2>사용자 정보</h2>
    </header>
    <div class="card-body">
      <slot></slot>
    </div>
  </article>
</template>

<script>
export default {
  name: 'BaseCard'
}
</script>

부모 컴포넌트

<template>
  <div id="app">
    <base-card>
      <p>이름: 김철수</p>
      <p>직책: 프론트엔드 개발자</p>
    </base-card>
  </div>
</template>

<script>
import BaseCard from './BaseCard.vue'

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

부모가 제공한 <p> 태그들이 자식의 <slot></slot> 위치에 렌더링된다.

네임드 슬롯: 다중 콘텐츠 영역 관리

복수의 슬롯이 필요한 경우 name 속성으로 구분한다. Vue 2.6+에서는 v-slot 디렉티브를 사용한다.

자식 컴포넌트 (PageLayout.vue)

<template>
  <div class="layout-container">
    <aside class="sidebar">
      <slot name="navigation"></slot>
    </aside>
    <main class="content">
      <slot name="main"></slot>
    </main>
    <footer class="footer">
      <slot name="copyright"></slot>
    </footer>
  </div>
</template>

부모 컴포넌트

<template>
  <page-layout>
    <template v-slot:navigation>
      <nav>
        <ul>
          <li><a href="/">홈</a></li>
          <li><a href="/about">소개</a></li>
        </ul>
      </nav>
    </template>
    
    <template #main>
      <article>
        <h1>메인 콘텐츠</h1>
        <p>본문 내용이 여기에 표시됩니다.</p>
      </article>
    </template>
    
    <template #copyright>
      <p>© 2024 My Company</p>
    </template>
  </page-layout>
</template>

v-slot:main은 축약형 #main으로 작성할 수 있다.

스코프드 슬롯: 자식 데이터를 부모에 노출

스코프드 슬롯(Scoped Slot)은 자식 컴포넌트가 데이터를 슬롯 콘텐츠로 전달할 수 있게 한다. 이를 통해 부모가 자식의 내부 상태를 기반으로 렌더링 로직을 제어한다.

자식 컴포넌트 (UserList.vue)

<template>
  <div class="user-list">
    <slot 
      v-for="member in members" 
      :key="member.id"
      :userInfo="member"
      :isActive="member.status === 'online'"
    ></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      members: [
        { id: 1, name: '박지민', role: '개발팀장', status: 'online' },
        { id: 2, name: '이수진', role: '디자이너', status: 'offline' },
        { id: 3, name: '최민호', role: '백엔드', status: 'online' }
      ]
    }
  }
}
</script>

부모 컴포넌트: 다양한 표현 방식

<template>
  <div class="app">
    <h3>형식 1: 테이블 렌더링</h3>
    <user-list v-slot="{ userInfo, isActive }">
      <tr :class="{ active: isActive }">
        <td>{{ userInfo.name }}</td>
        <td>{{ userInfo.role }}</td>
        <td>{{ isActive ? '접속중' : '부재중' }}</td>
      </tr>
    </user-list>

    <h3>형식 2: 카드 그리드</h3>
    <user-list v-slot="slotProps">
      <div class="profile-card">
        <span class="badge" v-if="slotProps.isActive">LIVE</span>
        <h4>{{ slotProps.userInfo.name }}</h4>
        <p>{{ slotProps.userInfo.role }}</p>
      </div>
    </user-list>

    <h3>형식 3: 구조 분해 없이 객체 접근</h3>
    <user-list v-slot="data">
      <span>{{ data.userInfo.name }} - {{ data.isActive ? '활성' : '비활성' }}</span>
    </user-list>
  </div>
</template>

실전 패턴: 재사용 가능한 데이터 테이블

스코프드 슬롯의 대표적 활용 사례는 헤더/본문/푸터를 모두 커스터마이징할 수 있는 테이블 컴포넌트다.

DataTable.vue

<template>
  <table class="data-grid">
    <thead>
      <tr>
        <slot name="header" :columns="columnDefs">
          <th v-for="col in columnDefs" :key="col.key">
            {{ col.label }}
          </th>
        </slot>
      </tr>
    </thead>
    <tbody>
      <tr v-for="record in dataset" :key="record.id">
        <slot name="row" :item="record" :meta="{ index: dataset.indexOf(record) }">
          <td v-for="col in columnDefs" :key="col.key">
            {{ record[col.key] }}
          </td>
        </slot>
      </tr>
    </tbody>
    <tfoot v-if="$slots.footer">
      <slot name="footer" :summary="computeSummary"></slot>
    </tfoot>
  </table>
</template>

<script>
export default {
  props: {
    columnDefs: Array,
    dataset: Array
  },
  computed: {
    computeSummary() {
      return {
        total: this.dataset.length,
        lastUpdated: new Date().toLocaleString()
      }
    }
  }
}
</script>

사용 예시

<template>
  <data-table :column-defs="headers" :dataset="products">
    <template #header="{ columns }">
      <th v-for="col in columns" :key="col.key" class="custom-header">
        {{ col.label }} ↕
      </th>
    </template>
    
    <template #row="{ item, meta }">
      <td>{{ meta.index + 1 }}</td>
      <td>
        <img :src="item.thumbnail" class="thumb">
        {{ item.name }}
      </td>
      <td :class="getPriceClass(item.price)">
        {{ formatCurrency(item.price) }}
      </td>
    </template>
    
    <template #footer="{ summary }">
      <tr>
        <td colspan="3">
          총 {{ summary.total }}개 항목 | 갱신: {{ summary.lastUpdated }}
        </td>
      </tr>
    </template>
  </data-table>
</template>

구문 변천사 및 하위 호환성

버전 네임드 슬롯 스코프드 슬롯
Vue 2.5 이하 slot="name" slot-scope="props"
Vue 2.6+ v-slot:name 또는 #name v-slot:name="props" 또는 #name="props"
Vue 3.x v-slot:name 또는 #name (권장) 동일, slot/slot-scope 제거됨

주의사항

  • 이름 불일치: 부모가 존재하지 않는 슬롯 이름에 콘텐츠를 할당하면 해당 콘텐츠는 렌더링되지 않는다.
  • 기본 슬롯 중복: 자식에 이름 없는 <slot>이 여러 개 있으면, 부모의 기본 콘텐츠가 모든 위치에 중복 삽입된다.
  • 단일 루트 제약: Vue 2에서는 v-slot이 지정된 <template>가 반드시 단일 루트 요소를 포함해야 했으나, Vue 3에서는 Fragment 지원으로 완화되었다.

태그: Vue.js Slot Scoped Slot Named Slot Component Composition

9월 10일 22:18에 게시됨