<Suspense>
컴포넌트
Vue에서 제공하는 내장 컴포넌트 중에서 <Suspense>
가 있다. 비동기 setup
혹은 비동기 컴포넌트의 데이터가 준비 완료되기 전까지 fallback
템플릿을 보여주는 기능으로, React 진영의 <Suspense>
와 사실상 동일하다고 볼 수 있다.
Vue 버전 3.5.13
기준으로 아직까지는 experimental 기능이지만, github에 올라오는 discussion 에 따르면 조만간 정식 기능으로서 추가될 것으로 보인다(2년동안 그러고 있다는 건 비밀).
그렇다면 어떻게 사용하는지에 대해서 알아보자.
공식문서에 따르면, <Suspense>
가 기다릴 수 있는 2가지 유형의 의존성이 있다. 사실 2가지로 나뉘어 있다기 보단, 1번 조건(async setup
)을 만족하면 자동적으로 2번 조건(async component
)이 충족된다에 가깝다.
1. async setup
Vue3의 Composition API에서는 기본적으로 setup(){...}
함수 안에 비즈니스 로직을 작성하게 된다. 만약 함수 내부에서 await
문을 사용하여 비동기 함수를 호출하고 싶다면 async setup
와 같이 작성하면 된다. script setup
을 사용하고 있더라도 동일하게 Top-level await
를 하면 된다.
예시코드
// AsyncChild.vue
<script>
export default {
async setup(){
const fetchData = () => {...}
await fetchData();
}
...
<script setup>
const fetchData = () => {...}
await fetchData();
...
2. async component
(비동기 컴포넌트)
defineAsyncComponent
함수와 동적 import
문을 조합하면 비동기 컴포넌트를 얻을 수 있다. 비동기 컴포넌트를 <Suspense>
문의 default slot으로 넣게되면 로딩이 완료되기 전까지는 fallback slot 으로 넣은 템플릿을 보여주다가, 완료되면 비동기 컴포넌트를 보여준다.
예시코드
// Parent.vue
<template>
<div>
<h2>Hello!</h2>
<Suspense>
<AsyncChild />
<template #fallback> Loading... </template>
</Suspense>
</div>
</template>
<script setup>
import { defineAsyncComponent } from "vue"
const AsyncChild = defineAsyncComponent(() => import("./AsyncChild.vue"))
</script>
로딩 UI 만들기
단순 텍스트 UI
이를 토대로 서버로부터 API를 요청하고 응답받기까지 걸리는 시간동안 보여줄 로딩 UI를 구성해보자.
API를 요청하는 fetchData
함수에서 로딩시간에 해당하는 3000ms 는 다음과 같이 모킹했다.
const fetchData = async () => {
await new Promise(resolve => setTimeout(resolve, 3000))
list.value = data
}
그리고 나는 async setup
의 Top-level await
를 이용하여 부모 컴포넌트에서 <Suspense>
안에 집어넣었다.
AsyncList.vue
// AsyncList.vue
<script setup>
import {ref} from 'vue'
const data = [
{id : 0, title: 'This is first'},
{id : 1, title: 'This is second'},
{id : 2, title: 'This is third'}]
const list = ref(null)
const fetchData = async() => {
await new Promise(resolve => setTimeout(resolve,3000))
list.value = data;
}
await fetchData();
</script>
<template>
<div class="container">
<ul>
<li v-for="item in list" :key="item.id">{{item.title}}</li>
</ul>
</div>
</template>
Parent.vue
// Parent.vue
<script setup>
import { defineAsyncComponent } from "vue"
const AsyncComponent = defineAsyncComponent(() => import("./AsyncList.vue"))
</script>
<template>
<div>
<h2>Hello!</h2>
<Suspense>
<AsyncComponent />
<template #fallback> Loading... </template>
</Suspense>
</div>
</template>
작동영상
Vue Playground
스켈레톤 UI
단순히 Loading 텍스트만 표시되는 건 심심하니 스켈레톤 UI를 적용하여 사용자에게 컨텐츠가 표시될 영역에 대한 힌트를 제공해주는 것 또한 가능하다.
Skeleton.vue
// Skeleton.vue
<template>
<div class="container">
<ul>
<li v-for="i in 3" :key="i" class="item"></li>
</ul>
</div>
</template>
<style scoped>
.container {
padding: 10px;
border-radius: 10px;
width: 100%;
background-color: #eaeaea;
}
ul {
display: flex;
flex-direction: column;
gap: 16px;
}
li {
width: 400px;
height: 30px;
background-color: #bcbcbc;
border-radius: 8px;
list-style-type: none;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
background-color: #ccc;
}
50% {
background-color: #ddd;
}
100% {
background-color: #ccc;
}
}
</style>
작동영상
Vue Playground
에러 핸들링
<Suspense>
안에서 에러가 발생할 경우 어떻게 에러 핸들링을 해야 하는지도 알아보자.
위에서 설명한 로딩 UI에서 defineAsyncComponent
함수를 사용할 때 인자로 동적 import
문을 사용했지만 errorComponent
속성 또는 onError
속성을 설정하면 에러를 핸들링 하는 것도 가능하다.
그 밖의 속성들은 공식문서 상에서 아래와 같이 확인할 수 있다.
function defineAsyncComponent(
source: AsyncComponentLoader | AsyncComponentOptions
): Component
type AsyncComponentLoader = () => Promise<Component>
interface AsyncComponentOptions {
loader: AsyncComponentLoader
loadingComponent?: Component
errorComponent?: Component
delay?: number
timeout?: number
suspensible?: boolean
onError?: (
error: Error,
retry: () => void,
fail: () => void,
attempts: number
) => any
}
errorComponent
를 설정하면 에러가 발생했을 때loader
에 넣은 비동기 컴포넌트 대신 다른 컴포넌트를 지정할 수 있다.onError
는 에러가 발생했을 때 호출되는 콜백 함수이다. 인자로 4개를 받게 된다.error
:Error
객체이다. 에러에 대한 정보를 담고 있다.retry
: 요청을 재시도한다.fail
: 더이상retry
하지 않고 실패로 간주한다.attempts
: 현재까지 시도한 횟수이다.
- 그래서 아래 예시코드와 같이 최대 3번까지
retry
시도 후, 실패처리하는 로직을 작성할 수도 있다.
onError: (error, retry, fail, attempts) => {
alert(`error, ${attempts} times`)
if (attempts < 3) {
// 최대 3번까지 재시도
retry()
} else fail()
}