PromleeBlog
sitemap
aboutMe

posting thumbnail
Vue 슬롯, 텔레포트, Provide/Inject - Vue 심화 외전 2편
Vue Slot, Teleport, and Provide/Inject - Vue Advanced Part 2

📅

🚀

들어가기 전에 🔗

지난 시간에는 커스텀 디렉티브와 컴포저블을 통해 코드 로직을 재사용하는 법을 배웠습니다.
오늘은 컴포넌트의
구조
데이터 흐름
을 한 차원 더 유연하게 만들어주는 세 가지 강력한 도구,
슬롯(Slot)
,
텔레포트(Teleport)
, 그리고
프로바이드/인젝트(Provide/Inject)
에 대해 알아보겠습니다.

이 기능들은 조금 생소하게 들릴 수 있지만, 복잡한 UI를 깔끔하게 구성하고 컴포넌트 간의 데이터를 효율적으로 관리하는 데 아주 중요한 역할을 합니다.

🚀

Slot - 컴포넌트의 콘텐츠 삽입구 🔗

우리가 어떤 상자를 만들었다고 상상해 봅시다.
이 상자의 모양과 색깔은 정해져 있지만, 안에 무엇을 담을지는 상자를 사용하는 사람이 결정하게 하고 싶을 때가 있습니다.
Vue 컴포넌트의
슬롯(Slot)
이 바로 이런 역할을 합니다. 리액트의
children
과 비슷한 개념입니다.

슬롯은 부모 컴포넌트에서 자식 컴포넌트의 특정 영역에 원하는 HTML 콘텐츠를 끼워 넣을 수 있게 해주는 '콘텐츠 삽입구'입니다.
이를 통해 자식 컴포넌트는 재사용 가능한 '틀'이 되고, 부모 컴포넌트는 그 틀 안의 '내용'을 자유롭게 결정할 수 있습니다.

1. 기본 슬롯 (Default Slot) 🔗

가장 간단한 형태의 슬롯입니다.
자식 컴포넌트(CardBox.vue)에 이름 없는 <slot> 태그를 하나만 두는 방식입니다.
src/components/CardBox.vue
<template>
  <div class="card">
    <header class="card-header">기본 카드</header>
    <main class="card-content">
      <!-- 부모가 주는 콘텐츠가 이 자리에 들어옵니다. -->
      <slot></slot>
    </main>
  </div>
