Teleport는 컴포넌트의 논리적 위치는 유지하면서, 렌더링되는 DOM 위치를 다른 곳으로 이동시키는 내장 컴포넌트입니다.

면접에서 "모달을 구현할 때 z-index 문제는 어떻게 해결하나요?"라고 물으면, Teleport를 활용해 body 직계 자식으로 렌더링하는 방법을 설명할 수 있어야 합니다.


왜 Teleport가 필요한가?

모달, 토스트, 드롭다운 같은 UI 요소는 논리적으로는 특정 컴포넌트에 속하지만, DOM 구조상으로는 <body> 바로 아래에 있어야 합니다.

PLAINTEXT
문제: 부모의 overflow: hidden, z-index, transform이 자식에게 영향
     → 모달이 잘리거나, 다른 요소 뒤에 숨거나, 위치가 어긋남

해결: Teleport로 DOM 위치를 body로 이동
     → CSS 쌓임 맥락(Stacking Context)에서 자유로움

기본 사용법

VUE
<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

VUE
<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>
HTML
<!-- index.html에 대상 컨테이너 추가 -->
<body>
  <div id="app"></div>
  <div id="modal-container"></div>
  <div class="toast-container"></div>
</body>

같은 대상에 여러 Teleport

VUE
<template>
  <!-- 같은 대상에 여러 Teleport — 순서대로 추가됨 -->
  <Teleport to="#notifications">
    <div class="toast">첫 번째 알림</div>
  </Teleport>

  <Teleport to="#notifications">
    <div class="toast">두 번째 알림</div>
  </Teleport>
</template>

실전 — 토스트 알림 시스템

TYPESCRIPT
// 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 }
}
VUE
<!-- 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)은 원래 컴포넌트 트리를 따릅니다.

댓글 로딩 중...