관련 지식
javascript, jquery

2018년 8월 NHN의 Meetup 에 ‘100,000개의 아이템도 거뜬한 셀렉트박스 만들기’ 라는 게시물이 올라왔습니다.

100,000개의 아이템도 거뜬한 셀렉트박스 만들기 : https://meetup.toast.com/posts/160?utm_source=gaerae.com&utm_campaign=%25EA%25B0%259C%25EB%25B0%259C%25EC%259E%2590%25EC%258A%25A4%25EB%259F%25BD%25EB%258B%25A4&utm_medium=social

사실 하나의 셀렉트박스에 10만개의 항목이 들어가면 화면 기획 자체가 잘못된 것이 아닌가 싶지만(선택할 항목 찾기도 힘들것 같은데;) 그런 환경을 만든다고 하니 똑같이 그렇게 만들어 볼려고 합니다. 하지만 저는 jquery 로 만들려고 합니다.

일단 제가 다룰려는 내용을 보시기 전에 위 링크에 있는 내용의 jquery 예제를 만들어보고, 개선한 코드를 만들어 속도 차이와 기능의 차이를 확인해보시는 것이 좋습니다. 제가 다시 만든 소스는 맨 하단에 있습니다.

개선된 코드의 문제점

위 링크에 나와있는 내용을 직접 구현해서 테스트 해보면 알겠지만 속도 차이는 굉장합니다. 그런데 개선한 코드에는 문제가 없을까요? 글쓰신 분도 언급했지만 셀렉트 박스를 클릭했을때 화면에 10만개의 아이템을 보여주기 위한 처리는 빠져있으므로 셀렉트 박스를 만드는 부분에 대해서만 체크하겠습니다.

혹시 ‘100,000개의 아이템도 거뜬한 셀렉트박스 만들기’ 게시물의 개선된 코드를 따라 만들기 힘드신 분도 계실듯 합니다. 아래 내용은 개선된 scripts.js 의 내용입니다. ChangeQueue.js 의 소스는 변경되는 부분이 없으므로 본문내용에 보이는 것을 그대로 쓰시면 됩니다.

  1. // 랙을 보기 위한 카운터
  2. var counter = 0;
  3. $(function() {
  4. // 매 초마다 카운터 갱신
  5. setInterval(function() {
  6. counter++;
  7. $('#counter').html(counter);
  8. }, 1000);
  9. // 데이터 생성
  10. var data = [];
  11. for (var i = 0; i < 100000; i++) {
  12. data.push('아이템' + i);
  13. }
  14. // 클릭 이벤트 핸들러
  15. $('#title').click(function(e) {
  16. $('#popup').toggle();
  17. });
  18. console.time();
  19. // 데이터로 셀렉트박스 항목 만들기
  20. for (let i = 0; i < data.length; i++) {
  21. changeQueue.enqueue({
  22. execute : function(fragment) {
  23. const elem = createItem(data[i]);
  24. // 입력받은 fragment에 추가
  25. fragment.appendChild(elem);
  26. }
  27. });
  28. }
  29. requestIdleCallback(processChanges);
  30. });
  31. function processChanges(deadline) {
  32. // DocumentFragment 생성
  33. var fragment = document.createDocumentFragment();
  34. while (deadline.timeRemaining() > 0 && !changeQueue.isEmpty()) {
  35. var c = changeQueue.dequeue();
  36. if (c)
  37. c.execute(fragment);
  38. }
  39. requestAnimationFrame(function() {
  40. // 개별 <li>태그 대신 fragment를 추가
  41. document.getElementById('list').appendChild(fragment);
  42. });
  43. if (!changeQueue.isEmpty())
  44. requestIdleCallback(processChanges);
  45. else
  46. console.timeEnd();
  47. }
  48. function createItem(d) {
  49. var elem = document.createElement('li');
  50. elem.textContent = d;
  51. elem.classList.add('item');
  52. elem.addEventListener('click', function() {
  53. $('#title').html(d);
  54. $('#popup').hide();
  55. });
  56. return elem;
  57. }

