프로그래밍/Vue.js

vuejs 가상스크롤로 로딩지연 개선

p-a-r-k 2023. 2. 27. 20:42
반응형

서버에서 가져올 enum(wasteType) 목록은 최초 접속시에 store에 없으면, 통신하여 담아두고 사용하고,
route 이동시에 store에 담겨있다면 재사용을 합니다. (현재 300여개)
이번에 A 메뉴에 해당 enum리스트가 조건으로 들어가는데,
최초로딩 시 혹은 다른; emun리스트를 한번도 가져오지않는 페이지 > A 메뉴 최초진입시에
ui가 2-5초 멈추는 현상이 발생해서, 해결하는 과정을 기록합니다.

 

1. vue3 devtools의 performance 체크

  1. 페이지 진입을 해보면 IntegratedSearch컴포넌트에 대한 성능 타임라인을 볼 수 있습니다.
  2. 실제로는 마우스클릭부터 렌더링까지 시간을 나눠 정확하게 보고싶었는데, 방법을 잘 모르겠어서 통합검색 컴포넌트만 filter를 걸어서 duration만 확인했습니다. 3.5초

  1. 통합검색 컴포넌트만 필터링하여 end evend의 duration만 체크

2. 실제로 300여개의 폐기물종류가 문자였으면 문제가 없을테지만, CheckBox 컴포넌트가 각 row마다 들어가서 시간이 걸리는듯하여 가상스크롤을 적용해보기로 했습니다.

  1. 문서에 대규모 목록 가상화에 대한 안내도 있긴 했었는데, 아직까지는 라이브러리가 필요할 정도로 빈번하게 사용할것같지는 않아서 일단은 직접 적용해보기로 했습니다..

새로 VirtureScroller라는 컴포넌트를 만들었습니다.

  1. 구글링하여 vue에서 비슷한 고민을 한 게시글이 보여 참고했는데,
  2. 전체 영역은 각 아이템의 높이값에 대한 padding으로 표현하고, 실제 리스트는 computed로 시작 인덱스와, 끝인덱스로 계산하여 표시만 하는 방식입니다.
  3. <script setup lang="ts">
    import {computed, onMounted, onUnmounted, reactive, ref, toRef, withDefaults} from "vue";
    import _throttle from "lodash/throttle";
    import _debounce from "lodash/debounce";
    
    interface IProps {
      dataList: any[];
      containerStyle?: any; // 가상 스크롤 컨테이너 스타일
      containerClass?: string; // 가상 스크롤 컨테이너 클래스
    }
    
    const props = withDefaults(defineProps<IProps>(), {});
    
    const virtualState = reactive({
      startIndex: 0, // 가상 스크롤 시작 인덱스
      scrollY: 0, // 스크롤 위치
      paddingTop: 0, // 가상 스크롤 상단 패딩
      paddingBottom: 0, // 가상 스크롤 하단 패딩
    });
    const getStyle = computed(() => {
      return {
        paddingTop: `${virtualState.paddingTop}px`,
        paddingBottom: `${virtualState.paddingBottom}px`,
        ...props.containerStyle,
      };
    });
    const defaultHeight = ref(0);
    const scrollerRef = ref<HTMLDivElement>();
    const dataListRef = toRef(props, 'dataList');
    const endIndex = computed(() => { // 가상 스크롤 끝 인덱스
      return Math.min(dataListRef.value.length, virtualState.startIndex + 6);
    });
    const displayList = computed(() => { // 가상 스크롤에 표시할 데이터
      return dataListRef.value.slice(virtualState.startIndex, endIndex.value);
    });
    
    function onScroll() {
      const container = scrollerRef.value;
    
      if (!container) return;
      virtualState.scrollY = container.scrollTop;
      virtualState.startIndex = Math.floor(virtualState.scrollY / defaultHeight.value);
    
      virtualState.paddingTop = defaultHeight.value * virtualState.startIndex;
      virtualState.paddingBottom = dataListRef.value.length === endIndex.value ?
        0 : defaultHeight.value * (dataListRef.value.length - endIndex.value);
    }
    const _onScroll = _throttle(onScroll, 100, {leading: true, trailing: true});
    
    onMounted(() => {
      const container = scrollerRef.value;
      // 각 아이템의 높이값 설정
      defaultHeight.value = container?.firstElementChild?.clientHeight || 0;
    
      scrollerRef.value?.addEventListener('scroll', _onScroll);
      onScroll();
    });
    onUnmounted(() => {
      scrollerRef.value?.removeEventListener('scroll', _onScroll);
    });
    </script>
    
    <template>
      <div class="virtual-scroller-container" ref="scrollerRef">
        <div class="virtual-scroller-inner" :style="getStyle" :class="containerClass">
          <div v-for="item of displayList" :key="item.index">
            <slot :data="item" :index="item.index"></slot>
          </div>
        </div>
      </div>
    </template>
    
    <style lang="scss" scoped>
    .virtual-scroller-container {
      position: relative;
      overflow-y: auto;
      .virtual-scroller-inner {
        display: flex;
      }
    }
    </style>

4. 컴포넌트 사용

  1. 가상스크롤 영역 내에 실제 반복되는 wrapper div 가 있어서, 스타일이나 클래스를 props로 받아서 사용하도록 하였습니다.
  2. 기본은 slot으로 data를 받아서 사용할 수 있도록 하였습니다.
  3. <VirtualScroller
      :data-list="card.options"
      class="card-option-body"
      container-class="colgap-24"
      :container-style="{}">
      <template
        v-slot="{data: optionItem, index: optionIdx}">
        <Checkbox
          :value="optionItem.value"
          :name="card.searchKey"
          v-model="card.value"
          :disabled="card.readonly ? card.readonly : false"
          @change="onChangeCardCheck(card, optionItem.value === null)">
          {{optionItem.label}}
        </Checkbox>
      </template>
    </VirtualScroller>

5. 다시 performance 체크

3.5초에서 0.4초로 줄어듬 확인

마치며..

물론 실제로 가상스크롤적용은 더 복잡하고 다른 방법을 사용할수도 있고, 통합검색 컴포넌트 자체의 구현문제로 멈춤현상이 일어나는 것 일수도 있지만 퀵하게 실제로 300여개를 렌더링하지않고 보여지는부분에 대한부분만 렌더링되는것을 테스트 해보고, 시간이 줄어드는것을 확인하면서 멈춤현상이 사라지는게 보여서 조은 시도 였던것 같습니다,,

단, 해당방법은 아이템의 세로가 같다는 가정하에 가능합니다..

해결못하면 문제생기면 라이브러리 써야할듯,,

반응형