Learning
레슨 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">&times;</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

provideinject는 부모-자식 간 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에서 타입 안전성을 확보할 수 있습니다.