어떠한 이유에서 이렇게 개선한것인지에 대한것은 본문에 잘 나와있으니 생략하겠습니다. 이 코드에서 나중에 문제가 생길수 있는 부분은 세 가지 입니다.

  1. 브라우저 호환성이 떨어지는 requestIdleCallback() 함수의 사용
  2. 코드의 복잡성 증가
  3. 화면의 느린 반응

1. 브라우저 호환성이 떨어지는 requestIdleCallback() 함수의 사용

http://caniuse.com‘ 에서 확인을 해보면 requestIdleCallback() 함수는 호환성이 매우 떨어집니다. Internet Explorer 전체가 지원을 안할 뿐더러 IOS의 사파리에서도 지원을 하지 않습니다. 어느 브라우저까지 지원을 해야할지 고민하는 웹 사이트에서 이 정도로 호환성이 안 좋은 함수는 범용적으로 사용하기가 매우 나쁩니다.
따라서 대고객 사이트에선 10만개의 셀렉터가 생겼을때 위의 방법을 사용하기가 어렵습니다. 사실 전 ‘requestIdleCallback()’ 이런 함수가 있다는거 처음 알았습니다.

2. 코드의 복잡성 증가

복잡한 코드는 예기치 못한 오류를 발생시키고 유지보수를 어렵게 만듭니다. 만약 초기 개발 이후 발견된 오류를 다른 개발자가 수정하려고 할 경우 간단하게 수정은 어려울 것입니다. 특히 별다른 기능이 없을 셀렉트박스를 위해 이 정도로 코딩을 해야할 지는 의문입니다.

3. 화면의 느린 반응

아래와 같은 HTML이 있다고 가정합니다.

  1. <select>
  2. <option value="1">아이템1</option>
  3. <option value="2">아이템2</option>
  4. <option value="3">아이템3</option>
  5. </select>

특별히 선택한 값이 없다면 첫번째 항목인 ‘아이템1’ 이 선택된 상태로 보이게 할 것입니다. 하지만 수정 화면같이 이미 입력된 값이 있다면 다른 항목이 선택된 채로 보이게 하기 위해 아래 코드를 쓰는것은 드문일이 아닐것입니다.

아이템3 선택)

  1. $(function() {
  2. $("select").val("3");
  3. });

하지만 개선된 코드는 셀렉트박스의 항목을 한번에 만드는것이 아니라 requestIdleCallback() 함수를 통해 브라우저에 실행 여유가 있을때마다 비동기로 항목을 추가하기 때문에 필요한 시점에 셀렉트박스의 항목에 아직 없을수도 있습니다. 실제로 제 PC에서는 10만개의 아이템을 만들기까지 3.37초 정도가 걸렸기 떄문에 3.37초 이내에서는 자바스크립트로 마지막 아이템을 선택하는 것이 불가능했습니다.(개발자 도구에 시간이 찍힙니다.)

최초 구현 코드의 문제점

이번엔 극악한 실행 시간을 보인 최초 구현 코드를 점검 해보겠습니다.

  1. // 랙을 보기 위한 카운터
  2. var counter = 0;
  3. $(function() {
  4. // 매 초마다 카운터 갱신
  5. setInterval(function() {
  6. counter++;
  7. $('#counter').html(counter);
  8. }, 1000);
  9. // 데이터 생성
  10. var data = [];
  11. for (var i = 0; i < 100000; i++) {
  12. data.push('아이템' + i);
  13. }
  14. // 클릭 이벤트 핸들러
  15. $('#title').click(function(e) {
  16. $('#popup').toggle();
  17. });
  18. console.time();
  19. // 데이터로 셀렉트박스 항목 만들기
  20. for (var i = 0; i < data.length; i++) {
  21. var elem = createItem(data[i]);
  22. $('#list').append(elem);
  23. }
  24. console.timeEnd();
  25. });
  26. function createItem(d) {
  27. var elem = $('<li>' + d + '</li>');
  28. elem.addClass('item');
  29. // 아이템 클릭 시 선택되도록 함
  30. elem.click(function() {
  31. $('#title').html(d);
  32. $('#popup').hide();
  33. });
  34. return elem;
  35. }

