슬롯의 핵심 개념
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 지원으로 완화되었다.