TIL

jstree 핸들링 방법(대용량 데이터 다루기 - 행걸리는 현상)

빈코 2024. 3. 7. 15:26

jstree - 대용량 데이터

개요

안녕하세요 빈코입니다! 오늘은 저번 포스팅인 jstree 상하위 입맛대로 선택하기에 이어서 jstree의 마지막 포스팅 대용량 데이터 다루기를 포스팅하려 합니다. 여기서 대용량 데이터가 의미하는 것은 tree의 node들이 적어도 1만 개 이상 일 경우를 뜻합니다.

 

jstree로 1만개의 node를 불러오는 데는 속도적인 측면에서 크게 문제 되지 않지만, 만약 특정한 노드들을 선택된 상태로 tree를 그려야 할 경우에는 어떻게 해야 할까요?

 

예를 들어서, 한 기업의 조직도를 tree로 그리고 기업의 부서들이 node라고 가정했을 때 어떠한 정책에 의해서 특정 부서들은 체크박스에 체크가 되어 있는 상태로 tree를 그려야 하는 경우입니다. 사실 예시가 글로 보면 이해가 어려울 수 있지만, 실무에서는 jstree를 사용할 때 흔하게 접할 수 있는 유형입니다. 

 

대부분의 개발자들은 jstree에서 제공하는 기능인 select_node를 사용하는데, 선택된 부서가 많을 경우에는 이 로직을 사용하면 상당한 시간이 걸리거나, 행이 걸리는 현상이 발생하기 때문에 그 부분을 다른 방식으로 해결하는 방법을 포스팅할 예정입니다.

 

jstree 예시

 

jstree의 select_node 기능📘

$("#groupTree").jstree({
   plugins: ["wholerow","checkbox","types","search","sort","changed"],
   checkbox: {
      "cascade_to_disabled": false,
   },
   core: {
      themes: { responsive: !1 },
      dblclick_toggle: false,
      data: data
   },
   sort: function (a, b) {
      a1 = this.get_node(a);
      b1 = this.get_node(b);
      if (a1.icon != b1.icon) {
         return a1.icon > b1.icon ? -1 : 1;
      }
   },
   types: {
      default: { icon: "fa fa-folder icon_state-warning icon_lg" },
   },
}).on('ready.jstree', function() {
   // targetGroup은 선택이 필요한 Node들의 id값을 모은 배열
   targetGroup.forEach((data,i)=>{
      $('#groupTree').jstree(true).select_node(data);
   })
});

 

jstree에서 제공하는 select_node 기능은 $('Tree명').jstree(true).select_node('node의 id값')으로 사용합니다. 그러면 jstree 내부적으로 기능을 활성화 하는데, 이 부분은 하단에서 자세히 설명해 볼게요!

 

위 코드의 하단을 보시면 'ready.jstree'는 jstree가 모두 준비가 되었으면 실행되는 함수입니다. 즉, 원하는 tree 데이터를 모두 가져온 다음에 무엇을 실행할지 정하는 공간이죠. 저는 여기서 js에서 가지고 있는 targetGroup을 반복문을 통해서 targetGroup안에 있는 id값들에 해당하는 각각의 node를 선택하게 만들었습니다.

 

targetGroup 안에 있는 id값이 적은 경우에는 문제없이 돌아가지만, id가 1000개 이상으로 넘어갈 때부터 많은 시간이 소요되고, 5000개가 넘어갔을 때는 페이지가 다운되는 현상이 발생했습니다.

 

왜 그런지 하단에서 알아볼게요 :)

 

jstree.js📒

/**
 * select a node
 * @name select_node(obj [, supress_event, prevent_open])
 * @param {mixed} obj an array can be used to select multiple nodes
 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
 * @param {Boolean} prevent_open if set to `true` parents of the selected node won't be opened
 * @trigger select_node.jstree, changed.jstree
 */