사실 개선된 소스와 차이점은 한 곳밖에 없습니다.

  1. $(function() {
  2. ... 생략 ...
  3. // 데이터로 셀렉트박스 항목 만들기
  4. for (var i = 0; i < data.length; i++) {
  5. var elem = createItem(data[i]);
  6. $('#list').append(elem);
  7. }
  8. console.timeEnd();
  9. });
  10. function createItem(d) {
  11. var elem = $('<li>' + d + '</li>');
  12. elem.addClass('item');
  13. // 아이템 클릭 시 선택되도록 함
  14. elem.click(function() {
  15. $('#title').html(d);
  16. $('#popup').hide();
  17. });
  18. return elem;
  19. }

즉 항목을 만들기 위해 추가하는 부분이 느린것입니다. 왜 느릴까요? 아마 한 가지는 금방 짐작하실수 있겠지만 다른것은 놓치실수도 있을것 같습니다.

  1. 동일 객체의 반복 탐색
  2. Element 의 추가
  3. Element 의 동적 생성

1. 동일 객체의 반복 탐색

$('#list') 의 경우 아이디가 list 인 DOM을 문서의 처음에서 부터 찾게 됩니다. 하지만 데이터 건수 만큼 문서 탐색을 반복적으로 하게 되므로 문서의 복잡도가 높을수록 느릴수 밖에 없어 집니다. 그러나 이 샘플에선 HTML이 매우 심플하여 그다지 영향이 없습니다.

2. Element 의 추가

append() 함수는 JQuery 를 사용하면서 종종 호출하는 함수일 것입니다. 단건 호출로 사용할때는 느린 함수가 아니지만 여기서는 굉장히 부하를 일으키는 함수입니다. 아래처럼 주석으로 막고 실행 속도를 재 보았습니다.

  1. $('#list');//.append(elem);

수정 전 약 12초)

수정 후 약 8초)

약 4초 정도의 시간이 줄어들었습니다. JQuery의 append() 함수가 잘못된 것일까요? 아닙니다. 그 함수는 그럴수 밖에 없는 함수입니다. 항상 노드의 마지막에 붙이기 위해 추가 연산이 동작하는 함수이기 때문에 원래 느릴 수밖에 없는 것입니다. 단지 그 기능이 필요한 상황에서 유용할 뿐입니다. 자료구조에서 Single Linked List 의 가장 마지막에 데이터를 넣기 위해 시간이 얼마가 걸리는지 생각하면 좋을것 같습니다.

3. Element 의 동적 생성

이번엔 아래 소스를 없애고 테스트 해보겠습니다.

삭제)

  1. elem.click(function() {
  2. $('#title').html(d);
  3. $('#popup').hide();
  4. });

2초 정도가 더 줄었습니다. 이벤트를 많이 만드는것은 브라우저에 좋지 않습니다. 이벤트를 감지하기 위해 브라우저가 일을 해야하기 때문입니다.

더 수정해서 아래 소스도 바꿔보겠습니다.

수정 전)

  1. var elem = $('<li>' + d + '</li>');
  2. elem.addClass('item');

수정 후)

  1. var elem = "a";

약 6초 정도에서 1초 이하로 바뀌었습니다. 왜 일까요? $(문자열) 은 단순히 JQuery 객체를 만드는것이 아니라 문자열에 해당하는 HTML DOM을 만드는 작업이기도 합니다. 이전에 append()부분을 막았기 때문에 문서의 어디에도 추가되진 않았지만 브라우저 내에서 DOM 요소는 만들어진다는 것입니다. DOM을 만드는 비용은 공짜가 아닙니다.

최초 구현 예제를 JQuery로 개선하기

최초 구현 예제는 이미 JQuery로 만들어져 있지만 대단히 잘못 만들어진 샘플입니다. 느릴수 밖에 없는 샘플이기 때문입니다. 이것을 동일한 JQuery 함수를 이용하여 개선해 볼것입니다. 물론 브라우저 호환성 문제도 없앨 것입니다.

