개요
HelloMaple 백오피스 개발하는 중 데이터를 불러올 때 테이블에 로딩 처리를 하는 페이지와 그렇지 않은 페이지가 있어서 로딩을 제거할지 아니면 테이블에 모두 적용할지 고민에 빠졌다. 같이 고민하면 좋을 것 같아서 팀 메신저를 통해 아래 내용을 물어봤다.
흠 HelloMaple 백오피스를 개발하면서 고민이 되는 사항이 있어서 여기 남겨봅니다.
백오피스에서 vuetify를 사용하는데요!
v-data-table
은loading
props가 있어 비동기 통신이 끝나기 전 까지 로딩화면을 보여주고 있어요. 하지만,
v-table
은loading
props가 없어 로딩 관련 처리를 하지 않고 있습니다.
라이브 기준으로 카테고리를 조회하는데 약 461ms 정도 소요되네요. 이 상황에서!
- 1안. 백오피스는 대유저 서비스가 아니니까 별도 로딩 처리를 하지 않아도 괜찮다!
- 2안. 무슨소리냐! 백오피스도 로딩처리를 해야한다.
2안으로 진행한다고 했을 때
n-data-table
,n-table
래핑 컴포넌트를 만들어서 컴포넌트 내부에서 로딩 처리해 재 사용성을 높이는 방법을 사용하면 좋을 것 같습니다. 또 로딩 UI 지속 시간에 대해서 생각할 때 추가 고민이 발생하네요. (아래)
- 2-A안. 서버 통신이 너무 짧을 경우 로딩 UI는 사용자에게 혼란스러운 느낌을 줄 수 있기 때문에 최소한 300ms 정도 로딩 UI를 보여준다.
- 2-B안. 로딩 UI를 바로 보여주지 않고 200ms 정도 빈 화면을 보여주고 그 이후 아직도 서버로 부터 데이터를 받아오지 못했다면 로딩 UI를 보여준다.
2-B안은 카카오 기술 블로그에서 로딩 처리 방법을 보고 가져온 내용입니다.
이에 팀원 대다수가 2-A안이 좋을 것 같다는 의견이 있어 개발 방향을 2-A안으로 잡았다.
구현하기
useDelayedLoading Composable 구현하기
Vue 3의 Composable 을 사용해 최소 300ms 만큼 지연시간을 갖는 로딩 상태 값을 구현했다. 아래 코드를 보자!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import type { Ref } from 'vue';
import dayjs from 'dayjs';
import { DEFAULT_PENDING_TIME } from 'assets/ts/common/constants';
export function useDelayedLoading(loading: Ref<boolean>) {
const startTime: Ref<null | number> = ref(null);
const delayedLoading = ref(false);
watch(
loading,
(newLoading: boolean) => {
if (newLoading) {
delayedLoading.value = true;
startTime.value = dayjs().valueOf();
} else {
if (!startTime.value) {
return;
}
const endTime = dayjs().valueOf();
const fetchingTime = endTime - startTime.value;
if (fetchingTime > DEFAULT_PENDING_TIME) {
clear();
} else {
setTimeout(clear, DEFAULT_PENDING_TIME - fetchingTime);
}
}
},
{ immediate: true }
);
function clear() {
startTime.value = null;
delayedLoading.value = false;
}
return delayedLoading;
}
코드를 설명하면,
- 외부에서
loading
이라는 상태(ref
)값을 전달 받아서watch
에서 구독한다. loading
상태 값이true
가 된 시점에startTime
을 기록한다.loading
상태 값이false
가 된 시점에endTime
을 기록한다.endTime
과startTime
차이를 구한다.- 300ms 보다 크면
delayedLoading
을false
로 변경하고 끝난다. - 300ms 보다 작으면 그 차이만큼 delay를 준 후
delayedLoading
을false
로 변경하고 끝난다.
- 300ms 보다 크면
n-data-table, n-table에 구현하기
n-data-table, n-table 원리는 거의 비슷해 n-table 코드만 부분적으로 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// props는 ref 형식이 아니라 watch를 이용해 ref 형식으로 변경한다.
const isLoading: Ref<boolean> = ref(false);
watch(
() => props.loading,
(newValue) => {
isLoading.value = newValue || false;
},
{ immediate: true }
);
// Composable을 사용해 delayedLoading 값을 가져온다.
const delayedLoading = useDelayedLoading(isLoading);
// props로 전달 받은 header 정보를 이용해 skeleton 배열을 만든다.
const skeletons = computed(() => {
return (
props.headers &&
props.headers.map((n, index) => {
return { key: index, width: n.width };
})
);
});
// template 부분 코드
<template v-if="delayedLoading">
<tbody>
<tr
:key="index"
class="v-data-table__tr"
v-for="index in ITEM_PER_PAGE"
>
<td
:key="key"
class="v-data-table__td"
:style="{ width }"
v-for="{ key, width } in skeletons"
>
<div class="d-flex">
<v-skeleton-loader
class="skeleton_table"
:class="!width && 'flex-grow-1'"
type="list-item"
/>
</div>
</td>
</tr>
</tbody>
</template>
<template v-else>
<slot name="body"></slot>
</template>
사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// useAsyncData 사용 부분 코드
const {
data: list,
refresh,
pending, // pending은 useAsnycData에서 내려주는 비동기 로딩 ref 값이다.
error
} = await useAsyncData(
() => {
return $api.category.getExposureCategoryList({
categoryTypeId: selectedTab.value,
countryGroupId: COUNTRY_GROUP_ID
});
},
{
watch: [selectedTab],
default(): TransformedExposureCategoryList {
return { result: [] };
},
lazy: true
}
);
// template 부분 코드 (pending 전달)
<n-table
class="table_category"
:loading="pending"
:headers="// prettier-ignore
[
{ title: '정렬', width: '65px', align: 'center' },
{ title: '카테고리 번호', width: '120px', align: 'center' },
{ title: '이름', width: '200px', },
{ title: '노출상태', width: '200px', align: 'center' },
{ title: '즉시노출', width: '100px', align: 'center' },
{ title: '노출시간설정', align: 'center' },
{ title: '삭제', width: '100px', align: 'center' }
]"
>
<template #body>
<!-- 중략...-—>
</template>
</n-table>
결과물
Mock 데이터를 활용해 테스트를 해봤다. 아래 영상을 보자
서버 통신이 300ms 보다 긴 경우 (서버 통신 시간: 1,500ms)
실제 통신 시간이 300ms 보다 길기 때문에 1,500ms 동안 Skeleton UI를 노출한다.