티스토리 뷰

많은 데이터로 인한 문제 (무한 스크롤성능 개선)

영업사원을 위한 페이 관련 웹/앱을 개발하고 출시하고 난 뒤에 문제가 발생하였습니다.

매장의 정보가 10,000건 정도로 많은 경우 웹/앱에서 매장 정보를 그려주는 데 시간이 꽤 오래 걸려 불편함이 있다는 피드백을 받게 되어 수정해야 했습니다.

좀 더 자세히 설명을 해보겠습니다.

해당 페이지에서 전체 매장의 개수가 약 10000건 정도 있을 때
담당 매장에서 전체 매장 버튼을 눌러 전체 매장의 리스트를 웹/앱에서 그려줄 때 시간이 꽤 오래 걸렸습니다.

 

코드 (Svelte)

작성 코드는 보기 좋게 간략화를 많이 진행한 코드입니다.

기존 개발(w. 코드)

코드를 보면 전체 매장 버튼을 눌렀을 때 전체 데이터를 상태변화하는 변수에 할당하여 전체 매장을 한 번에 그려주도록 하였습니다.

<div>
  <button on:click={on_click_part_store}>담당매장</button>
  <button on:click={on_click_total_store}>전체매장</button>
</div>
<div>
    {#each ref_store_arr as store_data}
    <Store_card {store_data}>
    {/each}
</div>

<script>
  let ref_store_arr = []; //상태 변화 매장 array
  let total_store_arr = []; //전체 매장 array
  let part_store_arr = []; // 담당 매장 array

    function on_click_part_store() {
        ref_store_arr = part_store_arr;
    }

    function on_click_total_store() {
        ref_store_arr = total_store_arr;
    }
</script>

다음과 같은 코드로 진행을 속도가 나오지 않아 다른 방법을 찾아야 했습니다.
2가지를 생각하였는데

  1. ag-grid 라이브러리를 사용하는 것
  2. Intersection Observer을 사용하는 것

여기서 저는 선택해야 해서 각각의 장단점을 간단하게 정리하였습니다.

ag-grid

  • ag-grid 장점
  1. 한 번에 데이터를 넣어도 빠르게 그려줄 수 있다.
  2. 자동 정렬, 원하는 데이터만 보기, drag and drop 등 다양한 기능을 쉽게 사용 가능
  • ag-grid 단점
  1. 기존 디자인과 다르게 ag-grid에서의 기본적인 디자인이 들어가 원래의 모습과 다르다
  2. ag-grid의 데이터의 형태를 맞춰줘야 하는 번거로움

Intersection Observer

  • Intersection Observer 장점
  1. ag-grid보다 익숙하여 빠르게 구현이 가능
  2. 디자인을 헤치지 않는다.
  • Intersection Observer 단점
  1. 담당 매장, 전체 매장을 구분해 가며 기능을 사용해야 되어서 고려해야 될 점이 많다. (어려울 수 있다...)

다음과 같이 정리해 보니 어렵지만 기존의 것을 헤치지 않은 방향과 빠르게 수정을 해서 드려야 하는 상황이었기에 익숙한 기술을 사용하여 개발을 하고 싶었고 팀원들과 이야기를 해서 Intersection Observer로 개발을 진행하게 되었습니다.

 

Intersection Observer 개발(w. 코드)

 

Intersection Observer API - Web APIs | MDN

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.

developer.mozilla.org

우선 코드를 보면 다음과 같습니다.
pagination과 같이 개발을 진행한 것을 볼 수 있습니다.
표현하고 싶은 매장의 개수(OBSERVER_STORE_CNT)를 정하고 이에 따라 처음 표현할 매장 리스트를 (.slice(0, OBSERVER_STORE_CNT))을 적용하고 Intersection Observer을 걸어 놓은 <div bind:this={ref_this_observer_target} style="height: 5px" /> 부분에 접근할 때마다 Pagination의 방법과 같이 page 위치를 파악해 가며 원하는 매장의 개수(OBSERVER_STORE_CNT)만큼 추가하는 형태입니다.

<div>
  <button on:click={on_click_part_store}>담당매장</button>
  <button on:click={on_click_total_store}>전체매장</button>
</div>
<div>
    {#each ref_store_arr as store_data}
    <Store_card {store_data}>
    {/each}
  <div bind:this={ref_this_observer_target} style="height: 5px" />
</div>

<script>
  const OBSERVER_STORE_CNT = 30; //매장을 표현할 개수

  let ref_store_arr = []; //상태 변화 매장 array
  let total_store_arr = []; //전체 매장 array
  let part_store_arr = []; // 담당 매장 array

  let current_store_cnt = 0; // 현재 어디까지 가져왔는지 파악할 수 있는 현재 매장 리스트 페이지 수
  let total_store_cnt = 0; // 전체 매장 리스트 페이지 수
  let current_target_store_arr = []; // 현재 가리키고 있는 매장 리스트 (담당 or 전체)

  let ref_this_observer_target = []; //observer target 위치
  let observer_loading = false; //observer loading

    function on_click_part_store() {
    target_store_arr = part_store_arr;
        ref_store_arr = part_store_arr.slice(0, OBSERVER_STORE_CNT);
        total_store_cnt = Math.ceil(part_store_arr.length / OBSERVER_STORE_CNT);
        current_store_cnt = 0;
    }

    function on_click_total_store() {
        target_store_arr = total_store_arr;
        ref_store_arr = total_store_arr.slice(0, OBSERVER_STORE_CNT);
        total_store_cnt = Math.ceil(total_store_arr.length / OBSERVER_STORE_CNT);
        current_store_cnt = 0;
    }

      //observer
    $: if (ref_this_observer_target) {
        const observer = new IntersectionObserver(handle_intersection);
        observer.observe(ref_this_observer_target);
    }

    async function handle_intersection(entries) {
        const entry = entries[0];
        if (entry.isIntersecting && !observer_loading) {
            observer_loading = true;
            load_more_data();
            observer_loading = false;
        }
    }

    function load_more_data() {
        if (current_store_cnt > total_store_cnt) return;

        const new_opt_data = target_opt_arr.slice(
            current_store_cnt * OBSERVER_OPT_CNT,
            current_store_cnt * OBSERVER_OPT_CNT + OBSERVER_OPT_CNT
        );
        current_store_cnt += 1;
        ref_store_arr = [...ref_store_arr, ...new_store_data];
    }
</script>

개발을 진행하면서 담당 매장, 전체 매장을 수시로 왔다 갔다 하는 상황이 많아 제어를 잘해야 하므로 변수가 많아져서 꼬이는 현상이 발생하였습니다.
그래서 모든 값을 변수로 두는 것보다 최대한 변수를 없애가며 개발하려고 하고 처음부터 기능의 큰 틀을 다시 재정의해서 본질적인 부분을 잘 구현되도록 노력하였습니다.
그리고 Intersection Observer에 잘 접근하지 못할 때가 있어 간략하게 style: heigth: 10px을 주어 문제를 해결하기도 했습니다.
많은 문제들을 빠르게 해결해 나가면서 개발의 속도가 조금씩 빨라지고 있다는 생각을 들게 하는 과정이었습니다.

 

성능 측정

새로운 방식으로 개발하니 속도가 훨씬 빨라진 것을 알 수 있었습니다. 그래서 걸리는 시간을 정확히 알고 싶고 팀원들에게 정확한 수치를 알려주고 싶어 ChromePerformance 기능을 이용해서 성능 측정을 하였습니다.

조건: 약 10,000개의 매장

기존 성능 측정

방법: 한 번에 매장 정보를 다 그려주는 방법
속도: 약 12,000ms (약 12초)

01

 

수정본 성능 측정(Intersection Observer)

방법: Intersection Observer로 30개씩 추가로 그려주는 방법
속도: 약 110ms (약 0.11초)

01

마치며

사용자가 불편함을 느낀 부분을 직접적으로 피드백을 받으니, 사용자와 가까워진 기분이 들었고 뭔가 말로 표현하지 못하는 즐거움을 느꼈습니다.
결제와 같이 엄청 크리티컬한 부분은 아니었지만 빠르게 수정해야 하는 상황에서 신속한 상황판단과 저의 실력에 따른 개발 소요 시간을 잘 파악하는 것이 중요하다는 것을 다시 느끼게 되었습니다.
아직 부족하여 정확한 저의 개발소요 시간을 나타낼 수 없지만 특정 기술에 대한 개발 소요 시간들을 줄이고 있고, 판단하게 되게 만드는 과정이었습니다.

긴 글 읽어주셔서 감사합니다.

반응형
공지사항
최근에 올라온 글