레슨 9 / 11·4개 토픽
Slots, Teleport, Provide/Inject
Named Slots
Slots는 부모 컴포넌트가 자식 컴포넌트의 특정 영역에 콘텐츠를 삽입할 수 있게 하는 기능입니다. Named Slot을 사용하면 여러 영역을 구분하여 콘텐츠를 전달할 수 있습니다.
vue
<!-- BaseCard.vue — 자식 컴포넌트 -->
<template>
<div class="card">
<header class="card-header">
<slot name="header">기본 제목</slot>
</header>
<main class="card-body">
<slot>기본 콘텐츠</slot> <!-- default slot -->
</main>
<footer class="card-footer">
<slot name="footer"></slot>
</footer>
</div>
</template>
<!-- 부모에서 사용 -->
<template>
<BaseCard>
<template #header>
<h2>사용자 프로필</h2>
</template>
<!-- default slot -->
<p>이름: 홍길동</p>
<p>이메일: hong@example.com</p>
<template #footer>
<button>수정</button>
<button>삭제</button>
</template>
</BaseCard>
</template>Scoped Slots
Scoped Slot은 자식 컴포넌트가 데이터를 부모에게 전달하면서 렌더링 방식은 부모가 결정하는 패턴입니다. 리스트 컴포넌트에서 각 항목의 렌더링을 위임할 때 자주 사용됩니다.
vue
<!-- DataList.vue — 자식: 데이터 제공, 렌더링은 부모에게 위임 -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot name="item" :item="item" :index="items.indexOf(item)">
{{ item.name }} <!-- 기본 렌더링 -->
</slot>
</li>
</ul>
</template>
<script setup lang="ts">
interface Item { id: number; name: string; status: string }
defineProps<{ items: Item[] }>()
</script>
<!-- 부모에서 사용: 렌더링 방식 결정 -->
<template>
<DataList :items="users">
<template #item="{ item, index }">
<span>{{ index + 1 }}. {{ item.name }}</span>
<span :class="item.status === 'active' ? 'text-green-500' : 'text-red-500'">
{{ item.status }}
</span>
</template>
</DataList>
</template>Teleport
는 컴포넌트의 템플릿 일부를 DOM 트리의 다른 위치로 이동시킵니다. 모달, 토스트 알림, 드롭다운 등 레이아웃 제약을 벗어나야 하는 UI에 유용합니다.
vue
<!-- Modal.vue -->
<template>
<!-- to: 렌더링할 DOM 위치 (CSS 선택자) -->
<Teleport to="body">
<div v-if="isOpen" class="modal-overlay" @click.self="close">
<div class="modal-content">
<header>
<slot name="title"><h2>알림</h2></slot>
<button @click="close">×</button>
</header>
<main>
<slot></slot>
</main>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
defineProps<{ isOpen: boolean }>()
const emit = defineEmits<{ close: [] }>()
const close = () => emit('close')
</script>
<!-- 사용하는 부모 컴포넌트 -->
<template>
<div>
<button @click="showModal = true">모달 열기</button>
<!-- DOM 상으로는 body 바로 아래에 렌더링됨 -->
<Modal :isOpen="showModal" @close="showModal = false">
<template #title><h2>확인</h2></template>
<p>정말 삭제하시겠습니까?</p>
</Modal>
</div>
</template>Provide / Inject
provide와 inject는 부모-자식 간 props 전달 없이 깊은 컴포넌트 트리에 데이터를 전달하는 의존성 주입(DI) 패턴입니다. React의 Context와 유사한 역할을 합니다.
vue
<!-- App.vue — provide로 데이터 제공 -->
<script setup lang="ts">
import { provide, ref, readonly } from 'vue'
import type { InjectionKey, Ref } from 'vue'
// 타입 안전한 키 정의
interface ThemeContext {
theme: Ref<'light' | 'dark'>
toggleTheme: () => void
}
export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
const theme = ref<'light' | 'dark'>('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// readonly로 제공하면 자식이 직접 수정 불가
provide(ThemeKey, {
theme: readonly(theme),
toggleTheme,
})
</script>
<!-- DeepChild.vue — inject로 데이터 사용 -->
<script setup lang="ts">
import { inject } from 'vue'
import { ThemeKey } from './App.vue'
// 두 번째 인자: provide가 없을 때 기본값
const { theme, toggleTheme } = inject(ThemeKey, {
theme: ref('light'),
toggleTheme: () => {},
})
</script>
<template>
<div :class="theme === 'dark' ? 'bg-gray-900 text-white' : 'bg-white text-black'">
<p>현재 테마: {{ theme }}</p>
<button @click="toggleTheme">테마 전환</button>
</div>
</template>💡
Provide/Inject는 플러그인, 테마, 인증 상태 등 앱 전역에서 공유되는 데이터에 적합합니다. 일반적인 부모-자식 통신에는 props와 emit을 사용하세요. InjectionKey를 사용하면 TypeScript에서 타입 안전성을 확보할 수 있습니다.