본문 바로가기
framework_library/vue

🌳Vue Suspense와 defineAsyncComponent - API 로딩 대기와 에러 핸들링 집중 탐구

by 죄니안죄니 2025. 5. 5.

🌳 Vue Suspense와 defineAsyncComponent - API 로딩 대기와 에러 핸들링 집중 탐구

Vue 고급 주제 > 7️⃣ 고급 주제 및 최적화 > Suspense와 defineAsyncComponent 활용


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 기능을 알아봅니다.

댓글