지난 시간에는 커스텀 디렉티브와 컴포저블을 통해 코드 로직을 재사용하는 법을 배웠습니다.
오늘은 컴포넌트의
구조
와
데이터 흐름
을 한 차원 더 유연하게 만들어주는 세 가지 강력한 도구,
슬롯(Slot)
,
텔레포트(Teleport)
, 그리고
프로바이드/인젝트(Provide/Inject)
에 대해 알아보겠습니다.
이 기능들은 조금 생소하게 들릴 수 있지만, 복잡한 UI를 깔끔하게 구성하고 컴포넌트 간의 데이터를 효율적으로 관리하는 데 아주 중요한 역할을 합니다.
우리가 어떤 상자를 만들었다고 상상해 봅시다.
이 상자의 모양과 색깔은 정해져 있지만, 안에 무엇을 담을지는 상자를 사용하는 사람이 결정하게 하고 싶을 때가 있습니다.
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 : 1 px solid #ccc ; border-radius : 8 px ; padding : 16 px ; }
</ 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-index
나
position
같은 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 : 100 vw ; height : 100 vh ; background : rgba ( 0 , 0 , 0 , 0.5 ); }
.modal-content { background : white ; padding : 20 px ; border-radius : 8 px ; 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
가 직접 받아서 사용할 수 있습니다.
provide
에
ref
나
reactive
객체를 전달하면, 데이터가 변경될 때 주입받은 모든 컴포넌트가 반응하여 업데이트됩니다.
오늘은 컴포넌트를 더욱 유연하고 강력하게 만들어주는 세 가지 고급 기술을 배웠습니다.
Slot
자식 컴포넌트에 콘텐츠를 주입하는 구멍입니다.
재사용 가능한 레이아웃 '틀'을 만들 때 매우 유용합니다.
Teleport
컴포넌트의 일부를 다른 DOM 위치로 보내 렌더링합니다.
모달이나 전역 알림처럼 DOM 계층 구조의 제약을 벗어나야 할 때 사용합니다.
Provide/Inject
'프롭 드릴링' 없이 조상에서 자손으로 데이터를 직접 전달하는 방법입니다.
애플리케이션 전반에 걸친 테마나 사용자 정보 등을 전달할 때 효과적입니다.
이 세 가지 도구를 잘 이해하고 상황에 맞게 사용한다면, 여러분은 훨씬 더 깔끔하고 효율적인 구조의 Vue 애플리케이션을 설계할 수 있게 될 것입니다.
다음 마지막 외전 3편에서는 서버 사이드 렌더링(SSR)의 개념과 Vue의 공식 프레임워크인 Nuxt 3의 기초에 대해 알아보겠습니다.