select_node : function (obj, supress_event, prevent_open, e) {
   var dom, t1, t2, th;
   if($.isArray(obj)) {
      obj = obj.slice();
      for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
         this.select_node(obj[t1], supress_event, prevent_open, e);
      }
      return true;
   }
   obj = this.get_node(obj);
   if(!obj || obj.id === $.jstree.root) {
      return false;
   }
   dom = this.get_node(obj, true);
   if(!obj.state.selected) {
      obj.state.selected = true;
      this._data.core.selected.push(obj.id);
      if(!prevent_open) {
         dom = this._open_to(obj);
      }
      if(dom && dom.length) {
         dom.attr('aria-selected', true).children('.jstree-anchor').addClass('jstree-clicked');
      }
      /**
       * triggered when an node is selected
       * @event
       * @name select_node.jstree
       * @param {Object} node
       * @param {Array} selected the current selection
       * @param {Object} event the event (if any) that triggered this select_node
       */
      this.trigger('select_node', { 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
      if(!supress_event) {
         /**
          * triggered when selection changes
          * @event
          * @name changed.jstree
          * @param {Object} node
          * @param {Object} action the action that caused the selection to change
          * @param {Array} selected the current selection
          * @param {Object} event the event (if any) that triggered this changed event
          */
         this.trigger('changed', { 'action' : 'select_node', 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
      }
   }
},

 

위 코드는 jstree.js에서 제공하는 select_node의 자세한 코드입니다.

 

주어진 노드가 배열인 경우, 각각의 노드에 대해 이 함수가 재귀적으로 호출됩니다. 선택된 노드의 상태를 변경하고, 선택된 노드의 ID를 배열에 추가합니다. 그리고 선택한 노드를 열 수 있도록 필요한 경우에만 해당 노드를 엽니다. 선택된 노드의 상태를 시각적으로 나타내기 위해 ARIA 속성과 CSS 클래스를 추가합니다. 마지막으로 'select_node' 및 'changed' 이벤트를 트리거하여 선택한 노드 및 선택된 노드의 배열을 제공합니다.

 

결론적으로 선택된 노드가 많을수록 재귀 함수는 계속해서 돌아가게 됩니다. 재귀적으로 돌면서 Node를 선택하고, 선택한 노드를 열고를 5000번 이상 반복한다는 것이죠. 5000번의 반복은 컴퓨터 상 크게 문제없어 보이지만, 제일 큰 문제는 'Tree를 재귀적으로 돈다'입니다.

 

예시를 들자면, A 노드 > B 노드 > C 노드 순으로 부모 자식 관계를 맺고 있을 때, 내가 선택했던 노드가 C라면 jstree에서는 바로 C를 select_node 하는 것이 아니라, 가장 상위인 A 노드부터 선택하면서 아래로 내려간다는 뜻입니다.

 

내가 선택했던 노드가 B, C 2개라면 상식적으로는 B를 선택하고 이어서 바로 C를 선택하면 된다라고 생각하지만, B를 선택할 때도 A부터 내려가면서 확인하고, 다시 C를 선택할 때도 A부터 다시 내려가는 과정을 거치게 됩니다.

 

대용량 조직일 경우에는 하위 조직이 무수히 많고, 그 하위 조직의 하위 조직, 하위 조직의 하위 조직...으로 구성됩니다. 그러면 select_node 할 때마다 무수히 많은 시간이 걸리게 될 수밖에 없는 거죠

 

그래서 이럴 경우에는 js에서 처리하는 것보다는 java단에서 처리하는 것이 효율적입니다. 아래서 확인해 볼게요!

 

Service(Java)📗

@Override
public JSONObject getTreeGroupList(Map<String, String> paramMap) {
    JSONObject responseJson = new JSONObject();
    JSONParser parser = new JSONParser();

    JSONArray selectedGroupArr = null;

    try {
        selectedGroupArr = (JSONArray) parser.parse(paramMap.get("targetGroup").toString());
    } catch (ParseException e) {
        logger.error(e.toString(),e);
    }

    List<Map<String,Object>> results = settingMapper.getTreeGroupList(paramMap);
    List<JSONObject> jsonGroup = new ArrayList<JSONObject>();

    for(int i=0; i<results.size(); i++){
       Map<String,Object> map = results.get(i);
       JSONObject group= new JSONObject();
       group.put("id",map.get("group_code").toString());
       if(selectedGroupArr.contains(map.get("group_code").toString())) {
            obj.put("selected",true);
            obj.put("opened",true);
       }
       group.put("state",obj);
       jsonGroup.add(group);
    }

    responseJson.put("jsonGroup", jsonGroup);

    return JSONResponseUtil.setResponseJsonUtil(responseJson);
 }

 

ajax를 이용하여 java단으로 paramMap안에 targetGroup을 넘겼습니다. 여기서 targetGroup은 선택될 node의 id값들을 모은 배열입니다. JSONArrayJSONParser를 이용하여 넘겨받은 id값들을 selectedGroupArr 변수에 담았습니다.

 

이후에 getTreeGroupList를 불러와서 Tree 데이터를 모두 results에 담았습니다. 이제 모든 tree node들을 돌면서 selectedGroupArr과 비교하면서 값들을 설정해줘야 합니다.

 

조건은 간단하죠? 만약 내가 가져온 selectedGroupArr이 현재 돌고 있는 node의 id값과 같다면 해당 node는 선택되어야 한다.

 

여기서 selected와 opened가 나옵니다. jstree.js에서는 내부적으로 각 node마다 state 값을 가지고 있고, state 안의 selected와 opened라는 설정값이 존재합니다.(jstree.js 뜯어보시면 자세한 코드 확인할 수 있습니다)

 

그러면 우리는 이 설정값들을 이용해서 아까 만든 비교문에서 통과한 node들에게 true로 설정해 주면 끝이 납니다. 그리고 설정값들을 당연히 state 값에 넣고, 이 state를 해당 group에 넣어줘야 하는 거죠. 그리고 모든 node를 돌았으면 각 node들이 모두 group으로 변환되어 jsonGroup에 들어가게 될 겁니다. 물론 선택한 값들은 selected와 opened가 true로 된 상태로요!

 

그러면 이 값들을 다시 js로 넘겨주고, 아까 작성한 select_node 관련 코드를 지워주면 성능이 빨라진 것을 확인할 수 있습니다.

 

테스트 진행

 

 

마치며

사실 포스팅만 보면 간단하지만, 저와 같은 상황(대규모 조직도 선택)을 겪은 내용의 포스팅은 하나도 없어서 jstree 내부를 뜯어보고 이해하느라 많은 시간이 걸렸었습니다. 이러한 상황을 겪을 분이 많지는 않겠지만 한 분이라도 도움이 되셨으면 합니다. jstree 관련 포스팅은 이번 포스팅을 마지막으로 마무리할게요😁


👨‍👩‍👦‍👦 오픈채팅방 운영

취업을 준비하는 예비 개발자분들을 위한 질문&답변할 수 있는 공간을 만들었습니다. 취업과 이직을 하기 위해서 어떤 걸 중점적으로 준비해야 하는지부터 포트폴리오&이력서 작성법 등 다양한 질문들을 받고 답변을 드립니다. 참여하셔서 다양한 정보 얻고 가시면 좋을 것 같네요😁

 

참여코드 : 456456

https://open.kakao.com/o/gVHZP8dg

 

비전공 개발자 취업 준비방(질문&답변)

#비전공 #개발자 #취업 #멘토링 #부트캠프 #국비지원 #백엔드 #프론트엔드 #중소기업 #중견기업 #자바 #Java #sql

open.kakao.com

 


👨‍💻 전자책 출간

아울러 제가  🌟비전공자에서 2년 만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해 주세요😁

https://kmong.com/gig/480954

 

비전공개발자 2년만에 중견기업 들어간 방법 | 14000원부터 시작 가능한 총 평점 0점의 전자책, 취

0개 총 작업 개수 완료한 총 평점 0점인 Binco의 전자책, 취업·이직 전자책 서비스를 0개의 리뷰와 함께 확인해 보세요. 전자책, 취업·이직 전자책 제공 등 14000원부터 시작 가능한 서비스

kmong.com


 

 

관련 포스팅

* [ jstree ] 상하위 노드 입맛대로 선택하는 방법

* [ jstree ] 이전 선택 값 남아있는 오류 해결하기

반응형