Teleport
Teleport는 컴포넌트의 논리적 위치는 유지하면서, 렌더링되는 DOM 위치를 다른 곳으로 이동시키는 내장 컴포넌트입니다.
면접에서 "모달을 구현할 때 z-index 문제는 어떻게 해결하나요?"라고 물으면, Teleport를 활용해 body 직계 자식으로 렌더링하는 방법을 설명할 수 있어야 합니다.
왜 Teleport가 필요한가?
모달, 토스트, 드롭다운 같은 UI 요소는 논리적으로는 특정 컴포넌트에 속하지만, DOM 구조상으로는 <body> 바로 아래에 있어야 합니다.
문제: 부모의 overflow: hidden, z-index, transform이 자식에게 영향
→ 모달이 잘리거나, 다른 요소 뒤에 숨거나, 위치가 어긋남
해결: Teleport로 DOM 위치를 body로 이동
→ CSS 쌓임 맥락(Stacking Context)에서 자유로움
기본 사용법
<script setup lang="ts">
import { ref } from 'vue'
const isOpen = ref(false)
</script>
<template>
<button @click="isOpen = true">모달 열기</button>
<!-- to 속성에 CSS 선택자나 DOM 엘리먼트를 지정 -->
<Teleport to="body">
<div v-if="isOpen" class="modal-overlay" @click.self="isOpen = false">
<div class="modal-content">
<h2>모달 제목</h2>
<p>이 모달은 body 직계 자식으로 렌더링됩니다.</p>
<button @click="isOpen = false">닫기</button>
</div>
</div>
</Teleport>
</template>
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 24px;
border-radius: 8px;
max-width: 500px;
width: 90%;
}
</style>
다른 대상에 Teleport
<template>
<!-- body 이외의 대상 -->
<Teleport to="#modal-container">
<div class="modal">모달 내용</div>
</Teleport>
<!-- CSS 선택자 사용 -->
<Teleport to=".toast-container">
<div class="toast">알림 메시지</div>
</Teleport>
<!-- disabled — 조건부로 Teleport 비활성화 -->
<Teleport to="body" :disabled="isMobile">
<div class="dropdown">
<!-- 모바일에서는 제자리에, 데스크톱에서는 body로 -->
</div>
</Teleport>
</template>
<!-- index.html에 대상 컨테이너 추가 -->
<body>
<div id="app"></div>
<div id="modal-container"></div>
<div class="toast-container"></div>
</body>
같은 대상에 여러 Teleport
<template>
<!-- 같은 대상에 여러 Teleport — 순서대로 추가됨 -->
<Teleport to="#notifications">
<div class="toast">첫 번째 알림</div>
</Teleport>
<Teleport to="#notifications">
<div class="toast">두 번째 알림</div>
</Teleport>
</template>
실전 — 토스트 알림 시스템
// composables/useToast.ts
import { ref } from 'vue'
interface Toast {
id: number
message: string
type: 'success' | 'error' | 'info'
}
const toasts = ref<Toast[]>([])
let nextId = 0
export function useToast() {
const show = (message: string, type: Toast['type'] = 'info') => {
const id = nextId++
toasts.value.push({ id, message, type })
// 3초 후 자동 제거
setTimeout(() => {
toasts.value = toasts.value.filter(t => t.id !== id)
}, 3000)
}
return { toasts, show }
}
<!-- ToastContainer.vue — App.vue에 한 번만 배치 -->
<script setup lang="ts">
import { useToast } from '@/composables/useToast'
const { toasts } = useToast()
</script>
<template>
<Teleport to="body">
<div class="toast-container">
<TransitionGroup name="toast">
<div v-for="toast in toasts" :key="toast.id" :class="['toast', `toast-${toast.type}`]">
{{ toast.message }}
</div>
</TransitionGroup>
</div>
</Teleport>
</template>
<style>
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
}
.toast {
padding: 12px 24px;
margin-bottom: 8px;
border-radius: 4px;
color: white;
}
.toast-success { background: #42b883; }
.toast-error { background: #e74c3c; }
.toast-info { background: #3498db; }
</style>
면접 팁
- Teleport는 컴포넌트의 논리적 소속은 바꾸지 않습니다 — Props, Events, Provide/Inject 모두 원래 부모와 연결됩니다
- React의
createPortal과 같은 개념이므로, 프레임워크 비교 질문에서 활용할 수 있습니다 - CSS 쌓임 맥락(Stacking Context) 을 이해하면 Teleport가 필요한 이유를 더 깊이 있게 설명할 수 있습니다
요약
Teleport는 컴포넌트의 DOM 출력 위치를 to 속성으로 지정한 곳으로 이동시킵니다. 모달, 토스트, 드롭다운 등 CSS 쌓임 맥락에서 벗어나야 하는 UI에 필수적이며, 논리적 소속(Props, Events)은 원래 컴포넌트 트리를 따릅니다.
댓글 로딩 중...