🌳 Vue Suspense와 defineAsyncComponent - API 로딩 대기와 에러 핸들링 집중 탐구
Vue 고급 주제 > 7️⃣ 고급 주제 및 최적화 > Suspense와 defineAsyncComponent 활용
- 1. 왜 비동기 컴포넌트 로딩이 필요한가?
- 2. defineAsyncComponent 기초
- 3. 로 API 로딩 대기 처리하기
- 4. API 에러 핸들링 전략
- 5. 실무 팁 & 주의사항
- 🔚 다음 글 안내
1. 왜 비동기 컴포넌트 로딩이 필요한가?
Vue 컴포넌트는 기본적으로 프로젝트 시작 시 모두 로딩됩니다. 지연 로딩 (Lazy Loading)은 다음과 같은 상황에서 성능 개선에 효과적입니다:
- 무거운 컴포넌트 또는 외부 API 호출이 동반될 경우 첫 페이지 렌더링 속도가 중요한 경우
- 비동기 API 호출이 포함된 컴포넌트
- 사용하지 않는 화면까지 로딩 → 불필요한 메모리 낭비하는 경우
해결책: defineAsyncComponent
+ <Suspense>
를 활용한 컴포넌트 지연 로딩 및 로딩 중 대기 처리
2. defineAsyncComponent 기초
defineAsyncComponent
는 컴포넌트를 비동기 로딩(동적 import)하는 Vue 3 전용 기능입니다.
// 지연 로딩 예시
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(() => import('./MyHeavyComponent.vue'));
또는 더 정교하게 로딩 지연, 타임아웃, 에러 컴포넌트를 함께 지정할 수 있습니다:
// router/index.js 또는 setup 정의 파일 등
import { defineAsyncComponent } from 'vue';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
import ErrorFallback from '@/components/common/ErrorFallback.vue';
const AsyncComponent = defineAsyncComponent({
loader: () => import('./MyHeavyComponent.vue'),
delay: 200,
timeout: 5000,
loadingComponent: LoadingSpinner,
errorComponent: ErrorFallback
});
- loader: 실제 비동기 컴포넌트를 불러오는 함수
- errorComponent: 실패했을 때 대신 렌더링할 컴포넌트
- timeout: 시간 초과 시에도 error 처리됨
- delay: 로딩 표시를 보여주기 전 대기 시간
에러 처리 컴포넌트 예시
<!-- ErrorFallback.vue -->
<template>
<div class="error-box">
⚠️ 컴포넌트를 불러오는 데 실패했습니다.
<button @click="$emit('retry')">다시 시도</button>
</div>
</template>
※ @retry 같은 커스텀 이벤트는 직접 구현해줘야 동작합니다.
3. <Suspense>로 API 로딩 대기 처리하기
<Suspense>
는 비동기 setup() 또는 비동기 컴포넌트를 사용하는 경우 로딩 대기/에러 fallback 등을 처리할 수 있는 Vue 3의 특수 태그입니다.
✅ 예시: API 호출이 완료될 때까지 로딩 대기(스피너 보여주기)
<Suspense>는 공통 로딩 처리나 오류 처리를 위한 래퍼(wrapper)로 전체 앱의 상위 또는 라우트 단위에서 자주 사용합니다. 전체 앱에서 페이지 전환 시나 컴포넌트 비동기 로딩이 많다면 App.vue 또는 Layout.vue처럼 공통적인 곳에 <Suspense> 래핑하는 구조가 깔끔합니다. (물론, 비동기 데이터 의존성이 큰 페이지는 그 컴포넌트만 감쌀 수도 있습니다.)
<!-- App.vue -->
<template>
<Suspense>
<template #default>
<UserProfile />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
<!-- UserProfile.vue -->
<template>
<div>
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const user = ref(null);
onMounted(async () => {
const res = await fetch('/api/user');
user.value = await res.json();
});
</script>
핵심: setup 함수 내에서 await 또는 fetch 사용 시 <Suspense>
가 자동으로 fallback을 보여줍니다.
4. API 에러 핸들링 전략
<Suspense>
단독으로는 에러 핸들링이 부족합니다. 다음 전략이 필요합니다:
기본 Error.vue 화면을 만들어 놓고,
main.js 파일에서 defineAsyncComponent 등록해놓고,
Error.vue component로 등록 해놓고,
실제 사용하는 화면에서 api를 호출하면서 try/catch로 error를 설정해서 error가 있으면 Error.vue를 렌더링 할 수 있도록 화면 분기
① try/catch와 errorComponent 결합
<!-- MyComponent.vue -->
<template>
<div class="error">⚠️ 데이터 불러오기 실패</div>
</template>
// App.vue 내에서
const AsyncUser = defineAsyncComponent({
loader: () => import('./MyComponent.vue'),
errorComponent: ErrorMessage,
timeout: 5000
})
② 에러 상태를 명시적으로 관리(실제 api를 호출하고 에러 화면을 띄울 컴포넌트)
//Options API
setup() {
const error = ref(null);
const data = ref(null);
onMounted(async () => {
try {
const res = await fetch('/api/data');
data.value = await res.json();
} catch (e) {
error.value = '데이터 불러오기 실패';
}
});
return { error, data };
}
//Composition API
<script setup>
import { ref, onMounted } from 'vue';
const user = ref(null);
const error = ref(null);
onMounted(async () => {
try {
const res = await fetch('/api/user');
if (!res.ok) throw new Error('API 오류');
user.value = await res.json();
} catch (e) {
error.value = e.message;
}
});
</script>
// 단순하게 데이터와 에러를 바로 화면에 보여주는 경우
<template>
<div v-if="error">{{ error }}</div>
<div v-else-if="user">
<h2>{{ user.name }}</h2>
</div>
</template>
// 각 화면을 별도로 관리하는 경우
<template>
<div v-if="error">
<ErrorMessage />
</div>
<div v-else-if="!userData">
<LoadingSpinner />
</div>
<div v-else>
<UserProfile :user="userData" />
</div>
</template>
// 더 간단하게
<template>
<div>
<LoadingSpinner v-if="loading" />
<ErrorComponent v-else-if="hasError" />
<MyPageContent v-else :data="data" />
</div>
</template>
③ Suspense fallback을 Loading이 아니라 Error로 바꾸는 팁 (자식 컴포넌트의 error를 부모컴포넌트에서 받아서 렌더링하는 케이스)
Vue 3의 <Suspense> 자체는 에러 상태를 감지하여 fallback을 자동 전환해주진 않기 때문에, 이를 부모 컴포넌트에서 수동으로 상태를 감지하여 fallback 내용을 전환하는 방식으로 처리해야 합니다.
- 자식 컴포넌트의 setup() 내부에서 error 상태를 ref로 관리
- 부모 컴포넌트에서 해당 error를 <Suspense>의 fallback에 조건부로 반영
// 자식 컴포넌트에서 error 관리 (UserProfile.vue)
<script setup>
import { ref, onMounted } from 'vue';
const data = ref(null);
const error = ref(null);
onMounted(async () => {
try {
const res = await fetch('/api/user');
if (!res.ok) throw new Error('서버 오류');
data.value = await res.json();
} catch (e) {
error.value = e.message || '알 수 없는 오류';
}
});
// error를 부모가 접근할 수 있도록 export ⚠️
defineExpose({ error });
</script>
<template>
<div v-if="data">
<p>사용자 이름: {{ data.name }}</p>
</div>
</template>
// 부모 컴포넌트에서 Suspense fallback 제어
<template>
<Suspense>
<template #default>
<UserProfile ref="userRef" /> // 자식 상태를 가져오는 방식 ⚠️
</template>
<template #fallback>
<template v-if="userRef?.error">
<ErrorBox :msg="userRef.error" />
</template>
<template v-else>
<LoadingSpinner />
</template>
</template>
</Suspense>
</template>
<script setup>
import { ref } from 'vue';
import UserProfile from './UserProfile.vue';
import ErrorBox from './ErrorBox.vue';
import LoadingSpinner from './LoadingSpinner.vue';
const userRef = ref(null);
</script>
위처럼 부모 컴포넌트에서 자식의 상태를 감지해 fallback 내용을 동적으로 변경할 수 있습니다.
5. 실무 팁 & 주의사항
- Suspense는 단 1개의 자식만 가질 수 있음 (여러 컴포넌트는 Wrap 필요)
setup()
안에서await
나 비동기 fetch가 있을 때만 대기- fetch가 오래 걸리는 경우 timeout 설정( 초기 렌더링은 timeout 설정 없으면 무기한 기다릴 수 있음)
- 또는 fallback loadingComponent(스피너/로딩바 등)로 UX 개선
- 에러 발생 시 fallback에 오류 메시지나 안내 UI 제공 필수
🔚 다음 글 안내
이제 컴포넌트 로딩과 API 대기를 자연스럽게 처리하는 법을 익혔습니다.
👉 다음 글: keep-alive와 캐싱 전략 – 탭 구조와 페이지 복원에 최적화된 Vue 기능을 알아봅니다.
'framework_library > vue' 카테고리의 다른 글
🌳 Vue의 Teleport로 모달 시스템 우아하게 구성하기 (0) | 2025.05.05 |
---|---|
🌳 Vue의 <keep-alive>와 캐싱 전략 완전정복 (0) | 2025.05.05 |
📌 글로벌 컴포넌트 등록 전략 (0) | 2025.05.05 |
📌 커스텀 디렉티브 만들기 (v-focus, v-mask 등) (1) | 2025.05.05 |
📌 Vue.js에서 전역 EventBus 또는 mitt 활용법 (0) | 2025.05.05 |
댓글