1. createItem() 함수 수정

  1. function createItem(d) {
  2. var elem = '<li class="item">' + d + '</li>';
  3. return elem;
  4. }

JQuery 객체를 생성하던 부분을 클래스 속성을 포함한 문자열로 바꿨습니다. 문자열이기 때문에 이벤트 추가 같은것은 하지 않습니다.

2. append() 호출 방법 변경

  1. var str = "";
  2. for (var i = 0; i < data.length; i++) {
  3. var elem = createItem(data[i]);
  4. str += elem;
  5. }
  6. $("#list").html(str);

return 받은 문자열을 바로 문서에 넣지 않습니다. 하나의 문자열로 계속 합친 후 단 한번만 html()함수를 사용했습니다. 10만개의 li 태그를 포함한 문자열이 한번에 문서에 들어갈 것을 기대해봅니다.

3. 이벤트 델리게이션 패턴

위 방식만 적용해도 10만개의 셀렉트박스는 만들어지지만 이벤트 클릭이 없기 때문에 항목 선택은 불가능합니다. 개개의 li 태그에 이벤트를 등록하지 않고 ul 태그 하나에만 클릭 이벤트를 등록하는 이벤트 델리게이션 패턴을 사용할 것입니다.

추가)

  1. $("#list").click(function(e) {
  2. var $this = $(e.target);
  3. $("#title").text($this.text());
  4. $("#popup").hide();
  5. });

시간을 재볼까요?

약 0.5초 정도가 걸립니다. 기능도 완전히 동일하게 동작합니다.

정리

사실 화면에서 하나의 기능 떄문에 0.5초가 걸리는 것도 바람직 하지 않습니다.
그러나 이 코드는 Meetup 에서 제시한 코드보다 절대적으로 유리한 부분이 있습니다.

  1. 모든 브라우저에서 사용 가능
  2. 쉬운 코드
  3. 화면이 열렸을때 모든 항목이 셀렉트 박스에 있음

실제로 제가 Meetup 링크를 받고 내용을 보면서 최초 구현소스를 제가 다시 만들어 개선된 코드와 성능 비교를 하는데까지 30분이 안걸렸습니다. 물론 최초 구현소스를 보고 ‘샘플이 이상한데?’ 싶어 이후 게시물은 제대로 읽지 않고 개선된 코드를 따라하기만 했기 때문입니다.

JQuery 는 Javascript 를 편리하게 사용하자가 취지이기 때문에 바닐라 자바스크립트보다 조금은 느릴수 밖에 없습니다. 그러나 그 특징을 잘 알고 사용한다면 대부분의 기능에서 느리다는 느낌을 받지 않을 것입니다.

셀렉트박스 외 다른 기능으로 화면 딜레이가 더 발생한다면 어떤 방식을 사용할지 고민 해야겠지만 그렇지 않다면 이 방식을 사용하는 것이 절대적으로 유리할 것입니다.

최종 소스

  1. // 랙을 보기 위한 카운터
  2. var counter = 0;
  3. $(function() {
  4. // 매 초마다 카운터 갱신
  5. setInterval(function() {
  6. counter++;
  7. $('#counter').html(counter);
  8. }, 1000);
  9. // 데이터 생성
  10. var data = [];
  11. for (var i = 0; i < 100000; i++) {
  12. data.push('아이템' + i);
  13. }
  14. // 클릭 이벤트 핸들러
  15. $('#title').click(function(e) {
  16. $('#popup').toggle();
  17. });
  18. console.time();
  19. $("#list").click(function(e) {
  20. var $this = $(e.target);
  21. $("#title").text($this.text());
  22. $("#popup").hide();
  23. });
  24. // 데이터로 셀렉트박스 항목 만들기
  25. var str = "";
  26. for (var i = 0; i < data.length; i++) {
  27. var elem = createItem(data[i]);
  28. str += elem;
  29. }
  30. $("#list").html(str);
  31. console.timeEnd();
  32. });
  33. function createItem(d) {
  34. var elem = '<li class="item">' + d + '</li>';
  35. return elem;
  36. }