</template>
<style scoped>
.card { border: 1px solid #ccc; border-radius: 8px; padding: 16px; }
</style>
이제 부모 컴포넌트에서는 CardBox 컴포넌트를 사용하고, 그 안에 원하는 내용을 자유롭게 넣을 수 있습니다.
src/components/ParentComponent.vue
<script setup lang="ts">
import CardBox from './CardBox.vue'
</script>
<!-- 부모 컴포넌트 -->
<template>
  <CardBox>
    <!-- 이 부분이 CardBox의 <slot> 자리로 전달됩니다. -->
    <p>이것은 카드 내용입니다.</p>
    <button>클릭하세요</button>
  </CardBox>
 
  <CardBox>
    <img src="/logo.png" alt="Vue Logo" />
  </CardBox>
</template>
같은 CardBox 컴포넌트를 사용하지만, 안에 들어가는 내용물이 완전히 다른 두 개의 카드를 쉽게 만들 수 있습니다.

2. 이름 있는 슬롯 (Named Slots) 🔗

만약 카드에 '헤더', '본문', '푸터'처럼 여러 개의 콘텐츠 삽입구가 필요하다면 어떻게 할까요?
이때는 각 <slot>name 속성을 부여하여 '이름 있는 슬롯'을 만들 수 있습니다.
src/components/MultiSlotCard.vue
<template>
  <div class="card">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot> <!-- name이 없는 슬롯은 'default'라는 이름을 가집니다. -->
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>
부모에서는 <template> 태그와 v-slot: 디렉티브를 사용해 각 이름에 맞는 콘텐츠를 전달합니다.
v-slot:#으로 줄여 쓸 수 있습니다.
src/components/ParentComponent.vue
<script setup lang="ts">
import MultiSlotCard from './MultiSlotCard.vue'
</script>
<!-- 부모 컴포넌트 -->
<template>
  <MultiSlotCard>
    <template #header>
      <h2>카드 제목</h2>
    </template>
 
    <!-- #default는 생략 가능합니다. -->
    <p>카드 본문 내용입니다.</p>
 
    <template #footer>
      <button>확인</button>
    </template>
  </MultiSlotCard>
</template>

🚀

Teleport - DOM의 제약 넘기 🔗

웹 애플리케이션을 만들다 보면 모달 창, 알림 창, 드롭다운 메뉴처럼 화면 전체를 덮는 UI를 만들어야 할 때가 많습니다.
이런 UI 요소들은 컴포넌트 구조상 깊숙한 곳에 위치하지만, 실제로는 <body> 태그 바로 아래에 있는 것처럼 동작해야 z-indexposition 같은 CSS 문제로부터 자유로워집니다.

이럴 때 사용하는 것이 바로
텔레포트(Teleport)
입니다.
텔레포트는 이름 그대로, 컴포넌트의 일부를 현재 위치가 아닌 다른 DOM 위치로 '순간이동'시켜 렌더링하는 기능입니다.
src/components/Modal.vue
<script setup lang="ts">
import { ref } from 'vue'
const showModal = ref(false)
</script>
 
<template>
  <button @click="showModal = true">모달 열기</button>
 
  <!-- Teleport를 사용하여 모달 콘텐츠를 body 태그로 보냅니다. -->
  <Teleport to="body">
    <div v-if="showModal" class="modal-background">
      <div class="modal-content">
        <p>안녕하세요! 저는 모달입니다.</p>
        <button @click="showModal = false">닫기</button>
      </div>
    </div>
  </Teleport>
</template>
 
<style scoped>
.modal-background { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.5); }
.modal-content { background: white; padding: 20px; border-radius: 8px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
</style>
Teleport 태그 안의 모달 div는 컴포넌트의 논리적 구조에는 속해있지만, 실제 브라우저의 DOM 트리에서는 <body> 태그의 직접적인 자식으로 렌더링됩니다.
덕분에 다른 요소들의 CSS 스타일에 구애받지 않고 안정적으로 화면 최상단에 표시될 수 있습니다.

🚀

Provide/Inject -깊은 컴포넌트에 데이터를 쉽게 전달하는 방법 🔗

부모 컴포넌트가 아주 깊숙한 곳에 있는 자손 컴포넌트에게 데이터를 전달해야 하는 상황을 생각해 봅시다.
일반적으로는 props를 통해 부모 → 자식 → 손자 → 증손자... 와 같이 한 단계씩 데이터를 계속해서 전달해야 합니다.
이것을
프롭 드릴링(prop drilling)
이라고 부르며, 코드를 매우 번거롭게 만듭니다.

프로바이드/인젝트(Provide/Inject)
는 이러한 '프롭 드릴링' 문제를 해결하기 위한 깔끔한 방법입니다.
조상 컴포넌트가 데이터를 "제공(Provide)"하면, 그 아래의 어떤 자손 컴포넌트든 중간 단계를 모두 건너뛰고 데이터를 직접 "주입(Inject)"받아 사용할 수 있습니다.
React의 Context API와 유사한 개념입니다.
src/App.vue
<script setup lang="ts">
import { provide, ref } from 'vue'
import DeepChild from './components/DeepChild.vue'
 
// 'theme'이라는 키로 'dark'라는 값을 제공합니다.
const theme = ref('dark')
provide('theme', theme)
 
function toggleTheme() {
  theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
</script>
 
<template>
  <button @click="toggleTheme">테마 변경</button>
  <DeepChild />
</template>
이제 아주 깊은 곳에 있는 자손 컴포넌트에서 이 데이터를 주입받아 사용해 보겠습니다.
src/components/DeepChild.vue
<script setup lang="ts">
import { inject } from 'vue'
 
// 'theme' 키로 제공된 데이터를 주입받습니다.
// inject<string>('theme') 처럼 타입을 명시할 수 있습니다.
const theme = inject('theme')
</script>
 
<template>
  <div :class="theme">
    현재 테마는: {{ theme }}
  </div>
</template>
 
<style scoped>
.dark { background: #333; color: white; }
.light { background: #eee; color: black; }
</style>
중간에 다른 컴포넌트가 몇 개가 있든 상관없이, App.vue에서 제공한 theme 데이터를 DeepChild.vue가 직접 받아서 사용할 수 있습니다.
providerefreactive 객체를 전달하면, 데이터가 변경될 때 주입받은 모든 컴포넌트가 반응하여 업데이트됩니다.

🚀

결론 🔗

오늘은 컴포넌트를 더욱 유연하고 강력하게 만들어주는 세 가지 고급 기술을 배웠습니다.
이 세 가지 도구를 잘 이해하고 상황에 맞게 사용한다면, 여러분은 훨씬 더 깔끔하고 효율적인 구조의 Vue 애플리케이션을 설계할 수 있게 될 것입니다.
다음 마지막 외전 3편에서는 서버 사이드 렌더링(SSR)의 개념과 Vue의 공식 프레임워크인 Nuxt 3의 기초에 대해 알아보겠습니다.

참고 🔗