Learning
레슨 10 / 11·5개 토픽

트랜지션과 비동기 컴포넌트

Transition 컴포넌트

Vue의 컴포넌트는 요소가 DOM에 추가되거나 제거될 때 CSS 전환 또는 애니메이션을 자동으로 적용합니다. v-if, v-show, 동적 컴포넌트 전환에 사용됩니다.

vue
<template>
  <button @click="show = !show">토글</button>

  <!-- name 속성으로 클래스 접두사 지정 -->
  <Transition name="fade">
    <div v-if="show" class="box">
      안녕하세요!
    </div>
  </Transition>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const show = ref(true)
</script>

<style scoped>
/* 진입 시작 상태 */
.fade-enter-from {
  opacity: 0;
  transform: translateY(-20px);
}

/* 진입 활성 상태 (트랜지션 정의) */
.fade-enter-active {
  transition: all 0.3s ease-out;
}

/* 진입 완료 상태 (생략 가능 — 기본값은 요소의 기본 스타일) */
.fade-enter-to {
  opacity: 1;
  transform: translateY(0);
}

/* 퇴장 시작 상태 */
.fade-leave-from {
  opacity: 1;
  transform: translateY(0);
}

/* 퇴장 활성 상태 */
.fade-leave-active {
  transition: all 0.3s ease-in;
}

/* 퇴장 완료 상태 */
.fade-leave-to {
  opacity: 0;
  transform: translateY(20px);
}
</style>

CSS 트랜지션 클래스 정리

  • v-enter-from — 진입 시작 상태 (요소 추가 직전)
  • v-enter-active — 진입 트랜지션 활성 (transition/animation 정의)
  • v-enter-to — 진입 완료 상태 (트랜지션 끝)
  • v-leave-from — 퇴장 시작 상태 (요소 제거 직전)
  • v-leave-active — 퇴장 트랜지션 활성
  • v-leave-to — 퇴장 완료 상태 (트랜지션 끝, 요소 제거)
  • name="fade" 지정 시 접두사가 fade-로 변경 (예: fade-enter-from)

TransitionGroup

v-for로 렌더링된 리스트의 항목이 추가, 제거, 이동될 때 트랜지션을 적용합니다. 각 항목에 반드시 고유한 key가 필요합니다.

vue
<template>
  <div>
    <input v-model="newItem" @keyup.enter="addItem" placeholder="항목 추가" />

    <TransitionGroup name="list" tag="ul" class="item-list">
      <li v-for="item in items" :key="item.id" class="item">
        {{ item.text }}
        <button @click="removeItem(item.id)">삭제</button>
      </li>
    </TransitionGroup>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

interface Item { id: number; text: string }

const newItem = ref('')
const items = ref<Item[]>([
  { id: 1, text: '첫 번째 항목' },
  { id: 2, text: '두 번째 항목' },
])
let nextId = 3

const addItem = () => {
  if (!newItem.value.trim()) return
  items.value.push({ id: nextId++, text: newItem.value })
  newItem.value = ''
}

const removeItem = (id: number) => {
  items.value = items.value.filter(i => i.id !== id)
}
</script>

<style scoped>
.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}

.list-enter-active,
.list-leave-active {
  transition: all 0.4s ease;
}

/* FLIP 애니메이션: 이동하는 항목에 적용 */
.list-move {
  transition: transform 0.4s ease;
}

/* 퇴장 항목이 레이아웃에서 빠지도록 */
.list-leave-active {
  position: absolute;
}
</style>

defineAsyncComponent

defineAsyncComponent를 사용하면 컴포넌트를 비동기적으로 로드하여 초기 번들 크기를 줄일 수 있습니다. 로딩/에러 상태를 세밀하게 제어할 수 있습니다.

vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

// 기본 사용법
const HeavyChart = defineAsyncComponent(
  () => import('./components/HeavyChart.vue')
)

// 고급 옵션: 로딩/에러 컴포넌트 지정
const AdminPanel = defineAsyncComponent({
  loader: () => import('./components/AdminPanel.vue'),
  loadingComponent: () => import('./components/LoadingSpinner.vue'),
  errorComponent: () => import('./components/ErrorMessage.vue'),
  delay: 200,      // 로딩 컴포넌트 표시 전 대기 시간 (ms)
  timeout: 10000,  // 타임아웃 시 에러 컴포넌트 표시 (ms)
})
</script>

<template>
  <div>
    <!-- 필요할 때만 로드 -->
    <HeavyChart v-if="showChart" />

    <!-- 로딩/에러 상태 자동 처리 -->
    <AdminPanel />
  </div>
</template>

Suspense와 async setup()

Vue 3의 는 비동기 의존성을 가진 컴포넌트 트리의 로딩 상태를 처리합니다. async setup() 또는 최상위 await를 사용하는 컴포넌트를 감쌀 수 있습니다.

vue
<!-- AsyncDashboard.vue — async setup() 사용 -->
<script setup lang="ts">
// 최상위 await — Suspense 필요
const response = await fetch('/api/dashboard')
const data = await response.json()
</script>

<template>
  <div class="dashboard">
    <h2>대시보드</h2>
    <div class="stats">
      <div v-for="stat in data.stats" :key="stat.label">
        <p class="text-2xl font-bold">{{ stat.value }}</p>
        <p class="text-gray-500">{{ stat.label }}</p>
      </div>
    </div>
  </div>
</template>

<!-- App.vue — Suspense로 감싸기 -->
<template>
  <Suspense>
    <!-- 기본 슬롯: 비동기 컴포넌트 -->
    <template #default>
      <AsyncDashboard />
    </template>

    <!-- fallback 슬롯: 로딩 중 표시 -->
    <template #fallback>
      <div class="flex items-center justify-center p-8">
        <div class="animate-spin h-8 w-8 border-4 border-green-500 border-t-transparent rounded-full"></div>
        <span class="ml-3">데이터를 불러오는 중...</span>
      </div>
    </template>
  </Suspense>
</template>
💡

Suspense는 아직 실험적(experimental) 기능입니다. 프로덕션에서는 defineAsyncComponent의 loadingComponent/errorComponent 옵션이 더 안정적입니다. 트랜지션은 mode="out-in" 속성으로 퇴장 완료 후 진입하도록 순서를 제어할 수 있습니다.