Vue의 <Suspense> 컴포넌트 파헤치기

2024-12-15


<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 setupTop-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>
여기까지 구현하고 나서 결과물을 살펴보자. 페이지를 새로고침해보면 3초간 Loading... 텍스트가 표시되다가 데이터가 나오는 걸 확인할 수가 있다.

작동영상

suspense

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>

작동영상

skeleton

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() }

Vue Playground

Reference


Profile picture

하주헌 Neon

개발 관련 내용들과 일상에서 느끼는 점들을 남기고 있어요. 흔하게 널린 글보다는 나만 쓸 수 있는 글을 남기려 하고있어요.

Loading script...